Skip to content

Commit 0b0059a

Browse files
committed
feat(agents): Initial commit
1 parent 429186b commit 0b0059a

18 files changed

Lines changed: 437 additions & 10 deletions

packages/agents/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
[Unreleased]: https://github.com/MetaMask/ocap-kernel/

packages/agents/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# `@ocap/agents`
2+
3+
A starting point for LLM integration
4+
5+
## Installation
6+
7+
`yarn add @ocap/agents`
8+
9+
or
10+
11+
`npm install @ocap/agents`
12+
13+
## Contributing
14+
15+
This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme).

packages/agents/package.json

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
{
2+
"name": "@ocap/agents",
3+
"version": "0.0.0",
4+
"private": true,
5+
"description": "A starting point for LLM integration",
6+
"homepage": "https://github.com/MetaMask/ocap-kernel/tree/main/packages/agents#readme",
7+
"bugs": {
8+
"url": "https://github.com/MetaMask/ocap-kernel/issues"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "https://github.com/MetaMask/ocap-kernel.git"
13+
},
14+
"type": "module",
15+
"exports": {
16+
".": {
17+
"import": {
18+
"types": "./dist/index.d.mts",
19+
"default": "./dist/index.mjs"
20+
},
21+
"require": {
22+
"types": "./dist/index.d.cts",
23+
"default": "./dist/index.cjs"
24+
}
25+
},
26+
"./package.json": "./package.json"
27+
},
28+
"files": [
29+
"dist/"
30+
],
31+
"scripts": {
32+
"build": "ts-bridge --project tsconfig.build.json --clean",
33+
"build:docs": "typedoc",
34+
"changelog:validate": "../../scripts/validate-changelog.sh @ocap/agents",
35+
"clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist",
36+
"lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies",
37+
"lint:dependencies": "depcheck",
38+
"lint:eslint": "eslint . --cache",
39+
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies",
40+
"lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path ../../.gitignore",
41+
"publish:preview": "yarn npm publish --tag preview",
42+
"test": "vitest run --config vitest.config.ts",
43+
"test:clean": "yarn test --no-cache --coverage.clean",
44+
"test:dev": "yarn test --mode development",
45+
"test:verbose": "yarn test --reporter verbose",
46+
"test:watch": "vitest --config vitest.config.ts"
47+
},
48+
"devDependencies": {
49+
"@arethetypeswrong/cli": "^0.17.4",
50+
"@metamask/auto-changelog": "^5.0.1",
51+
"@metamask/eslint-config": "^14.0.0",
52+
"@metamask/eslint-config-nodejs": "^14.0.0",
53+
"@metamask/eslint-config-typescript": "^14.0.0",
54+
"@metamask/logger": "workspace:^",
55+
"@metamask/ocap-kernel": "workspace:^",
56+
"@ocap/cli": "workspace:^",
57+
"@ocap/nodejs": "workspace:^",
58+
"@ocap/test-utils": "workspace:^",
59+
"@ts-bridge/cli": "^0.6.3",
60+
"@ts-bridge/shims": "^0.1.1",
61+
"@types/node": "^22.13.1",
62+
"@typescript-eslint/eslint-plugin": "^8.29.0",
63+
"@typescript-eslint/parser": "^8.29.0",
64+
"@typescript-eslint/utils": "^8.29.0",
65+
"@vitest/eslint-plugin": "^1.1.44",
66+
"depcheck": "^1.4.7",
67+
"eslint": "^9.23.0",
68+
"eslint-config-prettier": "^10.1.1",
69+
"eslint-import-resolver-typescript": "^4.3.1",
70+
"eslint-plugin-import-x": "^4.10.0",
71+
"eslint-plugin-jsdoc": "^50.6.9",
72+
"eslint-plugin-n": "^17.17.0",
73+
"eslint-plugin-prettier": "^5.2.6",
74+
"eslint-plugin-promise": "^7.2.1",
75+
"prettier": "^3.5.3",
76+
"rimraf": "^6.0.1",
77+
"typedoc": "^0.28.1",
78+
"typescript": "~5.8.2",
79+
"typescript-eslint": "^8.29.0",
80+
"vite": "^6.3.5",
81+
"vitest": "^3.1.3"
82+
},
83+
"engines": {
84+
"node": "^20 || >=22"
85+
},
86+
"dependencies": {
87+
"@endo/far": "^1.1.11",
88+
"@metamask/streams": "workspace:^",
89+
"ollama": "^0.5.16"
90+
}
91+
}

packages/agents/src/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import greet from './index.ts';
4+
5+
describe('Test', () => {
6+
it('greets', () => {
7+
const name = 'Huey';
8+
const result = greet(name);
9+
expect(result).toBe('Hello, Huey!');
10+
});
11+
});

packages/agents/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Example function that returns a greeting for the given name.
3+
*
4+
* @param name - The name to greet.
5+
* @returns The greeting.
6+
*/
7+
export default function greet(name: string): string {
8+
return `Hello, ${name}!`;
9+
}

packages/agents/src/llm.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import '@ocap/test-utils/mock-endoify';
2+
import { consoleTransport, Logger } from '@metamask/logger';
3+
import { kunser } from '@metamask/ocap-kernel';
4+
import type { Kernel } from '@metamask/ocap-kernel';
5+
import { makeKernel } from '@ocap/nodejs';
6+
import { expect, describe, it, beforeEach } from 'vitest';
7+
8+
import { getBundleSpec } from './vats/index.ts';
9+
10+
const logger = new Logger({
11+
tags: ['test'],
12+
transports: [consoleTransport],
13+
});
14+
15+
describe('llm', () => {
16+
let kernel: Kernel;
17+
beforeEach(async () => {
18+
const { port2: port } = new MessageChannel();
19+
kernel = await makeKernel({
20+
port,
21+
dbFilename: ':memory:',
22+
logger,
23+
});
24+
});
25+
26+
it(
27+
'should be able to generate text',
28+
{
29+
timeout: 5_000,
30+
},
31+
async () => {
32+
console.log('Starting test');
33+
const prompt = 'Count to 6. Do not use any words.';
34+
const result = await kernel.launchSubcluster({
35+
bootstrap: 'user',
36+
vats: {
37+
user: {
38+
bundleSpec: getBundleSpec('user'),
39+
parameters: { name: 'Alice', prompt },
40+
},
41+
ollama: {
42+
bundleSpec: getBundleSpec('ollama'),
43+
},
44+
},
45+
});
46+
expect(result).toBeDefined();
47+
const llmResponse = kunser(result as Parameters<typeof kunser>[0]);
48+
expect(typeof llmResponse).toBe('string');
49+
expect(llmResponse).toMatch(/1[^0-9]*2[^0-9]*3[^0-9]*4[^0-9]*5/u);
50+
},
51+
);
52+
});

packages/agents/src/llm.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { makeFarGenerator } from '@metamask/streams/vat';
2+
import type { Config as OllamaConfig, Message as ChatMessage } from 'ollama';
3+
import { Ollama } from 'ollama/browser';
4+
5+
const defaultConfig = {
6+
model: 'llama3.2:latest',
7+
baseUrl: 'http://localhost:11434',
8+
};
9+
10+
export type Llm = Awaited<ReturnType<typeof makeLlm>>;
11+
12+
/**
13+
* Connect to a local LLM instance.
14+
*
15+
* @param config - The configuration for the LLM.
16+
* @returns An LLM instance.
17+
*/
18+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
19+
export async function makeLlm(config: Partial<OllamaConfig> = {}) {
20+
const ollamaConfig = { ...defaultConfig, ...config };
21+
const ollama = new Ollama(ollamaConfig);
22+
const { model } = ollamaConfig;
23+
// await ollama.pull({ model });
24+
25+
/**
26+
* Generate a response from the LLM.
27+
*
28+
* @param prompt - The prompt to generate a response for.
29+
* @returns An iterator that yields the response.
30+
*/
31+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
32+
const generate = async (prompt: string) => {
33+
const response = await ollama.generate({ model, prompt, stream: true });
34+
const iterator = response[Symbol.asyncIterator]();
35+
return makeFarGenerator(iterator);
36+
};
37+
38+
/**
39+
* Chat with the LLM.
40+
*
41+
* @param messages - The messages to chat with.
42+
* @returns An iterator that yields the response.
43+
*/
44+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
45+
const chat = async (messages: ChatMessage[]) => {
46+
const response = await ollama.chat({ model, messages, stream: true });
47+
const iterator = response[Symbol.asyncIterator]();
48+
return makeFarGenerator(iterator);
49+
};
50+
51+
return {
52+
generate,
53+
chat,
54+
};
55+
}

packages/agents/src/vats/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Get the bundle spec for a given bundle name.
3+
*
4+
* @param bundleName - The name of the bundle.
5+
* @returns The bundle spec.
6+
*/
7+
export const getBundleSpec = (bundleName: string): string => {
8+
try {
9+
return `file://${new URL(`./${bundleName}.bundle`, import.meta.url).pathname}`;
10+
} catch (error) {
11+
throw new Error(
12+
`Failed to getBundleSpec for ${bundleName}: ${error instanceof Error ? error.message : 'Unknown error'}`,
13+
);
14+
}
15+
};

packages/agents/src/vats/ollama.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Far } from '@endo/far';
2+
3+
import { makeLlm } from '../llm.ts';
4+
5+
/**
6+
* Build the root object for an ollama vat.
7+
*
8+
* @returns {object} The root object.
9+
*/
10+
export async function buildRootObject() {
11+
return Far('root', {
12+
makeLlm: async (config) => Far('llm', { ...(await makeLlm(config)) }),
13+
});
14+
}

packages/agents/src/vats/user.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { E, Far } from '@endo/far';
2+
// eslint-disable-next-line import-x/no-unresolved
3+
import { makeEventualIterator } from '@metamask/streams/vat';
4+
5+
/**
6+
* Build the root object for a synthetic user.
7+
*
8+
* @param {*} _ - Unused.
9+
* @param {*} parameters - The parameters to use to build the root object.
10+
* @param {string} parameters.name - The name of the user.
11+
* @param {string} parameters.prompt - The prompt the user gives to the LLM.
12+
*
13+
* @returns {object} The root object.
14+
*/
15+
export function buildRootObject(_, { name, prompt }) {
16+
let llm;
17+
return Far(`user:${name}`, {
18+
bootstrap: async ({ ollama }, _services) => {
19+
console.log('Bootstrapping user');
20+
llm = await E(ollama).makeLlm();
21+
const responseStream = makeEventualIterator(
22+
await E(llm).generate(prompt),
23+
);
24+
let response = '';
25+
for await (const chunk of responseStream) {
26+
response += chunk.response;
27+
console.log(chunk.response);
28+
}
29+
return response;
30+
},
31+
});
32+
}

0 commit comments

Comments
 (0)