@oh-my-ai-sdk/omakase 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -10
- package/dist/agent-catalog.d.ts +202 -0
- package/dist/agent-catalog.js +427 -0
- package/dist/agent-runtime/manifest.json +1 -1
- package/dist/agent-runtime/slice-worker.mjs +112 -24
- package/dist/agent-runtime/versions.json +2 -2
- package/dist/agent-runtime/worker.mjs +18 -7
- package/dist/agent-slice-client.d.ts +54 -6
- package/dist/agent-slice-client.js +151 -22
- package/dist/agent-slice-contract.d.ts +4 -0
- package/dist/agent-slice.d.ts +15 -1
- package/dist/agent-slice.js +12 -5
- package/dist/agent-tasks.d.ts +112 -0
- package/dist/agent-tasks.js +398 -0
- package/dist/agents.d.ts +8 -1
- package/dist/agents.js +138 -70
- package/dist/code-mode.js +12 -2
- package/dist/durable-engine.js +23 -2
- package/dist/durable-host.d.ts +7 -4
- package/dist/durable-host.js +279 -13
- package/dist/durable-protocol.d.ts +11 -1
- package/dist/durable-protocol.js +3 -2
- package/dist/durable-worker/steps.mjs +27 -0
- package/dist/durable-worker/versions.json +1 -1
- package/dist/durable-worker/worker.mjs +100 -12
- package/dist/durable-worker/workflows.mjs +11 -6
- package/dist/durable-worker-client.d.ts +1 -0
- package/dist/durable-worker-client.js +5 -2
- package/dist/frontend.d.ts +2 -2
- package/dist/frontend.js +2 -2
- package/dist/guest-agent-error.d.ts +11 -0
- package/dist/guest-agent-error.js +50 -0
- package/dist/skills-cli.js +16 -3
- package/dist/skills-server.js +1 -1
- package/dist/workflow-host.js +9 -1
- package/dist/workflow.d.ts +4 -0
- package/examples/agent-tasks.config.mjs +63 -0
- package/package.json +5 -2
- package/skill/omakase-codemode/SKILL.md +86 -0
- package/skill/omakase-codemode/examples/0-parallel-agents.ts +76 -0
- package/skill/omakase-codemode/examples/1-decidable-probe.ts +62 -0
- package/skill/omakase-codemode/examples/2-review-branch.ts +95 -0
- package/skill/omakase-codemode/examples/3-fix-loop.ts +163 -0
- package/skill/omakase-codemode/examples/4-unit-pipeline.ts +171 -0
- package/skill/omakase-codemode/examples/5-inline-run.ts +48 -0
- package/skill/omakase-codemode/examples/6-judge-host.ts +144 -0
- package/skill/omakase-codemode/examples/7-discover-and-steer.ts +93 -0
- package/skill/omakase-codemode/examples/README.md +38 -0
- package/skill/omakase-codemode/references/briefs.md +82 -0
- package/skill/omakase-codemode/references/evidence.md +62 -0
- package/skill/omakase-codemode/references/facts.md +68 -0
- package/skill/omakase-codemode/references/gates.md +73 -0
- package/skill/omakase-codemode/references/preflight.md +48 -0
- package/skill/omakase-codemode/references/review.md +75 -0
- package/skill/omakase-codemode/references/runbook.md +82 -0
- package/skill/omakase-codemode/references/tool-surface.md +66 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host-owned agent catalog and the generic `agent` namespace it backs.
|
|
3
|
+
*
|
|
4
|
+
* `mountAgents` (agents.ts) turns trusted Flow backend references into named
|
|
5
|
+
* `agents.NAME` tools. This module adds the second, generic surface on the
|
|
6
|
+
* SAME execution machinery: `agent.run`, discriminated between a named
|
|
7
|
+
* registration (`{ agent, prompt, workspaceDir? }` — byte-identical result
|
|
8
|
+
* contract to `agents.NAME`) and an inline selection (`{ harness, provider,
|
|
9
|
+
* model, settings?, prompt, workspaceDir? }` → `{ sessionId, text }`), plus
|
|
10
|
+
* `agent.steer`, routed to the task-control port owned by agent-tasks.ts.
|
|
11
|
+
*
|
|
12
|
+
* Authority model: the host registers harnesses, the providers under each
|
|
13
|
+
* harness, the models authorized on each provider and named settings presets.
|
|
14
|
+
* Every credential pointer (`${file:…}`, `${VAR}`), sandbox env declaration,
|
|
15
|
+
* permission mode and option fragment lives in that host-owned registration
|
|
16
|
+
* and is composed into a trusted inline backend spec — the object
|
|
17
|
+
* presentation the flow provider already supports (inlineManifest). Guests
|
|
18
|
+
* pass only registered names, an authorized model id and a settings key;
|
|
19
|
+
* they can never supply executables, environment, paths or credentials, and
|
|
20
|
+
* no second backend loader exists.
|
|
21
|
+
*
|
|
22
|
+
* Identity protocol (agent-slice.ts): every agent tool registers a
|
|
23
|
+
* whole-catalog inspector (admission drift rejection — any change to the
|
|
24
|
+
* mounted authority, including unrelated additions, blocks recovery) and a
|
|
25
|
+
* selected-identity inspector resolving the per-invocation backend identity
|
|
26
|
+
* the durable host pins before native execution. The catalog fingerprint
|
|
27
|
+
* incorporates each named profile's resolved inspect identity, so content
|
|
28
|
+
* drift behind an unchanged ref string rejects recovery too. Inline specs
|
|
29
|
+
* never appear verbatim in identity, evidence or errors — only their stable
|
|
30
|
+
* content id does.
|
|
31
|
+
*/
|
|
32
|
+
import { createHash } from 'node:crypto';
|
|
33
|
+
import { tool, jsonSchema } from 'ai';
|
|
34
|
+
import { AgentHostError } from './agents.js';
|
|
35
|
+
import { agentSteerTool, invokeAgentSteer } from './agent-tasks.js';
|
|
36
|
+
import { registerAgentRecovery } from './agent-slice.js';
|
|
37
|
+
import { assertJson } from './schema.js';
|
|
38
|
+
const NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
39
|
+
const PERMISSION_MODES = ['allow-reads', 'allow-edits', 'allow-all'];
|
|
40
|
+
function isPlainObject(value) {
|
|
41
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
42
|
+
&& (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
43
|
+
}
|
|
44
|
+
function plainObject(value, where) {
|
|
45
|
+
if (!isPlainObject(value))
|
|
46
|
+
throw new AgentHostError('INVALID_INPUT', `${where} must be a plain object`);
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function rejectUnknownKeys(record, allowed, where) {
|
|
50
|
+
for (const key of Object.keys(record)) {
|
|
51
|
+
if (!allowed.includes(key))
|
|
52
|
+
throw new AgentHostError('INVALID_INPUT', `Unknown ${where} field: ${JSON.stringify(key)}; allowed: ${[...allowed].sort().join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function optionalNonblankString(value, where) {
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (typeof value !== 'string' || !value.trim())
|
|
59
|
+
throw new AgentHostError('INVALID_INPUT', `${where} must be a nonblank string when present`);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
/** Own-key lookup into a registration dictionary: names inherited from
|
|
63
|
+
* Object.prototype (`constructor`, `toString`, `__proto__`, …) never
|
|
64
|
+
* resolve — only explicitly registered own keys grant authority. */
|
|
65
|
+
function ownEntry(record, key) {
|
|
66
|
+
return record !== undefined && Object.hasOwn(record, key) ? record[key] : undefined;
|
|
67
|
+
}
|
|
68
|
+
/** A trusted, host-owned option fragment. JSON-only, cloned into the
|
|
69
|
+
* snapshot, and never allowed to declare `model`: model authority belongs to
|
|
70
|
+
* the registered model key the caller selects. */
|
|
71
|
+
function trustedOptions(value, where) {
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
const record = plainObject(value, where);
|
|
75
|
+
try {
|
|
76
|
+
assertJson(record);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw new AgentHostError('INVALID_INPUT', `${where} must be JSON-compatible (${error instanceof Error ? error.message : String(error)})`);
|
|
80
|
+
}
|
|
81
|
+
if (Object.hasOwn(record, 'model')) {
|
|
82
|
+
throw new AgentHostError('INVALID_INPUT', `${where} must not declare "model" — model authority belongs to the registered model key selected by the caller`);
|
|
83
|
+
}
|
|
84
|
+
// assertJson above established JSON compatibility; the cast states it.
|
|
85
|
+
return structuredClone(record);
|
|
86
|
+
}
|
|
87
|
+
function stringRecord(value, where) {
|
|
88
|
+
if (value === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
const record = plainObject(value, where);
|
|
91
|
+
// Null prototype: keys are freeform, so a trusted-JSON own "__proto__" key
|
|
92
|
+
// registers as the declared entry instead of rewiring this record's
|
|
93
|
+
// prototype.
|
|
94
|
+
const out = Object.create(null);
|
|
95
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
96
|
+
if (!key)
|
|
97
|
+
throw new AgentHostError('INVALID_INPUT', `${where} keys must be nonempty`);
|
|
98
|
+
if (typeof entry !== 'string' || !entry)
|
|
99
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.${key} must be a nonempty string (credential pointers like \${file:…} are resolved against the host environment only)`);
|
|
100
|
+
out[key] = entry;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function settingsLike(value, where) {
|
|
105
|
+
if (value === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
const record = plainObject(value, where);
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
110
|
+
if (!NAME_PATTERN.test(key))
|
|
111
|
+
throw new AgentHostError('INVALID_INPUT', `${where} key ${JSON.stringify(key)} must match ${NAME_PATTERN.source}`);
|
|
112
|
+
const fields = plainObject(entry, `${where}.${key}`);
|
|
113
|
+
rejectUnknownKeys(fields, ['options'], `${where}.${key}`);
|
|
114
|
+
const options = trustedOptions(fields.options, `${where}.${key}.options`);
|
|
115
|
+
out[key] = options === undefined ? {} : { options };
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/** Validates and snapshots a catalog before any host effect (namespace
|
|
120
|
+
* registration, filesystem, workers). Throws typed INVALID_INPUT naming the
|
|
121
|
+
* offending field; forbidden overrides (a `model` key in any option
|
|
122
|
+
* fragment) are rejected here, before effects, per the authority contract. */
|
|
123
|
+
export function validateAgentCatalog(catalog) {
|
|
124
|
+
const root = plainObject(catalog, 'catalog');
|
|
125
|
+
rejectUnknownKeys(root, ['harnesses'], 'catalog');
|
|
126
|
+
const harnessesRaw = plainObject(root.harnesses, 'catalog.harnesses');
|
|
127
|
+
if (Object.keys(harnessesRaw).length === 0)
|
|
128
|
+
throw new AgentHostError('INVALID_INPUT', 'catalog.harnesses must register at least one harness');
|
|
129
|
+
const harnesses = {};
|
|
130
|
+
for (const [harnessKey, harnessRaw] of Object.entries(harnessesRaw)) {
|
|
131
|
+
if (!NAME_PATTERN.test(harnessKey))
|
|
132
|
+
throw new AgentHostError('INVALID_INPUT', `catalog.harnesses key ${JSON.stringify(harnessKey)} must match ${NAME_PATTERN.source}`);
|
|
133
|
+
const where = `catalog.harnesses.${harnessKey}`;
|
|
134
|
+
const h = plainObject(harnessRaw, where);
|
|
135
|
+
rejectUnknownKeys(h, ['harness', 'description', 'instructions', 'permissionMode', 'toolApproval', 'activeTools', 'sandbox', 'options', 'providers'], where);
|
|
136
|
+
if (typeof h.harness !== 'string' || !h.harness.trim())
|
|
137
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.harness must be a nonblank adapter id (e.g. 'claude-code')`);
|
|
138
|
+
if (h.permissionMode !== undefined && !PERMISSION_MODES.includes(h.permissionMode)) {
|
|
139
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.permissionMode must be one of ${PERMISSION_MODES.join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
let activeTools;
|
|
142
|
+
if (h.activeTools !== undefined) {
|
|
143
|
+
if (!Array.isArray(h.activeTools))
|
|
144
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.activeTools must be an array of tool names`);
|
|
145
|
+
activeTools = h.activeTools.map(name => {
|
|
146
|
+
if (typeof name !== 'string' || !name)
|
|
147
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.activeTools entries must be nonempty strings`);
|
|
148
|
+
return name;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
let sandbox = {};
|
|
152
|
+
if (h.sandbox !== undefined) {
|
|
153
|
+
const raw = plainObject(h.sandbox, `${where}.sandbox`);
|
|
154
|
+
rejectUnknownKeys(raw, ['type', 'inheritEnv', 'warm'], `${where}.sandbox`);
|
|
155
|
+
if (raw.type !== undefined && raw.type !== 'local' && raw.type !== 'local-persistent')
|
|
156
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.sandbox.type must be 'local' or 'local-persistent'`);
|
|
157
|
+
if (raw.inheritEnv !== undefined && typeof raw.inheritEnv !== 'boolean')
|
|
158
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.sandbox.inheritEnv must be a boolean`);
|
|
159
|
+
if (raw.warm !== undefined && typeof raw.warm !== 'boolean')
|
|
160
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.sandbox.warm must be a boolean`);
|
|
161
|
+
sandbox = {
|
|
162
|
+
...(raw.type !== undefined ? { type: raw.type } : {}),
|
|
163
|
+
...(raw.inheritEnv !== undefined ? { inheritEnv: raw.inheritEnv } : {}),
|
|
164
|
+
...(raw.warm !== undefined ? { warm: raw.warm } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const providersRaw = plainObject(h.providers, `${where}.providers`);
|
|
168
|
+
if (Object.keys(providersRaw).length === 0)
|
|
169
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.providers must register at least one provider`);
|
|
170
|
+
const providers = {};
|
|
171
|
+
for (const [providerKey, providerRaw] of Object.entries(providersRaw)) {
|
|
172
|
+
if (!NAME_PATTERN.test(providerKey))
|
|
173
|
+
throw new AgentHostError('INVALID_INPUT', `${where}.providers key ${JSON.stringify(providerKey)} must match ${NAME_PATTERN.source}`);
|
|
174
|
+
const pWhere = `${where}.providers.${providerKey}`;
|
|
175
|
+
const p = plainObject(providerRaw, pWhere);
|
|
176
|
+
rejectUnknownKeys(p, ['description', 'models', 'options', 'sandboxEnv', 'processEnv', 'settings'], pWhere);
|
|
177
|
+
const description = optionalNonblankString(p.description, `${pWhere}.description`);
|
|
178
|
+
const providerOptions = trustedOptions(p.options, `${pWhere}.options`);
|
|
179
|
+
const sandboxEnv = stringRecord(p.sandboxEnv, `${pWhere}.sandboxEnv`);
|
|
180
|
+
const processEnv = stringRecord(p.processEnv, `${pWhere}.processEnv`);
|
|
181
|
+
const settings = settingsLike(p.settings, `${pWhere}.settings`);
|
|
182
|
+
const modelsRaw = plainObject(p.models, `${pWhere}.models`);
|
|
183
|
+
if (Object.keys(modelsRaw).length === 0)
|
|
184
|
+
throw new AgentHostError('INVALID_INPUT', `${pWhere}.models must authorize at least one model`);
|
|
185
|
+
// Null prototype: model ids are freeform, so a trusted-JSON own
|
|
186
|
+
// "__proto__" key registers as the declared entry instead of rewiring
|
|
187
|
+
// this record's prototype.
|
|
188
|
+
const models = Object.create(null);
|
|
189
|
+
for (const [modelKey, modelRaw] of Object.entries(modelsRaw)) {
|
|
190
|
+
if (!modelKey.trim())
|
|
191
|
+
throw new AgentHostError('INVALID_INPUT', `${pWhere}.models keys must be nonempty model ids`);
|
|
192
|
+
const mWhere = `${pWhere}.models.${JSON.stringify(modelKey)}`;
|
|
193
|
+
const m = plainObject(modelRaw, mWhere);
|
|
194
|
+
rejectUnknownKeys(m, ['options'], mWhere);
|
|
195
|
+
const modelOptions = trustedOptions(m.options, `${mWhere}.options`);
|
|
196
|
+
models[modelKey] = modelOptions === undefined ? {} : { options: modelOptions };
|
|
197
|
+
}
|
|
198
|
+
providers[providerKey] = {
|
|
199
|
+
key: providerKey,
|
|
200
|
+
...(description !== undefined ? { description } : {}),
|
|
201
|
+
models,
|
|
202
|
+
...(providerOptions !== undefined ? { options: providerOptions } : {}),
|
|
203
|
+
...(sandboxEnv !== undefined ? { sandboxEnv } : {}),
|
|
204
|
+
...(processEnv !== undefined ? { processEnv } : {}),
|
|
205
|
+
...(settings !== undefined ? { settings } : {}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const harnessDescription = optionalNonblankString(h.description, `${where}.description`);
|
|
209
|
+
const instructions = optionalNonblankString(h.instructions, `${where}.instructions`);
|
|
210
|
+
const toolApproval = trustedOptions(h.toolApproval, `${where}.toolApproval`);
|
|
211
|
+
const harnessOptions = trustedOptions(h.options, `${where}.options`);
|
|
212
|
+
harnesses[harnessKey] = {
|
|
213
|
+
key: harnessKey,
|
|
214
|
+
harness: h.harness,
|
|
215
|
+
...(harnessDescription !== undefined ? { description: harnessDescription } : {}),
|
|
216
|
+
...(instructions !== undefined ? { instructions } : {}),
|
|
217
|
+
...(h.permissionMode !== undefined ? { permissionMode: h.permissionMode } : {}),
|
|
218
|
+
...(toolApproval !== undefined ? { toolApproval } : {}),
|
|
219
|
+
...(activeTools !== undefined ? { activeTools } : {}),
|
|
220
|
+
sandbox,
|
|
221
|
+
...(harnessOptions !== undefined ? { options: harnessOptions } : {}),
|
|
222
|
+
providers,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return { harnesses };
|
|
226
|
+
}
|
|
227
|
+
/** Deterministic JSON with recursively sorted object keys: equal
|
|
228
|
+
* configurations hash equal regardless of authoring key order. */
|
|
229
|
+
export function canonicalJson(value) {
|
|
230
|
+
if (Array.isArray(value))
|
|
231
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
232
|
+
if (value !== null && typeof value === 'object') {
|
|
233
|
+
const keys = Object.keys(value).sort();
|
|
234
|
+
return `{${keys.map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
235
|
+
}
|
|
236
|
+
return JSON.stringify(value) ?? 'null';
|
|
237
|
+
}
|
|
238
|
+
/** Stable, secret-free fingerprint of the whole mounted agent authority.
|
|
239
|
+
* Inputs are the validated catalog snapshot, the trusted slice budget and —
|
|
240
|
+
* critically — each named profile's RESOLVED inspect identity, so content
|
|
241
|
+
* drift behind an unchanged ref string (an agent.yaml edit on the host)
|
|
242
|
+
* rejects admission recovery even for tools a pending cell never invoked.
|
|
243
|
+
* Inline config drift is covered the same way: the selected identity embeds
|
|
244
|
+
* the resolved manifest fingerprint of the composed spec. The result is a
|
|
245
|
+
* hex hash — no configuration object, pointer or secret is serialized. */
|
|
246
|
+
export function agentCatalogFingerprint(input) {
|
|
247
|
+
const material = {
|
|
248
|
+
v: 1,
|
|
249
|
+
kind: 'omakase-agent-catalog',
|
|
250
|
+
named: input.named.map(({ name, identity }) => ({ name, identity })),
|
|
251
|
+
catalog: input.catalog ?? null,
|
|
252
|
+
sliceSeconds: input.sliceSeconds,
|
|
253
|
+
};
|
|
254
|
+
return `omakase-agent-catalog-v1:${createHash('sha256').update(canonicalJson(material)).digest('hex')}`;
|
|
255
|
+
}
|
|
256
|
+
/** In-memory memo key for one backend ref: the ref itself when named, a hash
|
|
257
|
+
* of the canonical spec otherwise. Never persisted, logged or sent. */
|
|
258
|
+
export function agentBackendRefKey(ref) {
|
|
259
|
+
return typeof ref === 'string' ? ref : `inline:${createHash('sha256').update(canonicalJson(ref)).digest('hex')}`;
|
|
260
|
+
}
|
|
261
|
+
/** Deep merge of trusted option fragments: plain objects merge recursively,
|
|
262
|
+
* everything else (arrays, scalars) replaces. Later fragments win; the
|
|
263
|
+
* caller-selected model id is placed last under `options.model`. Own keys
|
|
264
|
+
* only: inherited names never merge, and a JSON-declared own `__proto__` key
|
|
265
|
+
* lands as a data property instead of rewiring the prototype. */
|
|
266
|
+
function mergeOptions(fragments) {
|
|
267
|
+
const out = {};
|
|
268
|
+
for (const fragment of fragments) {
|
|
269
|
+
if (fragment === undefined)
|
|
270
|
+
continue;
|
|
271
|
+
for (const [key, value] of Object.entries(fragment)) {
|
|
272
|
+
const prior = ownEntry(out, key);
|
|
273
|
+
const merged = isPlainObject(prior) && isPlainObject(value)
|
|
274
|
+
? mergeOptions([prior, value])
|
|
275
|
+
: value;
|
|
276
|
+
// Trusted JSON can declare an own "__proto__" data property; plain
|
|
277
|
+
// assignment would rewire out's prototype instead of merging it.
|
|
278
|
+
if (key === '__proto__')
|
|
279
|
+
Object.defineProperty(out, key, { value: merged, writable: true, enumerable: true, configurable: true });
|
|
280
|
+
else
|
|
281
|
+
out[key] = merged;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/** Validates one agent.run input against the mounted authority and resolves
|
|
287
|
+
* the backend selection. Pure: no worker, no filesystem, no effects — every
|
|
288
|
+
* unregistered or mixed selection fails here, typed, before anything runs.
|
|
289
|
+
* Registration lookups are own-key: names inherited from Object.prototype
|
|
290
|
+
* (`constructor`, `toString`, `__proto__`, …) never grant authority. */
|
|
291
|
+
export function resolveAgentRunSelection(input, named, catalog) {
|
|
292
|
+
if (!isPlainObject(input))
|
|
293
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run input must be an object');
|
|
294
|
+
if (input.agent !== undefined && typeof input.agent !== 'string')
|
|
295
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run agent must be a string when present');
|
|
296
|
+
if (input.harness !== undefined && typeof input.harness !== 'string')
|
|
297
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run harness must be a string when present');
|
|
298
|
+
if (input.provider !== undefined && typeof input.provider !== 'string')
|
|
299
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run provider must be a string when present');
|
|
300
|
+
if (input.model !== undefined && typeof input.model !== 'string')
|
|
301
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run model must be a string when present');
|
|
302
|
+
if (input.settings !== undefined && typeof input.settings !== 'string')
|
|
303
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run settings must be a string when present');
|
|
304
|
+
const { agent, harness, provider, model, settings, prompt, workspaceDir } = input;
|
|
305
|
+
if (prompt === undefined || !prompt.trim())
|
|
306
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run requires a nonblank prompt');
|
|
307
|
+
if (workspaceDir !== undefined && !workspaceDir)
|
|
308
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run workspaceDir must be a nonempty string when present');
|
|
309
|
+
const namedChosen = agent !== undefined;
|
|
310
|
+
const inlineChosen = harness !== undefined || provider !== undefined || model !== undefined || settings !== undefined;
|
|
311
|
+
if (namedChosen && inlineChosen)
|
|
312
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run takes either { agent } or { harness, provider, model, settings? }, never both');
|
|
313
|
+
if (!namedChosen && !inlineChosen)
|
|
314
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run requires either a registered agent name or a harness/provider/model selection');
|
|
315
|
+
if (namedChosen) {
|
|
316
|
+
if (!NAME_PATTERN.test(agent))
|
|
317
|
+
throw new AgentHostError('INVALID_INPUT', `agent.run agent name is invalid: ${JSON.stringify(agent)}`);
|
|
318
|
+
const registration = named.get(agent);
|
|
319
|
+
if (registration === undefined) {
|
|
320
|
+
throw new AgentHostError('INVALID_INPUT', `agent.run has no registered agent named ${JSON.stringify(agent)}; registered: ${[...named.keys()].sort().join(', ') || '(none)'}`);
|
|
321
|
+
}
|
|
322
|
+
return { kind: 'named', name: agent, ref: registration.ref, ...(registration.outputSchema !== undefined ? { outputSchema: registration.outputSchema } : {}) };
|
|
323
|
+
}
|
|
324
|
+
if (catalog === undefined)
|
|
325
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run inline selection requires a mounted harness catalog; this host registered none');
|
|
326
|
+
if (harness === undefined || provider === undefined || model === undefined) {
|
|
327
|
+
throw new AgentHostError('INVALID_INPUT', 'agent.run inline selection requires harness, provider and model');
|
|
328
|
+
}
|
|
329
|
+
const harnessRegistration = ownEntry(catalog.harnesses, harness);
|
|
330
|
+
if (harnessRegistration === undefined) {
|
|
331
|
+
throw new AgentHostError('INVALID_INPUT', `agent.run has no registered harness named ${JSON.stringify(harness)}; registered: ${Object.keys(catalog.harnesses).sort().join(', ')}`);
|
|
332
|
+
}
|
|
333
|
+
const providerRegistration = ownEntry(harnessRegistration.providers, provider);
|
|
334
|
+
if (providerRegistration === undefined) {
|
|
335
|
+
throw new AgentHostError('INVALID_INPUT', `agent.run harness ${JSON.stringify(harness)} has no provider named ${JSON.stringify(provider)}; registered: ${Object.keys(harnessRegistration.providers).sort().join(', ')}`);
|
|
336
|
+
}
|
|
337
|
+
const modelRegistration = ownEntry(providerRegistration.models, model);
|
|
338
|
+
if (modelRegistration === undefined) {
|
|
339
|
+
throw new AgentHostError('INVALID_INPUT', `agent.run model ${JSON.stringify(model)} is not authorized on ${harness}/${provider}; authorized: ${Object.keys(providerRegistration.models).sort().join(', ')}`);
|
|
340
|
+
}
|
|
341
|
+
let settingsOptions;
|
|
342
|
+
if (settings !== undefined) {
|
|
343
|
+
const preset = ownEntry(providerRegistration.settings, settings);
|
|
344
|
+
if (preset === undefined) {
|
|
345
|
+
throw new AgentHostError('INVALID_INPUT', `agent.run settings ${JSON.stringify(settings)} is not registered on ${harness}/${provider}; registered: ${Object.keys(providerRegistration.settings ?? {}).sort().join(', ') || '(none)'}`);
|
|
346
|
+
}
|
|
347
|
+
settingsOptions = preset.options;
|
|
348
|
+
}
|
|
349
|
+
const sandbox = {
|
|
350
|
+
...(harnessRegistration.sandbox.type !== undefined ? { type: harnessRegistration.sandbox.type } : {}),
|
|
351
|
+
...(providerRegistration.sandboxEnv !== undefined ? { env: providerRegistration.sandboxEnv } : {}),
|
|
352
|
+
...(harnessRegistration.sandbox.inheritEnv !== undefined ? { inheritEnv: harnessRegistration.sandbox.inheritEnv } : {}),
|
|
353
|
+
...(harnessRegistration.sandbox.warm !== undefined ? { warm: harnessRegistration.sandbox.warm } : {}),
|
|
354
|
+
};
|
|
355
|
+
const spec = {
|
|
356
|
+
harness: harnessRegistration.harness,
|
|
357
|
+
...(harnessRegistration.instructions !== undefined ? { instructions: harnessRegistration.instructions } : {}),
|
|
358
|
+
options: mergeOptions([harnessRegistration.options, providerRegistration.options, modelRegistration.options, settingsOptions, { model }]),
|
|
359
|
+
...(Object.keys(sandbox).length > 0 ? { sandbox } : {}),
|
|
360
|
+
...(providerRegistration.processEnv !== undefined ? { processEnv: providerRegistration.processEnv } : {}),
|
|
361
|
+
...(harnessRegistration.permissionMode !== undefined ? { permissionMode: harnessRegistration.permissionMode } : {}),
|
|
362
|
+
...(harnessRegistration.toolApproval !== undefined ? { toolApproval: harnessRegistration.toolApproval } : {}),
|
|
363
|
+
...(harnessRegistration.activeTools !== undefined ? { activeTools: harnessRegistration.activeTools } : {}),
|
|
364
|
+
};
|
|
365
|
+
return { kind: 'inline', spec, label: `inline ${harness}/${provider}/${model}${settings !== undefined ? ` settings=${settings}` : ''}` };
|
|
366
|
+
}
|
|
367
|
+
/** Nonsecret, host-derived selection summary for the tool description. */
|
|
368
|
+
function describeCatalog(mount) {
|
|
369
|
+
const namedNames = [...mount.named.keys()].sort();
|
|
370
|
+
const harnessLines = Object.values(mount.catalog?.harnesses ?? {}).sort((a, b) => a.key.localeCompare(b.key)).map(harness => {
|
|
371
|
+
const providers = Object.values(harness.providers).sort((a, b) => a.key.localeCompare(b.key)).map(provider => `${provider.key} (models: ${Object.keys(provider.models).sort().join(', ')}${provider.settings !== undefined ? `; settings: ${Object.keys(provider.settings).sort().join(', ')}` : ''})`);
|
|
372
|
+
return `${harness.key} [${harness.harness}]: ${providers.join(' | ')}`;
|
|
373
|
+
});
|
|
374
|
+
return [
|
|
375
|
+
`Named agents: ${namedNames.join(', ') || '(none registered)'}.`,
|
|
376
|
+
harnessLines.length > 0 ? `Inline harnesses: ${harnessLines.join(' ')}` : 'No inline harness catalog is registered on this host.',
|
|
377
|
+
].join(' ');
|
|
378
|
+
}
|
|
379
|
+
/** Registers the `agent` namespace — `run` (named or inline selection) and
|
|
380
|
+
* `steer` (routed to the task-control port) — and the recovery inspectors
|
|
381
|
+
* of `agent.run`. Called once from mountAgents after the named tools. */
|
|
382
|
+
export async function mountAgentCatalog(mount) {
|
|
383
|
+
const { host } = mount;
|
|
384
|
+
const runTool = tool({
|
|
385
|
+
description: `Run a registered agent. Named: { agent, prompt, workspaceDir? } returns the agent's declared structured result (identical to its agents.NAME tool). Inline: { harness, provider, model, settings?, prompt, workspaceDir? } composes the registered harness/provider configuration around the authorized model and returns { sessionId, text }. ${describeCatalog(mount)}`,
|
|
386
|
+
inputSchema: jsonSchema({
|
|
387
|
+
type: 'object',
|
|
388
|
+
properties: {
|
|
389
|
+
agent: { type: 'string', pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$', description: 'Named mode: a registered agent name. Exclusive with harness/provider/model/settings.' },
|
|
390
|
+
harness: { type: 'string', pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$', description: 'Inline mode: a registered harness key.' },
|
|
391
|
+
provider: { type: 'string', pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$', description: 'Inline mode: a provider key registered under the harness.' },
|
|
392
|
+
model: { type: 'string', minLength: 1, description: 'Inline mode: a model id authorized on the provider.' },
|
|
393
|
+
settings: { type: 'string', pattern: '^[a-zA-Z0-9][a-zA-Z0-9_-]*$', description: 'Inline mode: optional settings key registered on the provider.' },
|
|
394
|
+
prompt: { type: 'string', pattern: '\\S', description: 'The task for the agent.' },
|
|
395
|
+
workspaceDir: { type: 'string', minLength: 1, description: 'Omit to use the registered agent workspace. Optional subdirectory must stay inside that root; the calling harness current directory may be elsewhere.' },
|
|
396
|
+
},
|
|
397
|
+
required: ['prompt'],
|
|
398
|
+
additionalProperties: false,
|
|
399
|
+
}),
|
|
400
|
+
// Safe only because same-intent invocations are deduplicated in the
|
|
401
|
+
// shared run machinery, selection included.
|
|
402
|
+
metadata: { omakase: { replay: 'safe' } },
|
|
403
|
+
execute: async (input, execution) => {
|
|
404
|
+
if (typeof execution.toolCallId !== 'string' || !execution.toolCallId)
|
|
405
|
+
throw new AgentHostError('INVALID_INPUT', 'Agent tools require a tool call id');
|
|
406
|
+
const selection = resolveAgentRunSelection(input, mount.named, mount.catalog);
|
|
407
|
+
return await mount.runSelected(selection, input, { toolCallId: execution.toolCallId, abortSignal: execution.abortSignal });
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
await host.register('agent', { run: runTool, steer: agentSteerTool }, {
|
|
411
|
+
resolveBinding: invocation => invocation.toolName === 'agent.steer'
|
|
412
|
+
? { id: 'agent.steer', target: agentSteerTool }
|
|
413
|
+
: { id: 'agent.run', target: runTool },
|
|
414
|
+
executor: {
|
|
415
|
+
replays: false,
|
|
416
|
+
invoke: async (invocation, binding, options) => {
|
|
417
|
+
if (invocation.toolName === 'agent.steer')
|
|
418
|
+
return await invokeAgentSteer(host, invocation);
|
|
419
|
+
return await binding.target.execute(invocation.input, {
|
|
420
|
+
...options,
|
|
421
|
+
toolCallId: JSON.stringify([invocation.harnessId ?? null, invocation.sessionId ?? null, invocation.toolCallId]),
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
registerAgentRecovery(host, 'agent.run', mount.catalogFingerprint, input => mount.identityForSelection(resolveAgentRunSelection(input, mount.named, mount.catalog)));
|
|
427
|
+
}
|