@oh-my-ai-sdk/omakase 0.1.1 → 0.1.2
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 +90 -7
- package/dist/agent-runtime/manifest.json +1 -1
- package/dist/agent-runtime/versions.json +2 -2
- package/dist/cell-declarations.d.ts +1 -1
- package/dist/cell-declarations.js +7 -2
- package/dist/cli.js +2 -1
- package/dist/decision.d.ts +28 -0
- package/dist/decision.js +193 -0
- package/dist/durable-worker/versions.json +1 -1
- package/dist/execution-policy.d.ts +4 -0
- package/dist/execution-policy.js +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/skills-host.d.ts +3 -0
- package/dist/skills-host.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,15 +12,16 @@ configured URI sources. No model-facing registry-management tools.
|
|
|
12
12
|
## CLI setup
|
|
13
13
|
|
|
14
14
|
Requires Node.js 22.13 or newer. The host itself needs no model key; your calling
|
|
15
|
-
Claude/Codex session supplies its own model. Registered child agents
|
|
16
|
-
|
|
17
|
-
(see below). Configuration stays outside
|
|
15
|
+
Claude/Codex session supplies its own model. Registered child agents and decision
|
|
16
|
+
tools use their own configured credentials. Child agents additionally need Bun
|
|
17
|
+
and their bootstrap prerequisites (see below). Configuration stays outside
|
|
18
|
+
harness MCP registration.
|
|
18
19
|
|
|
19
|
-
For a local
|
|
20
|
+
For a local candidate, install its packed `.tgz` with `bun add /absolute/path/to/archive.tgz`
|
|
20
21
|
and configure your MCP client with the absolute path to that installation's
|
|
21
|
-
`node_modules/.bin/omakase`. The registry commands below
|
|
22
|
-
|
|
23
|
-
not publish it.
|
|
22
|
+
`node_modules/.bin/omakase`. The registry commands below install the published
|
|
23
|
+
`latest` release on the `0.1.x` line. Building or installing a local candidate
|
|
24
|
+
does not publish it.
|
|
24
25
|
|
|
25
26
|
Create `~/.config/omakase/host.config.mjs` (or
|
|
26
27
|
`$XDG_CONFIG_HOME/omakase/host.config.mjs`):
|
|
@@ -321,6 +322,88 @@ The private OMP adapter is not bundled: OMP **as a child backend** requires its
|
|
|
321
322
|
separately installed/built adapter; using an MCP-capable harness as the caller
|
|
322
323
|
does not require that child adapter.
|
|
323
324
|
|
|
325
|
+
## Decision controller in ordinary or durable cells
|
|
326
|
+
|
|
327
|
+
`createDecisionTool({ model, configurationId })` returns an ordinary AI SDK tool.
|
|
328
|
+
Register it as `tools.decision` in a custom host, or set the optional `decision`
|
|
329
|
+
configuration on `createSkillsHost` to add it alongside `tools.read`. The existing
|
|
330
|
+
MCP server still exposes one `cell` tool; no separate decision server is needed.
|
|
331
|
+
|
|
332
|
+
Example `host.config.mjs` (install the provider beside this config, not inside
|
|
333
|
+
Omakase):
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
import { createTypeSafeAi } from '@ai-sdk/typesafe-ai';
|
|
337
|
+
|
|
338
|
+
export default async ({ createSkillsHost }) => createSkillsHost({
|
|
339
|
+
collections: [],
|
|
340
|
+
cwd: '/absolute/path/to/workspace',
|
|
341
|
+
decision: {
|
|
342
|
+
model: createTypeSafeAi().decisionModel('jev-1.13.0'),
|
|
343
|
+
configurationId: 'review-controller-v1',
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
This example uses `TYPESAFE_AI_API_KEY` and the tested
|
|
349
|
+
`@ai-sdk/typesafe-ai@3.0.13` adapter with Omakase's pinned `ai@7.0.128`.
|
|
350
|
+
The experimental decision protocol can change in patch releases: adapter
|
|
351
|
+
`3.0.17` expects a newer state representation and is not compatible with this
|
|
352
|
+
core version. No provider is installed, selected, or silently substituted by
|
|
353
|
+
Omakase. Other SDK decision-model instances may be supplied, but their provider
|
|
354
|
+
compatibility must be verified; this release does not include a Clef adapter.
|
|
355
|
+
|
|
356
|
+
Native custom hosts import `createDecisionTool` from `@oh-my-ai-sdk/omakase` and
|
|
357
|
+
use `await host.register('tools', { decision: createDecisionTool(options) })`.
|
|
358
|
+
Include other `tools` leaves in that same registration: namespaces cannot be
|
|
359
|
+
registered twice. The CLI also supplies `createDecisionTool` to its host factory.
|
|
360
|
+
|
|
361
|
+
Submit this body through the existing `cell` API:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
const { answers } = await tools.decision({
|
|
365
|
+
state: { reviews: ["No blocking defects"], tests: "passed" },
|
|
366
|
+
questions: {
|
|
367
|
+
blocked: { type: "boolean", instructions: "Is there a blocking defect?" },
|
|
368
|
+
next: {
|
|
369
|
+
type: "choice",
|
|
370
|
+
instructions: "Choose the next action.",
|
|
371
|
+
criteria: { fix: "Repair a defect", finish: "Verified completion" },
|
|
372
|
+
},
|
|
373
|
+
quality: {
|
|
374
|
+
type: "score",
|
|
375
|
+
instructions: "Rate completeness.",
|
|
376
|
+
criteria: ["Incomplete", "Partly complete", "Complete"],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
const { blocked, next, quality } = answers;
|
|
381
|
+
if (!blocked || !next || !quality ||
|
|
382
|
+
blocked.type !== "boolean" || next.type !== "choice" || quality.type !== "score")
|
|
383
|
+
throw new Error("Unexpected decision answer types");
|
|
384
|
+
return { blocked: blocked.probability, next: next.choice, quality: quality.score };
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
- State and instructions accept a JSON string, object or array. Question names
|
|
388
|
+
identify answers. Criteria descriptions can additionally be `null`.
|
|
389
|
+
- `boolean` returns P(true), not a Boolean flag. `choice` returns an option name.
|
|
390
|
+
`score` returns a fractional position from zero through the last rubric index.
|
|
391
|
+
Choice/score distributions are optional; calibration is not portable across
|
|
392
|
+
providers. The result contains `{ answers, modelId }`, not SDK transport data.
|
|
393
|
+
- JavaScript owns branching. A judgment never grants tool permissions or bypasses
|
|
394
|
+
native approval. Malformed answers and provider errors fail the tool call;
|
|
395
|
+
no fallback judgment or automatic inference retry is supplied.
|
|
396
|
+
- Ordinary cells execute directly. Durable cells record each decision invocation;
|
|
397
|
+
all questions in one call share that checkpoint. Separate calls can progress
|
|
398
|
+
independently. Recorded answers are reused; an interrupted inference without a
|
|
399
|
+
committed receipt fails `OUTCOME_UNKNOWN` rather than silently re-inferencing.
|
|
400
|
+
- The configured provider/model and `configurationId` contribute to durable
|
|
401
|
+
catalog identity via `metadata.omakase.executionIdentity`. Change the revision
|
|
402
|
+
whenever endpoint, adapter or hidden settings change, and keep model instances
|
|
403
|
+
immutable. Pending runs reject configuration drift with `RECOVERY_BLOCKED`;
|
|
404
|
+
retain the admitted configuration to recover them. Do not use secrets in this
|
|
405
|
+
revision or a moving model alias when reproducibility matters.
|
|
406
|
+
|
|
324
407
|
## Skill reads through Code Mode
|
|
325
408
|
|
|
326
409
|
Call `cell({})` to inspect the current full leaf catalog. Submit the following
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-10-
|
|
2
|
+
"generatedAt": "2026-10-11T16:56:28.358Z",
|
|
3
3
|
"source": "resolved package manifests walked from esbuild metafile inputs (authoritative, not pin literals)",
|
|
4
4
|
"packages": {
|
|
5
|
-
"@oh-my-ai-sdk/omakase": "0.1.
|
|
5
|
+
"@oh-my-ai-sdk/omakase": "0.1.2",
|
|
6
6
|
"@ai-sdk/provider": "4.0.7",
|
|
7
7
|
"eventsource-parser": "3.1.1",
|
|
8
8
|
"@workflow/serde": "4.1.0",
|
|
@@ -9,7 +9,7 @@ export type CellGuestContract = Readonly<{
|
|
|
9
9
|
declarations: string;
|
|
10
10
|
/** Sorted tool names of the snapshot, frozen with the fingerprint. */
|
|
11
11
|
names: readonly string[];
|
|
12
|
-
/** Stable digest of the
|
|
12
|
+
/** Stable digest of the admitted catalog, including declared execution identities. */
|
|
13
13
|
fingerprint: string;
|
|
14
14
|
}>;
|
|
15
15
|
/** Project one frozen host snapshot into the guest TypeScript contract. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { asSchema } from 'ai';
|
|
3
3
|
import { isRecord } from './type-guard.js';
|
|
4
|
+
import { toolExecutionIdentity } from './execution-policy.js';
|
|
4
5
|
/**
|
|
5
6
|
* Ambient TypeScript contract for one exact tool catalog snapshot plus the
|
|
6
7
|
* guest bindings both cell engines really provide. The semantic preflight and
|
|
@@ -63,7 +64,7 @@ export async function projectCellGuestContract(tools) {
|
|
|
63
64
|
const leaf = tools[name];
|
|
64
65
|
const input = await disclosedSchema(leaf.inputSchema);
|
|
65
66
|
const output = leaf.outputSchema === undefined ? undefined : await disclosedSchema(leaf.outputSchema);
|
|
66
|
-
projected.push({ name, input, output, signature: `(input: ${schemaToType(input)}) => Promise<${output === undefined ? 'unknown' : schemaToType(output)}>` });
|
|
67
|
+
projected.push({ name, input, output, executionIdentity: toolExecutionIdentity(leaf), signature: `(input: ${schemaToType(input)}) => Promise<${output === undefined ? 'unknown' : schemaToType(output)}>` });
|
|
67
68
|
}
|
|
68
69
|
const canonicalNames = new Set(projected.map(entry => entry.name));
|
|
69
70
|
const fields = projected.flatMap(entry => {
|
|
@@ -96,7 +97,11 @@ export async function projectCellGuestContract(tools) {
|
|
|
96
97
|
'}>;',
|
|
97
98
|
].join('\n'));
|
|
98
99
|
const declarations = [CONSOLE_DECLARATION, PARALLEL_DECLARATIONS, ...toolEntries, ...namespaceEntries].join('\n');
|
|
99
|
-
|
|
100
|
+
// Preserve existing fingerprints when no implementation identity was declared.
|
|
101
|
+
const fingerprint = createHash('sha256').update(JSON.stringify(projected.map(entry => [
|
|
102
|
+
entry.name, entry.input, entry.output ?? null,
|
|
103
|
+
...(entry.executionIdentity === undefined ? [] : [entry.executionIdentity]),
|
|
104
|
+
]))).digest('hex');
|
|
100
105
|
return Object.freeze({ declarations, names: Object.freeze(projected.map(entry => entry.name)), fingerprint });
|
|
101
106
|
}
|
|
102
107
|
// JSON Schema to TypeScript projection adapted from packages/bun-workbench
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createToolHost } from './host.js';
|
|
|
11
11
|
import { mountMcpConfigFile } from './mcp-config.js';
|
|
12
12
|
import { createCellFrontend } from './frontend.js';
|
|
13
13
|
import { createLazyDurableCellExecutor } from './cell.js';
|
|
14
|
+
import { createDecisionTool } from './decision.js';
|
|
14
15
|
let host;
|
|
15
16
|
let server;
|
|
16
17
|
let durable;
|
|
@@ -107,7 +108,7 @@ async function main() {
|
|
|
107
108
|
throw new Error('Config must export a default async host factory');
|
|
108
109
|
const { createSkillsHost } = await import('./skills-host.js');
|
|
109
110
|
const { createAgentHost, mountAgents } = await import('./agents.js');
|
|
110
|
-
host = await config.default(Object.freeze({ ...context, createSkillsHost, createAgentHost, mountAgents }));
|
|
111
|
+
host = await config.default(Object.freeze({ ...context, createSkillsHost, createAgentHost, mountAgents, createDecisionTool }));
|
|
111
112
|
}
|
|
112
113
|
if (interrupted) {
|
|
113
114
|
stopping = undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Experimental_DecisionAnswer, type Experimental_DecisionQuestion, type Experimental_DecisionModel, type JSONValue, type Tool } from 'ai';
|
|
2
|
+
/** Model and revision are operator authority; guests only supply state and questions. */
|
|
3
|
+
export type DecisionToolOptions = {
|
|
4
|
+
/** Decision model instance bound at construction. Model id strings are rejected:
|
|
5
|
+
* they would silently resolve through the ambient default provider or gateway. */
|
|
6
|
+
readonly model: Exclude<Experimental_DecisionModel, string>;
|
|
7
|
+
/** Operator revision token distinguishing endpoint or settings revisions of the
|
|
8
|
+
* same provider and model. Never a credential; never guest-supplied. */
|
|
9
|
+
readonly configurationId: string;
|
|
10
|
+
};
|
|
11
|
+
/** Guest-visible input: one shared state plus named questions of any supported type. */
|
|
12
|
+
export type DecisionInput = {
|
|
13
|
+
state: DecisionState;
|
|
14
|
+
questions: Record<string, Experimental_DecisionQuestion>;
|
|
15
|
+
};
|
|
16
|
+
/** JSON-only result: answers keyed by question name plus the answering model id.
|
|
17
|
+
* Never the full SDK decision result, whose response carries a Date. */
|
|
18
|
+
export type DecisionOutput = {
|
|
19
|
+
answers: Record<string, Experimental_DecisionAnswer<Experimental_DecisionQuestion>>;
|
|
20
|
+
modelId: string;
|
|
21
|
+
};
|
|
22
|
+
/** SDK decision input contract: shared state must be a JSON string, object, or array. */
|
|
23
|
+
type DecisionState = string | Readonly<Record<string, JSONValue>> | readonly JSONValue[];
|
|
24
|
+
/** Decision tool over the SDK decide operation. The model instance, invocation
|
|
25
|
+
* options (maxRetries 0, caller-supplied abort signal), and identity hash are
|
|
26
|
+
* pinned at construction; guest input can never select or reconfigure them. */
|
|
27
|
+
export declare function createDecisionTool(options: DecisionToolOptions): Tool<DecisionInput, DecisionOutput>;
|
|
28
|
+
export {};
|
package/dist/decision.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { experimental_decide, jsonSchema, tool, } from 'ai';
|
|
3
|
+
import { assertJson, prepareSchema } from './schema.js';
|
|
4
|
+
import { isRecord } from './type-guard.js';
|
|
5
|
+
/** Bump whenever decision tool invocation semantics change; hashed into execution identity. */
|
|
6
|
+
const IDENTITY_VERSION = 'omakase-decision-tool/v1';
|
|
7
|
+
const instructionsSchema = {
|
|
8
|
+
description: 'What to judge: a JSON string, object, or array.',
|
|
9
|
+
anyOf: [{ type: 'string' }, { type: 'object' }, { type: 'array' }],
|
|
10
|
+
};
|
|
11
|
+
const descriptionSchema = {
|
|
12
|
+
description: 'Option or level description: null or a JSON string, object, or array.',
|
|
13
|
+
anyOf: [{ type: 'string' }, { type: 'object' }, { type: 'array' }, { type: 'null' }],
|
|
14
|
+
};
|
|
15
|
+
const questionSchema = {
|
|
16
|
+
anyOf: [
|
|
17
|
+
{
|
|
18
|
+
type: 'object',
|
|
19
|
+
description: 'boolean question: answered with P(true).',
|
|
20
|
+
properties: {
|
|
21
|
+
type: { const: 'boolean' },
|
|
22
|
+
instructions: instructionsSchema,
|
|
23
|
+
criteria: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
description: 'Optional outcome descriptions; only the keys "true" and "false" are allowed.',
|
|
26
|
+
properties: { true: descriptionSchema, false: descriptionSchema },
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ['type', 'instructions'],
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'object',
|
|
35
|
+
description: 'choice question: answered with exactly one named option.',
|
|
36
|
+
properties: {
|
|
37
|
+
type: { const: 'choice' },
|
|
38
|
+
instructions: instructionsSchema,
|
|
39
|
+
criteria: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
description: 'Nonempty map of option name to description; the answer names one option.',
|
|
42
|
+
minProperties: 1,
|
|
43
|
+
additionalProperties: descriptionSchema,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ['type', 'instructions', 'criteria'],
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'object',
|
|
51
|
+
description: 'score question: answered with a position on an ordered rubric.',
|
|
52
|
+
properties: {
|
|
53
|
+
type: { const: 'score' },
|
|
54
|
+
instructions: instructionsSchema,
|
|
55
|
+
criteria: {
|
|
56
|
+
type: 'array',
|
|
57
|
+
description: 'Ordered rubric levels indexed from zero; at least two levels.',
|
|
58
|
+
minItems: 2,
|
|
59
|
+
items: descriptionSchema,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ['type', 'instructions', 'criteria'],
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
const decisionInputContract = {
|
|
68
|
+
type: 'object',
|
|
69
|
+
description: 'Ask the operator-configured decision model questions about one shared state. Model, provider, and endpoint selection is host authority and never accepted here.',
|
|
70
|
+
properties: {
|
|
71
|
+
state: {
|
|
72
|
+
description: 'The single shared state every question is judged against: a JSON string, object, or array.',
|
|
73
|
+
anyOf: [{ type: 'string' }, { type: 'object' }, { type: 'array' }],
|
|
74
|
+
},
|
|
75
|
+
questions: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
description: 'Nonempty map of question name to question; exactly one typed answer is returned per name.',
|
|
78
|
+
minProperties: 1,
|
|
79
|
+
additionalProperties: questionSchema,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ['state', 'questions'],
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
};
|
|
85
|
+
const answerSchema = {
|
|
86
|
+
description: 'One typed answer; the type field narrows the payload.',
|
|
87
|
+
anyOf: [
|
|
88
|
+
{
|
|
89
|
+
type: 'object',
|
|
90
|
+
description: 'Boolean answer: probability is P(true).',
|
|
91
|
+
properties: {
|
|
92
|
+
type: { const: 'boolean' },
|
|
93
|
+
probability: { type: 'number', minimum: 0, maximum: 1 },
|
|
94
|
+
},
|
|
95
|
+
required: ['type', 'probability'],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: 'object',
|
|
99
|
+
description: 'Choice answer: the selected option name, plus an optional distribution over the options.',
|
|
100
|
+
properties: {
|
|
101
|
+
type: { const: 'choice' },
|
|
102
|
+
choice: { type: 'string' },
|
|
103
|
+
probabilities: { type: 'object', additionalProperties: { type: 'number', minimum: 0, maximum: 1 } },
|
|
104
|
+
},
|
|
105
|
+
required: ['type', 'choice'],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'object',
|
|
109
|
+
description: 'Score answer: the rubric position, plus an optional distribution over level indexes.',
|
|
110
|
+
properties: {
|
|
111
|
+
type: { const: 'score' },
|
|
112
|
+
score: { type: 'number', minimum: 0 },
|
|
113
|
+
probabilities: { type: 'object', additionalProperties: { type: 'number', minimum: 0, maximum: 1 } },
|
|
114
|
+
},
|
|
115
|
+
required: ['type', 'score'],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
const decisionOutputContract = {
|
|
120
|
+
type: 'object',
|
|
121
|
+
description: 'Answers keyed by question name plus the answering model id. JSON only.',
|
|
122
|
+
properties: {
|
|
123
|
+
answers: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
description: 'Exactly one answer per requested question name.',
|
|
126
|
+
minProperties: 1,
|
|
127
|
+
additionalProperties: answerSchema,
|
|
128
|
+
},
|
|
129
|
+
modelId: { type: 'string', minLength: 1, description: 'The decision model that produced the answers.' },
|
|
130
|
+
},
|
|
131
|
+
required: ['answers', 'modelId'],
|
|
132
|
+
additionalProperties: false,
|
|
133
|
+
};
|
|
134
|
+
function deepFreeze(value) {
|
|
135
|
+
for (const child of Object.values(value))
|
|
136
|
+
if (child && typeof child === 'object')
|
|
137
|
+
deepFreeze(child);
|
|
138
|
+
Object.freeze(value);
|
|
139
|
+
}
|
|
140
|
+
deepFreeze(decisionInputContract);
|
|
141
|
+
deepFreeze(decisionOutputContract);
|
|
142
|
+
const decisionInputSchema = jsonSchema(decisionInputContract);
|
|
143
|
+
const decisionOutputSchema = jsonSchema(decisionOutputContract);
|
|
144
|
+
// The host and direct native invocation share the same boundary validator.
|
|
145
|
+
// Prepare lazily once; do not duplicate the SDK's question/answer semantics.
|
|
146
|
+
let preparedInput;
|
|
147
|
+
/** Projects SDK-validated answers onto the JSON output contract: optional
|
|
148
|
+
* probabilities fields are emitted only when the provider defined them, so
|
|
149
|
+
* answers carrying explicit undefined values stay JSON-valid. Required fields
|
|
150
|
+
* pass through verbatim; the SDK has already validated their semantics. */
|
|
151
|
+
function projectDecisionAnswers(answers) {
|
|
152
|
+
return Object.fromEntries(Object.entries(answers).map(([id, answer]) => {
|
|
153
|
+
if (answer.type === 'choice')
|
|
154
|
+
return [id, { type: 'choice', choice: answer.choice, ...(answer.probabilities === undefined ? {} : { probabilities: answer.probabilities }) }];
|
|
155
|
+
if (answer.type === 'score')
|
|
156
|
+
return [id, { type: 'score', score: answer.score, ...(answer.probabilities === undefined ? {} : { probabilities: answer.probabilities }) }];
|
|
157
|
+
return [id, { type: 'boolean', probability: answer.probability }];
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/** Decision tool over the SDK decide operation. The model instance, invocation
|
|
161
|
+
* options (maxRetries 0, caller-supplied abort signal), and identity hash are
|
|
162
|
+
* pinned at construction; guest input can never select or reconfigure them. */
|
|
163
|
+
export function createDecisionTool(options) {
|
|
164
|
+
if (!isRecord(options))
|
|
165
|
+
throw new Error('Decision tool options must be an object');
|
|
166
|
+
if (typeof options.configurationId !== 'string' || options.configurationId.trim() === '')
|
|
167
|
+
throw new Error('Decision tool configurationId must be a nonblank string');
|
|
168
|
+
const model = options.model;
|
|
169
|
+
if (!model || typeof model !== 'object' || typeof model.provider !== 'string' || !model.provider.trim() || typeof model.modelId !== 'string' || !model.modelId.trim())
|
|
170
|
+
throw new Error('Decision tool requires a configured decision model instance');
|
|
171
|
+
const provider = model.provider;
|
|
172
|
+
const modelId = model.modelId;
|
|
173
|
+
const executionIdentity = createHash('sha256').update(JSON.stringify([IDENTITY_VERSION, provider, modelId, options.configurationId])).digest('hex');
|
|
174
|
+
const metadata = { omakase: { replay: 'safe', executionIdentity } };
|
|
175
|
+
Object.freeze(metadata.omakase);
|
|
176
|
+
Object.freeze(metadata);
|
|
177
|
+
return tool({
|
|
178
|
+
description: 'Answer typed judgment questions about one shared state with the operator-configured decision model. Ask several named questions in a single call: boolean questions return P(true), choice questions select exactly one named option, score questions place the state on an ordered rubric indexed from zero. The decision model, provider, and endpoint are fixed by the host operator and cannot be selected or reconfigured through input. Returns { answers, modelId } with exactly one typed answer per question name.',
|
|
179
|
+
inputSchema: decisionInputSchema,
|
|
180
|
+
outputSchema: decisionOutputSchema,
|
|
181
|
+
metadata,
|
|
182
|
+
execute: async (input, execution) => {
|
|
183
|
+
if (model.provider !== provider || model.modelId !== modelId)
|
|
184
|
+
throw new Error('Decision model provider or modelId changed after decision tool construction; recreate the tool to bind the new identity');
|
|
185
|
+
const prepared = await (preparedInput ??= prepareSchema(decisionInputSchema));
|
|
186
|
+
await prepared.validate(input);
|
|
187
|
+
const decision = await experimental_decide({ model, state: input.state, questions: input.questions, maxRetries: 0, abortSignal: execution.abortSignal });
|
|
188
|
+
const answers = projectDecisionAnswers(decision.answers);
|
|
189
|
+
assertJson(answers);
|
|
190
|
+
return { answers, modelId: decision.response.modelId };
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -5,3 +5,7 @@ export declare function toolReplayPolicy(tool: {
|
|
|
5
5
|
}): ToolReplayPolicy | undefined;
|
|
6
6
|
/** Unknown executors fail closed for an explicitly unsafe implementation. */
|
|
7
7
|
export declare function assertToolReplayPolicy(policy: ToolReplayPolicy | undefined, replays: boolean | undefined, name: string): void;
|
|
8
|
+
/** Optional trusted configuration revision, included in durable admission. */
|
|
9
|
+
export declare function toolExecutionIdentity(tool: {
|
|
10
|
+
metadata?: unknown;
|
|
11
|
+
}): string | undefined;
|
package/dist/execution-policy.js
CHANGED
|
@@ -19,3 +19,20 @@ export function assertToolReplayPolicy(policy, replays, name) {
|
|
|
19
19
|
if (policy === 'unsafe' && replays !== false)
|
|
20
20
|
throw new Error(`Replay-unsafe tool cannot use a replaying or unspecified executor: ${name}`);
|
|
21
21
|
}
|
|
22
|
+
/** Optional trusted configuration revision, included in durable admission. */
|
|
23
|
+
export function toolExecutionIdentity(tool) {
|
|
24
|
+
const metadata = tool.metadata;
|
|
25
|
+
if (!metadata || typeof metadata !== 'object')
|
|
26
|
+
return undefined;
|
|
27
|
+
const omakase = metadata.omakase;
|
|
28
|
+
if (omakase === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (!omakase || typeof omakase !== 'object' || Array.isArray(omakase))
|
|
31
|
+
throw new Error('Invalid omakase execution metadata');
|
|
32
|
+
const identity = omakase.executionIdentity;
|
|
33
|
+
if (identity === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (typeof identity !== 'string' || !identity.trim())
|
|
36
|
+
throw new Error('Tool execution identity must be a nonempty string');
|
|
37
|
+
return identity;
|
|
38
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,3 +11,4 @@ export { createToolHostFrontend, createCellFrontend, HOST_FRONTEND_PREAMBLE, CEL
|
|
|
11
11
|
export { createSkillLibrary, mountSkills, type SkillLibrary, type SkillEntry, type SkillRead } from './skills.js';
|
|
12
12
|
export { createReadTool, createMcpReadResolver, mcpResourceAddress, type ReadResult, type ReadResolver, type ReadOptions } from './read.js';
|
|
13
13
|
export { toolReplayPolicy, assertToolReplayPolicy, type ToolReplayPolicy } from './execution-policy.js';
|
|
14
|
+
export { createDecisionTool, type DecisionToolOptions, type DecisionInput, type DecisionOutput } from './decision.js';
|
package/dist/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { createToolHostFrontend, createCellFrontend, HOST_FRONTEND_PREAMBLE, CEL
|
|
|
9
9
|
export { createSkillLibrary, mountSkills } from './skills.js';
|
|
10
10
|
export { createReadTool, createMcpReadResolver, mcpResourceAddress } from './read.js';
|
|
11
11
|
export { toolReplayPolicy, assertToolReplayPolicy } from './execution-policy.js';
|
|
12
|
+
export { createDecisionTool } from './decision.js';
|
package/dist/skills-host.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SkillCollection, type SkillsPeer } from './skills-server.js';
|
|
2
|
+
import { type DecisionToolOptions } from './decision.js';
|
|
2
3
|
/** Ready-to-run local host. The skills service owns collections and discovery;
|
|
3
4
|
* the host imports its single list tool through the ordinary MCP adapter.
|
|
4
5
|
*/
|
|
@@ -6,4 +7,6 @@ export declare function createSkillsHost(options: {
|
|
|
6
7
|
collections: readonly SkillCollection[];
|
|
7
8
|
peers?: readonly SkillsPeer[];
|
|
8
9
|
cwd?: string;
|
|
10
|
+
/** Optional host-owned decision model, exposed alongside tools.read. */
|
|
11
|
+
decision?: DecisionToolOptions;
|
|
9
12
|
}): Promise<import("./host.js").ToolHost>;
|
package/dist/skills-host.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createToolHost } from './host.js';
|
|
|
3
3
|
import { createSkillsServer } from './skills-server.js';
|
|
4
4
|
import { mountMcpSdk } from './mcp-sdk-source.js';
|
|
5
5
|
import { createReadTool, createMcpReadResolver, mcpResourceAddress } from './read.js';
|
|
6
|
+
import { createDecisionTool } from './decision.js';
|
|
6
7
|
/** Ready-to-run local host. The skills service owns collections and discovery;
|
|
7
8
|
* the host imports its single list tool through the ordinary MCP adapter.
|
|
8
9
|
*/
|
|
@@ -28,7 +29,10 @@ export async function createSkillsHost(options) {
|
|
|
28
29
|
return { ...result, structuredContent: { entries: scoped }, content: [{ type: 'text', text: JSON.stringify(scoped) }] };
|
|
29
30
|
},
|
|
30
31
|
});
|
|
31
|
-
await host.register('tools', {
|
|
32
|
+
await host.register('tools', {
|
|
33
|
+
read: createReadTool({ cwd: options.cwd, resolvers: { mcp: createMcpReadResolver({ skills: source }) } }),
|
|
34
|
+
...(options.decision ? { decision: createDecisionTool(options.decision) } : {}),
|
|
35
|
+
}, {
|
|
32
36
|
dispose: async () => { if (timer)
|
|
33
37
|
clearInterval(timer); await service.close(); },
|
|
34
38
|
});
|