Skip to content

Commit 1c5bb4d

Browse files
authored
feat: Add makeDiscoverableExo constructor (#705)
Closes: #649 This PR adds the `makeDiscoverableExo` constructor, which associates to a capability interface the sort of descriptive metadata needed by `@ocap/kernel-agents` to perform agentic capability composition.
1 parent 6c1f82f commit 1c5bb4d

File tree

19 files changed

+452
-35
lines changed

19 files changed

+452
-35
lines changed

packages/kernel-agents/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
"node": "^20.11 || >=22"
103103
},
104104
"dependencies": {
105+
"@endo/eventual-send": "^1.3.4",
105106
"@metamask/kernel-errors": "workspace:^",
106107
"@metamask/kernel-utils": "workspace:^",
107108
"@metamask/logger": "workspace:^",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { E } from '@endo/eventual-send';
2+
import type { DiscoverableExo, MethodSchema } from '@metamask/kernel-utils';
3+
4+
import type { CapabilityRecord, CapabilitySpec } from '../types.ts';
5+
6+
/**
7+
* Discover the capabilities of a discoverable exo. Intended for use from inside a vat.
8+
* This function fetches the schema from the discoverable exo and creates capabilities that can be used by kernel agents.
9+
*
10+
* @param exo - The discoverable exo to convert to a capability record.
11+
* @returns A promise for a capability record.
12+
*/
13+
export const discover = async (
14+
exo: DiscoverableExo,
15+
): Promise<CapabilityRecord> => {
16+
// @ts-expect-error - E type doesn't remember method names
17+
const description = (await E(exo).describe()) as Record<string, MethodSchema>;
18+
19+
const capabilities: CapabilityRecord = Object.fromEntries(
20+
Object.entries(description).map(([name, schema]) => {
21+
// Get argument names in order from the schema.
22+
// IMPORTANT: This relies on the schema's args object having keys in the same
23+
// order as the method's parameters. The schema must be defined with argument
24+
// names matching the method parameter order (e.g., for method `add(a, b)`,
25+
// the schema must have `args: { a: ..., b: ... }` in that order).
26+
// JavaScript objects preserve insertion order for string keys, so Object.keys()
27+
// will return keys in the order they were defined in the schema.
28+
const argNames = Object.keys(schema.args);
29+
30+
// Create a capability function that accepts an args object
31+
// and maps it to positional arguments for the exo method
32+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
33+
const func = async (args: Record<string, unknown>) => {
34+
// Map object arguments to positional arguments in schema order.
35+
// The order of argNames matches the method parameter order by convention.
36+
const positionalArgs = argNames.map((argName) => args[argName]);
37+
// @ts-expect-error - E type doesn't remember method names
38+
return E(exo)[name](...positionalArgs);
39+
};
40+
41+
return [name, { func, schema }] as [
42+
string,
43+
CapabilitySpec<never, unknown>,
44+
];
45+
}),
46+
);
47+
48+
return capabilities;
49+
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const search = capability(
1818
args: { query: { type: 'string', description: 'The query to search for' } },
1919
returns: {
2020
type: 'array',
21-
item: {
21+
items: {
2222
type: 'object',
2323
properties: {
2424
source: {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const add = capability(
1919
summands.reduce((acc, summand) => acc + summand, 0),
2020
{
2121
description: 'Add a list of numbers.',
22-
args: { summands: { type: 'array', item: { type: 'number' } } },
22+
args: { summands: { type: 'array', items: { type: 'number' } } },
2323
returns: { type: 'number', description: 'The sum of the numbers.' },
2424
},
2525
);
@@ -33,7 +33,7 @@ export const multiply = capability(
3333
factors: {
3434
type: 'array',
3535
description: 'The list of numbers to multiply.',
36-
item: { type: 'number' },
36+
items: { type: 'number' },
3737
},
3838
},
3939
returns: { type: 'number', description: 'The product of the factors.' },

packages/kernel-agents/src/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as indexModule from './index.ts';
66
describe('index', () => {
77
it('has the expected exports', () => {
88
expect(Object.keys(indexModule).sort()).toStrictEqual(
9-
expect.arrayContaining([]),
9+
expect.arrayContaining(['discover']),
1010
);
1111
});
1212
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export type { CapabilityRecord } from './types.ts';
2+
export { discover } from './capabilities/discover.ts';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { JsonSchema } from './json-schema.ts';
1+
import type { JsonSchema } from '@metamask/kernel-utils';
22

33
export type Capability<Args extends Record<string, unknown>, Return = null> = (
44
args: Args,

packages/kernel-agents/src/types/json-schema.ts

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

packages/kernel-agents/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
{ "path": "../kernel-language-model-service" },
1010
{ "path": "../kernel-utils" },
1111
{ "path": "../logger" },
12+
{ "path": "../ocap-kernel" },
1213
{ "path": "../repo-tools" }
1314
],
1415
"include": [
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { makeDiscoverableExo } from '@metamask/kernel-utils/discoverable';
2+
import { makeDefaultExo } from '@metamask/kernel-utils/exo';
3+
4+
/**
5+
* Build function for a vat that exports a discoverable exo capability.
6+
*
7+
* @param {*} _vatPowers - Special powers granted to this vat (not used here).
8+
* @param {*} _parameters - Initialization parameters from the vat's config object.
9+
* @param {*} _baggage - Root of vat's persistent state (not used here).
10+
* @returns {*} The root object for the new vat.
11+
*/
12+
export function buildRootObject(_vatPowers, _parameters, _baggage) {
13+
const calculator = makeDiscoverableExo(
14+
'Calculator',
15+
{
16+
add: (a, b) => a + b,
17+
multiply: (a, b) => a * b,
18+
greet: (name) => `Hello, ${name}!`,
19+
},
20+
{
21+
add: {
22+
description: 'Adds two numbers together',
23+
args: {
24+
a: {
25+
type: 'number',
26+
description: 'First number',
27+
},
28+
b: {
29+
type: 'number',
30+
description: 'Second number',
31+
},
32+
},
33+
returns: {
34+
type: 'number',
35+
description: 'The sum of the two numbers',
36+
},
37+
},
38+
multiply: {
39+
description: 'Multiplies two numbers together',
40+
args: {
41+
a: {
42+
type: 'number',
43+
description: 'First number',
44+
},
45+
b: {
46+
type: 'number',
47+
description: 'Second number',
48+
},
49+
},
50+
returns: {
51+
type: 'number',
52+
description: 'The product of the two numbers',
53+
},
54+
},
55+
greet: {
56+
description: 'Greets a person by name',
57+
args: {
58+
name: {
59+
type: 'string',
60+
description: 'The name of the person to greet',
61+
},
62+
},
63+
returns: {
64+
type: 'string',
65+
description: 'A greeting message',
66+
},
67+
},
68+
},
69+
);
70+
71+
return makeDefaultExo('root', {
72+
bootstrap() {
73+
return 'discoverable-capability-vat ready';
74+
},
75+
getCalculator() {
76+
return calculator;
77+
},
78+
});
79+
}

0 commit comments

Comments
 (0)