Skip to content

Commit 1ab9888

Browse files
grypezclaude
andcommitted
refactor(kernel-agents): make the exo membrane the sole capability arg enforcer
Now that every capability is a pattern-guarded discoverable exo, retire the membraneless authoring and validation paths: - Remove the `capability()` authoring helper and the `validateCapabilityArgs` validator (and its now-dead module). A capability's arguments are enforced only by its exo's interface guard; the chat strategy no longer re-validates before invoking, relying on the guard rejection it already catches. - Add a `test/make-method-capability.ts` helper that wraps a single described method (an `S.method`) into a guarded discoverable-exo capability via `makeInternalCapabilities`, and migrate the chat and JSON evaluator tests (and the capability test, repurposed to cover the surviving `extract*` helpers) onto it. - Collapse the redundant `CapabilitySchema` type into kernel-utils' `MethodSchema`: a capability's `schema` is exactly the `MethodSchema` its exo describes, so the parallel type (and its `ExtractRecordKeys` helper) is gone. - Drop the unused `@metamask/superstruct` dependency. BREAKING: the `capability()` authoring helper is no longer exported from `@ocap/kernel-agents/capabilities/capability`. Author capabilities as discoverable exos (via the `described*()` combinators) and convert them with `discover`. (`validateCapabilityArgs` was internal and never exported.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c631ac4 commit 1ab9888

13 files changed

Lines changed: 112 additions & 151 deletions

packages/kernel-agents/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- 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))
1313

14+
### Removed
15+
16+
- **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))
17+
1418
[Unreleased]: https://github.com/MetaMask/ocap-kernel/

packages/kernel-agents/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@
196196
"@metamask/kernel-errors": "workspace:^",
197197
"@metamask/kernel-utils": "workspace:^",
198198
"@metamask/logger": "workspace:^",
199-
"@metamask/superstruct": "^3.2.1",
200199
"@ocap/kernel-language-model-service": "workspace:^",
201200
"partial-json": "^0.1.7",
202201
"ses": "^1.14.0"
Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
1+
import { S } from '@metamask/kernel-utils';
12
import { describe, it, expect } from 'vitest';
23

3-
import { capability } from './capability.ts';
4+
import { extractCapabilities, extractCapabilitySchemas } from './capability.ts';
5+
import { makeMethodCapability } from '../../test/make-method-capability.ts';
46

5-
describe('capability', () => {
6-
it('creates a capability with func and schema', () => {
7-
const testCapability = capability(async () => Promise.resolve('test'), {
8-
description: 'a test capability',
9-
args: {},
10-
});
11-
expect(testCapability.func).toBeInstanceOf(Function);
12-
expect(testCapability.schema).toStrictEqual({
13-
description: 'a test capability',
7+
describe('capability extraction', () => {
8+
const makeRecord = () => ({
9+
ping: makeMethodCapability(
10+
'Server',
11+
'ping',
12+
async () => 'pong',
13+
S.method('Ping', [], S.string()),
14+
),
15+
});
16+
17+
it('extractCapabilities returns the functions keyed by name', async () => {
18+
const funcs = extractCapabilities(makeRecord());
19+
expect(Object.keys(funcs)).toStrictEqual(['ping']);
20+
expect(await funcs.ping(undefined as never)).toBe('pong');
21+
});
22+
23+
it('extractCapabilitySchemas returns the schemas keyed by name', () => {
24+
const schemas = extractCapabilitySchemas(makeRecord());
25+
expect(schemas.ping).toStrictEqual({
26+
description: 'Ping',
1427
args: {},
28+
required: [],
29+
returns: { type: 'string' },
1530
});
1631
});
1732
});

packages/kernel-agents/src/capabilities/capability.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,8 @@
1-
import type { ExtractRecordKeys } from '../types/capability.ts';
2-
import type {
3-
CapabilityRecord,
4-
CapabilitySpec,
5-
CapabilitySchema,
6-
Capability,
7-
} from '../types.ts';
1+
import type { MethodSchema } from '@metamask/kernel-utils';
82

9-
/**
10-
* Create a capability specification.
11-
*
12-
* @param func - The function to create a capability specification for
13-
* @param schema - The schema for the capability
14-
* @returns A capability specification
15-
*/
16-
export const capability = <Args extends Record<string, unknown>, Return = null>(
17-
func: Capability<Args, Return>,
18-
schema: CapabilitySchema<ExtractRecordKeys<Args>>,
19-
): CapabilitySpec<Args, Return> => ({ func, schema });
3+
import type { CapabilityRecord, CapabilitySpec } from '../types.ts';
204

21-
type SchemaEntry = [string, { schema: CapabilitySchema<string> }];
5+
type SchemaEntry = [string, { schema: MethodSchema }];
226
/**
237
* Extract only the serializable schemas from the capabilities
248
*

packages/kernel-agents/src/capabilities/validate-capability-args.test.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.

packages/kernel-agents/src/capabilities/validate-capability-args.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

packages/kernel-agents/src/strategies/chat-agent.test.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import '@ocap/repo-tools/test-utils/mock-endoify';
22

3+
import { S } from '@metamask/kernel-utils';
34
import type {
45
ChatMessage,
56
ChatResult,
@@ -9,7 +10,7 @@ import { describe, expect, it, vi } from 'vitest';
910

1011
import { makeChatAgent } from './chat-agent.ts';
1112
import type { BoundChat } from './chat-agent.ts';
12-
import { capability } from '../capabilities/capability.ts';
13+
import { makeMethodCapability } from '../../test/make-method-capability.ts';
1314

1415
const makeToolCall = (
1516
id: string,
@@ -62,15 +63,17 @@ describe('makeChatAgent', () => {
6263
});
6364

6465
it('dispatches a tool call and returns final text answer', async () => {
65-
const add = vi.fn(async ({ a, b }: { a: number; b: number }) => a + b);
66-
const addCap = capability(add, {
67-
description: 'Add two numbers',
68-
args: {
69-
a: { type: 'number' },
70-
b: { type: 'number' },
71-
},
72-
returns: { type: 'number' },
73-
});
66+
const add = vi.fn(async (a: number, b: number) => a + b);
67+
const addCap = makeMethodCapability(
68+
'Math',
69+
'add',
70+
add,
71+
S.method(
72+
'Add two numbers',
73+
[S.arg('a', S.number()), S.arg('b', S.number())],
74+
S.number(),
75+
),
76+
);
7477

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

8891
const result = await agent.task('add 3 and 4');
89-
expect(add).toHaveBeenCalledWith({ a: 3, b: 4 });
92+
expect(add).toHaveBeenCalledWith(3, 4);
9093
expect(result).toBe('7');
9194
});
9295

9396
it('injects tool result message before next turn', async () => {
9497
const recorded: ChatMessage[][] = [];
95-
const ping = capability(async () => 'pong', {
96-
description: 'Ping',
97-
args: {},
98-
returns: { type: 'string' },
99-
});
98+
const ping = makeMethodCapability(
99+
'Server',
100+
'ping',
101+
async () => 'pong',
102+
S.method('Send a ping', [], S.string()),
103+
);
100104

101105
let call = 0;
102106
const chat: BoundChat = async ({ messages }) => {
@@ -152,10 +156,12 @@ describe('makeChatAgent', () => {
152156
});
153157

154158
it('throws when invocation budget is exceeded', async () => {
155-
const ping = capability(async () => 'pong', {
156-
description: 'Ping',
157-
args: {},
158-
});
159+
const ping = makeMethodCapability(
160+
'Server',
161+
'ping',
162+
async () => 'pong',
163+
S.method('Send a ping', [], S.string()),
164+
);
159165
const chat: BoundChat = async () =>
160166
makeToolCallResponse('0', [makeToolCall('c1', 'ping', {})]);
161167

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

178184
it('passes tools to the chat function', async () => {
179185
const recordedTools: unknown[] = [];
180-
const ping = capability(async () => 'pong', {
181-
description: 'Ping the server',
182-
args: {},
183-
returns: { type: 'string' },
184-
});
186+
const ping = makeMethodCapability(
187+
'Server',
188+
'ping',
189+
async () => 'pong',
190+
S.method('Ping the server', [], S.string()),
191+
);
185192

186193
const chat: BoundChat = async ({ tools }) => {
187194
recordedTools.push(tools);

packages/kernel-agents/src/strategies/chat-agent.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type {
77
import { parseToolArguments } from '@ocap/kernel-language-model-service/utils/parse-tool-arguments';
88

99
import { extractCapabilitySchemas } from '../capabilities/capability.ts';
10-
import { validateCapabilityArgs } from '../capabilities/validate-capability-args.ts';
1110
import type { Agent } from '../types/agent.ts';
1211
import { Message } from '../types/messages.ts';
1312
import type { CapabilityRecord, Experience } from '../types.ts';
@@ -178,7 +177,9 @@ export const makeChatAgent = ({
178177
let toolResult: unknown;
179178
try {
180179
const args = parseToolArguments(argsJson);
181-
validateCapabilityArgs(args, spec.schema);
180+
// The capability is backed by a pattern-guarded exo, so its
181+
// interface guard enforces the argument shape; a mismatch rejects
182+
// here and is reported as the tool error below.
182183
toolResult = await spec.func(args as never);
183184
} catch (error) {
184185
const errorContent = `Error calling ${name}: ${(error as Error).message}`;

packages/kernel-agents/src/strategies/json/evaluator.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
import { S } from '@metamask/kernel-utils';
12
import { describe, it, expect } from 'vitest';
23

34
import { makeEvaluator } from './evaluator.ts';
45
import { AssistantMessage, CapabilityResultMessage } from './messages.ts';
5-
import { capability } from '../../capabilities/capability.ts';
6+
import { makeMethodCapability } from '../../../test/make-method-capability.ts';
67

78
describe('invokeCapabilities', () => {
89
it("invokes the assistant's chosen capability", async () => {
9-
const testCapability = capability(async () => Promise.resolve('test'), {
10-
description: 'a test capability',
11-
args: {},
12-
});
10+
const testCapability = makeMethodCapability(
11+
'Test',
12+
'testCapability',
13+
async () => Promise.resolve('test'),
14+
S.method('a test capability', [], S.string()),
15+
);
1316
const evaluator = makeEvaluator({ capabilities: { testCapability } });
1417
const result = await evaluator(
1518
[],

packages/kernel-agents/src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export type {
22
Capability,
33
CapabilityRecord,
4-
CapabilitySchema,
54
CapabilitySpec,
65
} from './types/capability.ts';
76
export type { TaskArgs } from './types/task.ts';

0 commit comments

Comments
 (0)