Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions packages/kernel-agents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- 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 ([#959](https://github.com/MetaMask/ocap-kernel/pull/959))

### Removed

- **BREAKING:** Remove the `capability()` authoring helper from `@ocap/kernel-agents/capabilities/capability`. Author capabilities as pattern-guarded discoverable exos (via the `described*()` combinators in `@metamask/kernel-utils`) and convert them with `discover`, so the exo's interface guard is the sole argument enforcer ([#960](https://github.com/MetaMask/ocap-kernel/pull/960))

[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
35 changes: 25 additions & 10 deletions packages/kernel-agents/src/capabilities/capability.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { S } from '@metamask/kernel-utils';
import { describe, it, expect } from 'vitest';

import { capability } from './capability.ts';
import { extractCapabilities, extractCapabilitySchemas } from './capability.ts';
import { makeMethodCapability } from '../../test/make-method-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: makeMethodCapability(
'Server',
'ping',
async () => 'pong',
S.method('Ping', [], S.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: {},
required: [],
returns: { type: 'string' },
});
});
});
22 changes: 3 additions & 19 deletions packages/kernel-agents/src/capabilities/capability.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,8 @@
import type { ExtractRecordKeys } from '../types/capability.ts';
import type {
CapabilityRecord,
CapabilitySpec,
CapabilitySchema,
Capability,
} from '../types.ts';
import type { MethodSchema } from '@metamask/kernel-utils';

/**
* 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 });
import type { CapabilityRecord, CapabilitySpec } from '../types.ts';

type SchemaEntry = [string, { schema: CapabilitySchema<string> }];
type SchemaEntry = [string, { schema: MethodSchema }];
/**
* 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 { S } 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 { makeMethodCapability } from '../../test/make-method-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 = makeMethodCapability(
'Math',
'add',
add,
S.method(
'Add two numbers',
[S.arg('a', S.number()), S.arg('b', S.number())],
S.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 = makeMethodCapability(
'Server',
'ping',
async () => 'pong',
S.method('Send a ping', [], S.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 = makeMethodCapability(
'Server',
'ping',
async () => 'pong',
S.method('Send a ping', [], S.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 = makeMethodCapability(
'Server',
'ping',
async () => 'pong',
S.method('Ping the server', [], S.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 { S } 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 { makeMethodCapability } from '../../../test/make-method-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 = makeMethodCapability(
'Test',
'testCapability',
async () => Promise.resolve('test'),
S.method('a test capability', [], S.string()),
);
const evaluator = makeEvaluator({ capabilities: { testCapability } });
const result = await evaluator(
[],
Expand Down
1 change: 0 additions & 1 deletion packages/kernel-agents/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export type {
Capability,
CapabilityRecord,
CapabilitySchema,
CapabilitySpec,
} from './types/capability.ts';
export type { TaskArgs } from './types/task.ts';
Expand Down
13 changes: 2 additions & 11 deletions packages/kernel-agents/src/types/capability.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import type { JsonSchema } from '@metamask/kernel-utils';
import type { MethodSchema } from '@metamask/kernel-utils';

export type Capability<Args extends Record<string, unknown>, Return = null> = (
args: Args,
) => Promise<Return>;

export type CapabilitySchema<ArgNames extends string> = {
description: string;
args: Record<ArgNames, JsonSchema>;
returns?: JsonSchema;
};

export type ExtractRecordKeys<Rec> =
Rec extends Record<infer Key, unknown> ? Key : never;

export type CapabilitySpec<
Args extends Record<string, unknown> = Record<string, unknown>,
Return = void,
> = {
func: Capability<Args, Return>;
schema: CapabilitySchema<ExtractRecordKeys<Args>>;
schema: MethodSchema;
};

export type CapabilityRecord<Keys extends string = string> = Record<
Expand Down
35 changes: 35 additions & 0 deletions packages/kernel-agents/test/make-method-capability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { S } from '@metamask/kernel-utils';
import type { DescribedMethod } from '@metamask/kernel-utils';

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

/**
* Build a capability exposing a single method, 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 `S.method` from
* `@metamask/kernel-utils`).
* @returns The capability spec.
*/
export const makeMethodCapability = (
name: string,
method: string,
impl: (...args: never[]) => unknown,
described: DescribedMethod,
): CapabilitySpec<never, unknown> => {
const capabilities = makeInternalCapabilities(
name,
{ [method]: impl } as Record<
string,
(...args: never[]) => Promise<unknown>
>,
S.interface(name, { [method]: described }),
);
return capabilities[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