Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/kernel-agents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- The built-in capabilities (`math`, `end`, `examples`) are now pattern-guarded discoverable exos authored with the `described*()` combinators, so their argument shapes are enforced by the exo's interface guard at invocation rather than only described in the prompt
- A capability's arguments are now validated solely by its exo's interface guard (the membrane); the chat strategy no longer re-validates arguments before invoking a capability

### Removed

- **BREAKING:** Remove the `capability()` authoring helper and the `validateCapabilityArgs` validator. Capabilities are authored as pattern-guarded discoverable exos (via the `described*()` combinators in `@metamask/kernel-utils`) and discovered into capability records, so there is no membraneless authoring path and the membrane is the sole argument enforcer

[Unreleased]: https://github.com/MetaMask/ocap-kernel/
1 change: 0 additions & 1 deletion packages/kernel-agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@
"@metamask/kernel-errors": "workspace:^",
"@metamask/kernel-utils": "workspace:^",
"@metamask/logger": "workspace:^",
"@metamask/superstruct": "^3.2.1",
"@ocap/kernel-language-model-service": "workspace:^",
"partial-json": "^0.1.7",
"ses": "^1.14.0"
Expand Down
34 changes: 24 additions & 10 deletions packages/kernel-agents/src/capabilities/capability.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { D, describedMethod } from '@metamask/kernel-utils';
import { describe, it, expect } from 'vitest';

import { capability } from './capability.ts';
import { extractCapabilities, extractCapabilitySchemas } from './capability.ts';
import { makeCapability } from '../../test/make-capability.ts';

describe('capability', () => {
it('creates a capability with func and schema', () => {
const testCapability = capability(async () => Promise.resolve('test'), {
description: 'a test capability',
args: {},
});
expect(testCapability.func).toBeInstanceOf(Function);
expect(testCapability.schema).toStrictEqual({
description: 'a test capability',
describe('capability extraction', () => {
const makeRecord = () => ({
ping: makeCapability(
'Server',
'ping',
async () => 'pong',
describedMethod('Ping', [], D.string()),
),
});

it('extractCapabilities returns the functions keyed by name', async () => {
const funcs = extractCapabilities(makeRecord());
expect(Object.keys(funcs)).toStrictEqual(['ping']);
expect(await funcs.ping(undefined as never)).toBe('pong');
});

it('extractCapabilitySchemas returns the schemas keyed by name', () => {
const schemas = extractCapabilitySchemas(makeRecord());
expect(schemas.ping).toStrictEqual({
description: 'Ping',
args: {},
returns: { type: 'string' },
});
});
});
14 changes: 0 additions & 14 deletions packages/kernel-agents/src/capabilities/capability.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
import type { ExtractRecordKeys } from '../types/capability.ts';
import type {
CapabilityRecord,
CapabilitySpec,
CapabilitySchema,
Capability,
} from '../types.ts';

/**
* Create a capability specification.
*
* @param func - The function to create a capability specification for
* @param schema - The schema for the capability
* @returns A capability specification
*/
export const capability = <Args extends Record<string, unknown>, Return = null>(
func: Capability<Args, Return>,
schema: CapabilitySchema<ExtractRecordKeys<Args>>,
): CapabilitySpec<Args, Return> => ({ func, schema });

type SchemaEntry = [string, { schema: CapabilitySchema<string> }];
/**
* Extract only the serializable schemas from the capabilities
Expand Down

This file was deleted.

This file was deleted.

57 changes: 32 additions & 25 deletions packages/kernel-agents/src/strategies/chat-agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '@ocap/repo-tools/test-utils/mock-endoify';

import { D, arg, describedMethod } from '@metamask/kernel-utils';
import type {
ChatMessage,
ChatResult,
Expand All @@ -9,7 +10,7 @@ import { describe, expect, it, vi } from 'vitest';

import { makeChatAgent } from './chat-agent.ts';
import type { BoundChat } from './chat-agent.ts';
import { capability } from '../capabilities/capability.ts';
import { makeCapability } from '../../test/make-capability.ts';

const makeToolCall = (
id: string,
Expand Down Expand Up @@ -62,15 +63,17 @@ describe('makeChatAgent', () => {
});

it('dispatches a tool call and returns final text answer', async () => {
const add = vi.fn(async ({ a, b }: { a: number; b: number }) => a + b);
const addCap = capability(add, {
description: 'Add two numbers',
args: {
a: { type: 'number' },
b: { type: 'number' },
},
returns: { type: 'number' },
});
const add = vi.fn(async (a: number, b: number) => a + b);
const addCap = makeCapability(
'Math',
'add',
add,
describedMethod(
'Add two numbers',
[arg('a', D.number()), arg('b', D.number())],
D.number(),
),
);

let call = 0;
const chat: BoundChat = async () => {
Expand All @@ -86,17 +89,18 @@ describe('makeChatAgent', () => {
const agent = makeChatAgent({ chat, capabilities: { add: addCap } });

const result = await agent.task('add 3 and 4');
expect(add).toHaveBeenCalledWith({ a: 3, b: 4 });
expect(add).toHaveBeenCalledWith(3, 4);
expect(result).toBe('7');
});

it('injects tool result message before next turn', async () => {
const recorded: ChatMessage[][] = [];
const ping = capability(async () => 'pong', {
description: 'Ping',
args: {},
returns: { type: 'string' },
});
const ping = makeCapability(
'Server',
'ping',
async () => 'pong',
describedMethod('Ping', [], D.string()),
);

let call = 0;
const chat: BoundChat = async ({ messages }) => {
Expand Down Expand Up @@ -152,10 +156,12 @@ describe('makeChatAgent', () => {
});

it('throws when invocation budget is exceeded', async () => {
const ping = capability(async () => 'pong', {
description: 'Ping',
args: {},
});
const ping = makeCapability(
'Server',
'ping',
async () => 'pong',
describedMethod('Ping', [], D.string()),
);
const chat: BoundChat = async () =>
makeToolCallResponse('0', [makeToolCall('c1', 'ping', {})]);

Expand All @@ -177,11 +183,12 @@ describe('makeChatAgent', () => {

it('passes tools to the chat function', async () => {
const recordedTools: unknown[] = [];
const ping = capability(async () => 'pong', {
description: 'Ping the server',
args: {},
returns: { type: 'string' },
});
const ping = makeCapability(
'Server',
'ping',
async () => 'pong',
describedMethod('Ping the server', [], D.string()),
);

const chat: BoundChat = async ({ tools }) => {
recordedTools.push(tools);
Expand Down
5 changes: 3 additions & 2 deletions packages/kernel-agents/src/strategies/chat-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
import { parseToolArguments } from '@ocap/kernel-language-model-service/utils/parse-tool-arguments';

import { extractCapabilitySchemas } from '../capabilities/capability.ts';
import { validateCapabilityArgs } from '../capabilities/validate-capability-args.ts';
import type { Agent } from '../types/agent.ts';
import { Message } from '../types/messages.ts';
import type { CapabilityRecord, Experience } from '../types.ts';
Expand Down Expand Up @@ -178,7 +177,9 @@ export const makeChatAgent = ({
let toolResult: unknown;
try {
const args = parseToolArguments(argsJson);
validateCapabilityArgs(args, spec.schema);
// The capability is backed by a pattern-guarded exo, so its
// interface guard enforces the argument shape; a mismatch rejects
// here and is reported as the tool error below.
toolResult = await spec.func(args as never);
} catch (error) {
const errorContent = `Error calling ${name}: ${(error as Error).message}`;
Expand Down
13 changes: 8 additions & 5 deletions packages/kernel-agents/src/strategies/json/evaluator.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { D, describedMethod } from '@metamask/kernel-utils';
import { describe, it, expect } from 'vitest';

import { makeEvaluator } from './evaluator.ts';
import { AssistantMessage, CapabilityResultMessage } from './messages.ts';
import { capability } from '../../capabilities/capability.ts';
import { makeCapability } from '../../../test/make-capability.ts';

describe('invokeCapabilities', () => {
it("invokes the assistant's chosen capability", async () => {
const testCapability = capability(async () => Promise.resolve('test'), {
description: 'a test capability',
args: {},
});
const testCapability = makeCapability(
'Test',
'testCapability',
async () => Promise.resolve('test'),
describedMethod('a test capability', [], D.string()),
);
const evaluator = makeEvaluator({ capabilities: { testCapability } });
const result = await evaluator(
[],
Expand Down
43 changes: 43 additions & 0 deletions packages/kernel-agents/test/make-capability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
describedInterface,
makeDiscoverableExo,
} from '@metamask/kernel-utils';
import type { DescribedMethod } from '@metamask/kernel-utils';

import { discoverLocal } from '../src/capabilities/discover.ts';
import type { CapabilitySpec } from '../src/types.ts';

/**
* Build a single capability backed by a pattern-guarded discoverable exo, for
* tests that need an ad-hoc capability. Mirrors how the built-in capabilities
* are authored, so the exo's interface guard enforces the method's argument
* shape — there is no membraneless authoring path.
*
* @param name - The exo/interface name.
* @param method - The method (and capability) name.
* @param impl - The method implementation (positional arguments).
* @param described - The method's guard and schema (use the `described*()`
* combinators from `@metamask/kernel-utils`).
* @returns The capability spec.
*/
export const makeCapability = (
name: string,
method: string,
impl: (...args: never[]) => unknown,
described: DescribedMethod,
): CapabilitySpec<never, unknown> => {
const { interfaceGuard, schemas } = describedInterface(name, {
[method]: described,
});
const exo = makeDiscoverableExo(
name,
{ [method]: impl } as Record<string, (...args: unknown[]) => unknown>,
schemas,
interfaceGuard,
);
const record = discoverLocal(exo) as Record<
string,
CapabilitySpec<never, unknown>
>;
return record[method] as CapabilitySpec<never, unknown>;
};
1 change: 0 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3989,7 +3989,6 @@ __metadata:
"@metamask/kernel-errors": "workspace:^"
"@metamask/kernel-utils": "workspace:^"
"@metamask/logger": "workspace:^"
"@metamask/superstruct": "npm:^3.2.1"
"@ocap/kernel-language-model-service": "workspace:^"
"@ocap/repo-tools": "workspace:^"
"@ts-bridge/cli": "npm:^0.6.3"
Expand Down
Loading