@zq-silk/yui 2.0.0 → 2.2.0
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 +4 -0
- package/dist/cli/commandCatalog.js +318 -289
- package/dist/cli/commandDiscovery.js +72 -0
- package/dist/cli/completion.js +22 -227
- package/dist/cli/dynamicCompletion.js +12 -6
- package/dist/cli/invocationAuthority.js +2 -2
- package/dist/cli/invocationRouter.js +18 -7
- package/dist/cli.js +23 -3030
- package/dist/commands/capabilityCommands.js +5 -5
- package/dist/commands/globalRoleCommands.js +2 -1
- package/dist/commands/taskCommands.js +90 -21
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskUpstreamCommands.js +3 -1
- package/dist/context/runContextPack.js +132 -5
- package/dist/context/sourceRunContext.js +4 -2
- package/dist/context/taskContext.js +12 -3
- package/dist/controlPlaneCli.js +3092 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
- package/dist/controller/jobControl.js +21 -0
- package/dist/executor/agentExecutor.js +1 -1
- package/dist/executor/effectiveLaunch.js +18 -5
- package/dist/executor/fileRoleLaunchPlanner.js +15 -19
- package/dist/integration/gitIntegrationService.js +50 -15
- package/dist/kernel/accessAssessment.js +1 -0
- package/dist/kernel/builtinCapabilities.js +28 -1
- package/dist/kernel/capabilityRegistry.js +64 -19
- package/dist/message/messageContinuation.js +7 -2
- package/dist/nativeAgent/agent.js +275 -0
- package/dist/nativeAgent/cliDemo.js +37 -0
- package/dist/nativeAgent/codingTools.js +11 -0
- package/dist/nativeAgent/commandTool.js +215 -0
- package/dist/nativeAgent/compactionDemo.js +158 -0
- package/dist/nativeAgent/composition.js +71 -0
- package/dist/nativeAgent/context/budget.js +44 -0
- package/dist/nativeAgent/context/index.js +339 -0
- package/dist/nativeAgent/context/providerCompressor.js +103 -0
- package/dist/nativeAgent/contracts.js +1 -0
- package/dist/nativeAgent/demo.js +40 -0
- package/dist/nativeAgent/evaluation/cases.js +38 -0
- package/dist/nativeAgent/evaluation/checks.js +91 -0
- package/dist/nativeAgent/evaluation/demo.js +19 -0
- package/dist/nativeAgent/evaluation/files.js +54 -0
- package/dist/nativeAgent/evaluation/fixture.js +36 -0
- package/dist/nativeAgent/evaluation/index.js +239 -0
- package/dist/nativeAgent/executionOwner.js +209 -0
- package/dist/nativeAgent/filePatterns.js +170 -0
- package/dist/nativeAgent/fileToolsSupport.js +202 -0
- package/dist/nativeAgent/index.js +16 -0
- package/dist/nativeAgent/interaction/cli.js +358 -0
- package/dist/nativeAgent/interaction/contracts.js +1 -0
- package/dist/nativeAgent/interaction/index.js +3 -0
- package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
- package/dist/nativeAgent/interaction/renderer.js +34 -0
- package/dist/nativeAgent/localSafety.js +258 -0
- package/dist/nativeAgent/mockProvider.js +43 -0
- package/dist/nativeAgent/model/anthropicMessages.js +204 -0
- package/dist/nativeAgent/model/chatCompletions.js +210 -0
- package/dist/nativeAgent/model/errors.js +47 -0
- package/dist/nativeAgent/model/gateway.js +423 -0
- package/dist/nativeAgent/model/index.js +7 -0
- package/dist/nativeAgent/model/observationAdapter.js +21 -0
- package/dist/nativeAgent/model/protocols.js +19 -0
- package/dist/nativeAgent/model/responses.js +263 -0
- package/dist/nativeAgent/model/types.js +1 -0
- package/dist/nativeAgent/model/wire.js +73 -0
- package/dist/nativeAgent/observability/index.js +220 -0
- package/dist/nativeAgent/product/catalog.js +82 -0
- package/dist/nativeAgent/product/config.js +295 -0
- package/dist/nativeAgent/product/facts.js +30 -0
- package/dist/nativeAgent/product/index.js +62 -0
- package/dist/nativeAgent/product/location.js +44 -0
- package/dist/nativeAgent/product/runtime.js +276 -0
- package/dist/nativeAgent/product/storage.js +49 -0
- package/dist/nativeAgent/product/tools.js +47 -0
- package/dist/nativeAgent/product/transport.js +54 -0
- package/dist/nativeAgent/projectGuidance/index.js +425 -0
- package/dist/nativeAgent/searchTools.js +305 -0
- package/dist/nativeAgent/session/backends.js +293 -0
- package/dist/nativeAgent/session/catalog.js +97 -0
- package/dist/nativeAgent/session/catalogDemo.js +87 -0
- package/dist/nativeAgent/session/contracts.js +1 -0
- package/dist/nativeAgent/session/format.js +269 -0
- package/dist/nativeAgent/session/index.js +5 -0
- package/dist/nativeAgent/session/location.js +36 -0
- package/dist/nativeAgent/session/sqliteFormat.js +134 -0
- package/dist/nativeAgent/session/store.js +248 -0
- package/dist/nativeAgent/textTools.js +293 -0
- package/dist/nativeAgent/toolManager/executor.js +290 -0
- package/dist/nativeAgent/toolManager/index.js +4 -0
- package/dist/nativeAgent/validation.js +95 -0
- package/dist/plugins/pluginService.js +98 -8
- package/dist/resources/projectResourceService.js +23 -1
- package/dist/runtime/managedIdentity.js +6 -0
- package/dist/surface/surfaceContributions.js +3 -0
- package/dist/task/taskAuthority.js +61 -10
- package/dist/web/assets/assetManifest.js +38 -22
- package/dist/web/assets/client/api.js +115 -0
- package/dist/web/assets/client/app.js +615 -862
- package/dist/web/assets/client/components.js +323 -897
- package/dist/web/assets/client/detail.js +333 -0
- package/dist/web/assets/client/dock.js +225 -0
- package/dist/web/assets/client/dom.js +55 -3
- package/dist/web/assets/client/evidence.js +172 -0
- package/dist/web/assets/client/format.js +25 -14
- package/dist/web/assets/client/forms.js +230 -0
- package/dist/web/assets/client/i18n.js +1024 -791
- package/dist/web/assets/client/overview.js +118 -0
- package/dist/web/assets/client/records.js +63 -0
- package/dist/web/assets/client/sections.js +313 -0
- package/dist/web/assets/client/sidebar.js +146 -0
- package/dist/web/assets/client/theme.js +61 -22
- package/dist/web/assets/icons.js +39 -0
- package/dist/web/assets/shell.js +125 -118
- package/dist/web/assets/styles/base.js +41 -0
- package/dist/web/assets/styles/components.js +169 -0
- package/dist/web/assets/styles/layout.js +56 -77
- package/dist/web/assets/styles/markdown.js +16 -24
- package/dist/web/assets/styles/responsive.js +41 -44
- package/dist/web/assets/styles/tokens.js +65 -89
- package/dist/web/assets/styles/views.js +292 -0
- package/dist/web/webServer.js +2 -1
- package/docs/agent-result-consumption.md +16 -0
- package/docs/agent-result-consumption.zh-CN.md +13 -0
- package/docs/examples/agent-offline.mjs +194 -0
- package/docs/native-agent.md +283 -0
- package/docs/release-workflow.md +61 -0
- package/docs/release-workflow.zh-CN.md +46 -0
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +5 -0
- package/skills/yui-runtime/SKILL.md +33 -0
- package/dist/web/assets/client/taskSummary.js +0 -350
- package/dist/web/assets/client/taskSurface.js +0 -616
- package/dist/web/assets/client/view.js +0 -608
- package/dist/web/assets/styles/cards.js +0 -247
- package/dist/web/assets/styles/widgets.js +0 -168
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { lstatSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { createCodingTools } from './codingTools.js';
|
|
5
|
+
const object = (value) => value !== null && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype;
|
|
6
|
+
function fields(value, allowed) {
|
|
7
|
+
return object(value) && Object.keys(value).every(key => allowed.includes(key));
|
|
8
|
+
}
|
|
9
|
+
function freeze(value) {
|
|
10
|
+
if (value && typeof value === 'object') {
|
|
11
|
+
Object.values(value).forEach(freeze);
|
|
12
|
+
Object.freeze(value);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
const invalid = (code, message) => ({ code, message, effect: 'none' });
|
|
17
|
+
function absolute(value) {
|
|
18
|
+
return typeof value === 'string' && path.isAbsolute(value) && !value.includes('\0')
|
|
19
|
+
&& path.normalize(value) === value;
|
|
20
|
+
}
|
|
21
|
+
/** Reject symlinks in every component, not just the leaf. Controlled filesystems only. */
|
|
22
|
+
function directory(value) {
|
|
23
|
+
try {
|
|
24
|
+
if (!absolute(value))
|
|
25
|
+
throw new Error();
|
|
26
|
+
let current = path.parse(value).root;
|
|
27
|
+
for (const component of value.slice(current.length).split(path.sep).filter(Boolean)) {
|
|
28
|
+
current = path.join(current, component);
|
|
29
|
+
const stat = lstatSync(current);
|
|
30
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
31
|
+
throw new Error();
|
|
32
|
+
}
|
|
33
|
+
if (!statSync(value).isDirectory() || realpathSync(value) !== value)
|
|
34
|
+
throw new Error();
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error('An accessible absolute directory without symlink components is required');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const inside = (root, cwd) => {
|
|
42
|
+
const relative = path.relative(root, cwd);
|
|
43
|
+
return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
44
|
+
};
|
|
45
|
+
function directoryIdentity(value) {
|
|
46
|
+
try {
|
|
47
|
+
directory(value);
|
|
48
|
+
const stat = statSync(value);
|
|
49
|
+
return `${stat.dev}:${stat.ino}`;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error('Bound directory is unavailable; settle and rebuild the binding');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function executableIdentity(value) {
|
|
56
|
+
try {
|
|
57
|
+
if (!absolute(value))
|
|
58
|
+
throw new Error();
|
|
59
|
+
directory(path.dirname(value));
|
|
60
|
+
const stat = lstatSync(value);
|
|
61
|
+
if (!stat.isFile() || !(stat.mode & 0o111) || realpathSync(value) !== value)
|
|
62
|
+
throw new Error();
|
|
63
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.mode}`;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error('Reviewed executable must be an accessible absolute regular executable without symlinks');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function environment(value) {
|
|
70
|
+
// Deliberately small, complete environment. No HOME, config, proxies, tokens,
|
|
71
|
+
// interpreter/loader injection or process.env merge. Widen only with a reviewed contract.
|
|
72
|
+
if (!object(value) || Object.entries(value).some(([key, entry]) => {
|
|
73
|
+
if (typeof entry !== 'string' || entry.includes('\0') || entry.length > 4096)
|
|
74
|
+
return true;
|
|
75
|
+
if (key === 'PATH')
|
|
76
|
+
return entry.split(path.delimiter).some(part => !absolute(part));
|
|
77
|
+
if (key === 'LANG' || key === 'LC_ALL')
|
|
78
|
+
return !/^[A-Za-z0-9_.@-]{1,128}$/.test(entry);
|
|
79
|
+
if (key === 'TZ')
|
|
80
|
+
return !/^[A-Za-z0-9_+./-]{1,128}$/.test(entry) || entry.includes('..');
|
|
81
|
+
return true;
|
|
82
|
+
}))
|
|
83
|
+
throw new Error('Command environment permits only deliberate PATH, LANG, LC_ALL and TZ values');
|
|
84
|
+
return { ...value };
|
|
85
|
+
}
|
|
86
|
+
/** One common factory owns the actual prebound tools and their matching lease.
|
|
87
|
+
* Use with createToolExecutor, never the Agent's three-argument tools shorthand.
|
|
88
|
+
* No resources are started during construction/acquisition; commandTool owns its processes.
|
|
89
|
+
*/
|
|
90
|
+
export function createLocalToolBinding(options) {
|
|
91
|
+
if (!fields(options, ['root', 'cwd', 'allowWrite', 'allowCommand', 'text', 'search', 'command', 'redact'])
|
|
92
|
+
|| [options.allowWrite, options.allowCommand].some(v => v !== undefined && typeof v !== 'boolean')
|
|
93
|
+
|| (options.text !== undefined && !fields(options.text, ['maxBytes']))
|
|
94
|
+
|| (options.search !== undefined && !fields(options.search, ['maxEntries', 'maxResults', 'maxOutputBytes', 'maxFileBytes', 'maxTotalBytes', 'maxDepth']))) {
|
|
95
|
+
throw new Error('Invalid local tool configuration');
|
|
96
|
+
}
|
|
97
|
+
const root = directory(options.root);
|
|
98
|
+
const cwd = directory(options.cwd);
|
|
99
|
+
if (!inside(root, cwd))
|
|
100
|
+
throw new Error('cwd must be within the explicit root directory');
|
|
101
|
+
const rootIdentity = directoryIdentity(root);
|
|
102
|
+
const cwdIdentity = directoryIdentity(cwd);
|
|
103
|
+
const allowWrite = options.allowWrite === true;
|
|
104
|
+
const allowCommand = options.allowCommand === true;
|
|
105
|
+
if (options.redact !== undefined && (!Array.isArray(options.redact) || options.redact.length > 128
|
|
106
|
+
|| options.redact.some(v => typeof v !== 'string' || !v.length || v.length > 4096))) {
|
|
107
|
+
throw new Error('Invalid known-secret redaction configuration');
|
|
108
|
+
}
|
|
109
|
+
const secrets = [...(options.redact ?? [])].sort((a, b) => b.length - a.length);
|
|
110
|
+
const secretPattern = secrets.length
|
|
111
|
+
? new RegExp(secrets.map(secret => secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g') : undefined;
|
|
112
|
+
const scrub = (text) => secretPattern ? text.replace(secretPattern, '[REDACTED]') : text;
|
|
113
|
+
const scrubJson = (text) => {
|
|
114
|
+
const visit = (value) => typeof value === 'string' ? scrub(value)
|
|
115
|
+
: Array.isArray(value) ? value.map(visit)
|
|
116
|
+
: value && typeof value === 'object'
|
|
117
|
+
? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, k === 'sha256' ? v : visit(v)])) : value;
|
|
118
|
+
try {
|
|
119
|
+
return JSON.stringify(visit(JSON.parse(text)));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return scrub(text);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const specs = [];
|
|
126
|
+
let commandOptions;
|
|
127
|
+
if (options.command !== undefined) {
|
|
128
|
+
if (!fields(options.command, ['env', 'specs', 'timeoutMs', 'maxOutputBytes', 'killGraceMs'])
|
|
129
|
+
|| !Array.isArray(options.command.specs) || options.command.specs.length > 128) {
|
|
130
|
+
throw new Error('Invalid reviewed command configuration');
|
|
131
|
+
}
|
|
132
|
+
const env = environment(options.command.env);
|
|
133
|
+
for (const spec of options.command.specs) {
|
|
134
|
+
if (!fields(spec, ['executable', 'argv', 'effect']) || !absolute(spec.executable)
|
|
135
|
+
|| !Array.isArray(spec.argv) || spec.argv.length > 256
|
|
136
|
+
|| spec.argv.some((v) => typeof v !== 'string' || v.includes('\0'))
|
|
137
|
+
|| !['read', 'write'].includes(spec.effect)
|
|
138
|
+
|| Buffer.byteLength(JSON.stringify(spec)) > 16 * 1024) {
|
|
139
|
+
throw new Error('Invalid exact reviewed command specification');
|
|
140
|
+
}
|
|
141
|
+
if (specs.some(entry => entry.spec.executable === spec.executable
|
|
142
|
+
&& JSON.stringify(entry.spec.argv) === JSON.stringify(spec.argv))) {
|
|
143
|
+
throw new Error('Duplicate reviewed command specification');
|
|
144
|
+
}
|
|
145
|
+
specs.push({ spec: freeze({ ...spec, argv: [...spec.argv] }), identity: executableIdentity(spec.executable) });
|
|
146
|
+
}
|
|
147
|
+
commandOptions = { env, timeoutMs: options.command.timeoutMs,
|
|
148
|
+
maxOutputBytes: options.command.maxOutputBytes, killGraceMs: options.command.killGraceMs };
|
|
149
|
+
}
|
|
150
|
+
if (allowCommand && !commandOptions)
|
|
151
|
+
throw new Error('Explicit command environment and specifications are required');
|
|
152
|
+
// Validate command budgets even when disabled; do not silently ignore invalid configuration.
|
|
153
|
+
const all = createCodingTools({
|
|
154
|
+
root, text: options.text, search: options.search, command: commandOptions,
|
|
155
|
+
});
|
|
156
|
+
const description = freeze({
|
|
157
|
+
root, cwd, allowWrite, allowCommand,
|
|
158
|
+
envKeys: Object.keys(commandOptions?.env ?? {}).sort(), commandCount: specs.length,
|
|
159
|
+
});
|
|
160
|
+
// Object member order is not part of the public ToolInvocation contract.
|
|
161
|
+
// Project its required identity fields; compare JSON values without sorting
|
|
162
|
+
// arrays or weakening parameter/definition equality.
|
|
163
|
+
const invocationValue = ({ identity, call, definition }) => [
|
|
164
|
+
identity.sessionId, identity.turnId, identity.step, identity.toolCallId, identity.name,
|
|
165
|
+
call.id, call.name, call.arguments, definition,
|
|
166
|
+
];
|
|
167
|
+
const active = new WeakMap();
|
|
168
|
+
const matchingLease = (lease, invocation) => isDeepStrictEqual(active.get(lease), invocationValue(invocation));
|
|
169
|
+
const selected = all.filter(tool => (allowWrite || !['write', 'edit'].includes(tool.definition.name))
|
|
170
|
+
&& (allowCommand || tool.definition.name !== 'command'));
|
|
171
|
+
const definitions = new Map(selected.map(tool => [tool.definition.name,
|
|
172
|
+
freeze(structuredClone(tool.definition))]));
|
|
173
|
+
const fresh = () => directoryIdentity(root) === rootIdentity && directoryIdentity(cwd) === cwdIdentity;
|
|
174
|
+
const validate = (tool, args) => {
|
|
175
|
+
const bad = tool.validate(args);
|
|
176
|
+
if (bad)
|
|
177
|
+
return bad;
|
|
178
|
+
if (tool.definition.name !== 'command')
|
|
179
|
+
return null;
|
|
180
|
+
const input = args;
|
|
181
|
+
const match = specs.find(({ spec }) => spec.executable === input.command && input.cwd === cwd
|
|
182
|
+
&& JSON.stringify(spec.argv) === JSON.stringify(input.argv));
|
|
183
|
+
if (!match || (match.spec.effect === 'write' && !allowWrite)) {
|
|
184
|
+
return invalid('command_not_authorized', 'Command requires an exact reviewed executable/argv/cwd and permitted effect');
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
if (executableIdentity(match.spec.executable) !== match.identity)
|
|
188
|
+
throw new Error();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return invalid('binding_changed', 'Reviewed executable changed; settle and rebuild the binding');
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
};
|
|
195
|
+
const validInvocation = (invocation) => {
|
|
196
|
+
const name = invocation.call.name;
|
|
197
|
+
return invocation.identity.name === name && invocation.identity.toolCallId === invocation.call.id
|
|
198
|
+
&& definitions.has(name) && isDeepStrictEqual(invocation.definition, definitions.get(name));
|
|
199
|
+
};
|
|
200
|
+
const tools = selected.map(tool => {
|
|
201
|
+
const definition = definitions.get(tool.definition.name);
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
definition,
|
|
204
|
+
validate: (args) => validate(tool, args),
|
|
205
|
+
async execute(args, identity, signal, lease) {
|
|
206
|
+
// Guard here too: a permissive foreign permission, copied value, expired
|
|
207
|
+
// lease or the three-argument shorthand cannot bypass the actual tool.
|
|
208
|
+
const invocation = { identity: { ...identity, name: definition.name },
|
|
209
|
+
call: { id: identity.toolCallId, name: definition.name, arguments: args }, definition };
|
|
210
|
+
if (!lease || !matchingLease(lease, invocation)) {
|
|
211
|
+
return { ok: false, error: invalid('binding_mismatch', 'A matching live invocation lease is required') };
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
if (!fresh())
|
|
215
|
+
throw new Error();
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return { ok: false, error: invalid('binding_changed', 'Workspace changed; settle and rebuild the binding') };
|
|
219
|
+
}
|
|
220
|
+
const bad = validate(tool, args);
|
|
221
|
+
if (bad)
|
|
222
|
+
return { ok: false, error: bad };
|
|
223
|
+
const outcome = await tool.execute(args, identity, signal);
|
|
224
|
+
return outcome.ok ? { ok: true, content: scrubJson(outcome.content) }
|
|
225
|
+
: { ok: false, error: { ...outcome.error, message: scrubJson(outcome.error.message) } };
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
return Object.freeze({
|
|
230
|
+
binding: description, tools: Object.freeze(tools),
|
|
231
|
+
environment: Object.freeze({
|
|
232
|
+
async acquire(invocation, signal) {
|
|
233
|
+
if (signal.aborted || !validInvocation(invocation) || !fresh()) {
|
|
234
|
+
throw new Error('Local binding unavailable or declaration mismatched');
|
|
235
|
+
}
|
|
236
|
+
const lease = Object.freeze({ ...description });
|
|
237
|
+
active.set(lease, freeze(structuredClone(invocationValue(invocation))));
|
|
238
|
+
return { value: lease, async release() { active.delete(lease); } };
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
permission: Object.freeze({
|
|
242
|
+
async check(invocation, lease, signal) {
|
|
243
|
+
try {
|
|
244
|
+
if (signal.aborted || !validInvocation(invocation) || !matchingLease(lease, invocation)
|
|
245
|
+
|| !fresh())
|
|
246
|
+
throw new Error();
|
|
247
|
+
const tool = selected.find(tool => tool.definition.name === invocation.call.name);
|
|
248
|
+
if (validate(tool, invocation.call.arguments))
|
|
249
|
+
throw new Error();
|
|
250
|
+
return { allowed: true };
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return { allowed: false, reason: 'Local binding or reviewed capability does not match this invocation' };
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** A deliberately small read/write mock, not a semantic planner. */
|
|
2
|
+
export function createMockProvider(options) {
|
|
3
|
+
const { toolCallProbability: p, random = Math.random, readPath = 'input.txt', writePath = 'output.txt' } = options;
|
|
4
|
+
if (!Number.isFinite(p) || p < 0 || p > 1)
|
|
5
|
+
throw new Error('toolCallProbability must be in [0, 1]');
|
|
6
|
+
const latestRead = (request) => {
|
|
7
|
+
for (const m of [...request.messages].reverse()) {
|
|
8
|
+
if (m.role === 'tool' && m.name === 'read' && m.outcome.ok) {
|
|
9
|
+
try {
|
|
10
|
+
const data = JSON.parse(m.outcome.content);
|
|
11
|
+
if (data && typeof data === 'object' && 'text' in data && typeof data.text === 'string')
|
|
12
|
+
return data.text;
|
|
13
|
+
}
|
|
14
|
+
catch { /* Other read implementations need not use the text-tools payload. */ }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
async complete(request, signal) {
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
const u = random();
|
|
23
|
+
if (!Number.isFinite(u) || u < 0 || u >= 1)
|
|
24
|
+
throw new Error('random must return a number in [0, 1)');
|
|
25
|
+
if (u >= p) {
|
|
26
|
+
const outcomes = request.messages.filter(m => m.role === 'tool');
|
|
27
|
+
const success = outcomes.filter(m => m.role === 'tool' && m.outcome.ok).length;
|
|
28
|
+
return { kind: 'final', content: `Observed ${success} successful and ${outcomes.length - success} failed tool results.` };
|
|
29
|
+
}
|
|
30
|
+
const content = latestRead(request);
|
|
31
|
+
const write = request.step % 2 === 0 && content !== undefined;
|
|
32
|
+
const used = new Set(request.messages.flatMap(m => m.role === 'assistant' ? m.toolCalls.map(c => c.id) : []));
|
|
33
|
+
let suffix = 1;
|
|
34
|
+
while (used.has(`mock-${suffix}`))
|
|
35
|
+
suffix++;
|
|
36
|
+
const call = { id: `mock-${suffix}`, name: write ? 'write' : 'read',
|
|
37
|
+
arguments: write ? { path: writePath, content } : { path: readPath } };
|
|
38
|
+
if (!request.tools.some(t => t.name === call.name))
|
|
39
|
+
throw new Error(`Mock requires the ${call.name} tool`);
|
|
40
|
+
return { kind: 'tool_calls', content: '', calls: [call] };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { ModelGatewayError, object, parse, protocol } from './errors.js';
|
|
2
|
+
import { argumentsObject, bounded, counts, id, index, jsonBody, result, sseFrames, text } from './wire.js';
|
|
3
|
+
const usageKeys = { input_tokens: 'inputTokens', output_tokens: 'outputTokens',
|
|
4
|
+
cache_read_input_tokens: 'cachedInputTokens', cache_creation_input_tokens: 'cacheWriteInputTokens' };
|
|
5
|
+
function classify(status, body) {
|
|
6
|
+
if (status === 401 || status === 403)
|
|
7
|
+
return 'authentication';
|
|
8
|
+
const root = body && typeof body === 'object' ? body : {};
|
|
9
|
+
const e = root.error && typeof root.error === 'object' ? root.error : {};
|
|
10
|
+
if (e.type === 'authentication_error' || e.type === 'permission_error')
|
|
11
|
+
return 'authentication';
|
|
12
|
+
// No documented Messages quota discriminator: do not guess from message text.
|
|
13
|
+
return status === 429 && e.type === 'rate_limit_error' ? 'rate_limit' : 'http';
|
|
14
|
+
}
|
|
15
|
+
function failure(root) {
|
|
16
|
+
const code = classify(429, root);
|
|
17
|
+
throw new ModelGatewayError(code === 'http' ? 'incomplete' : code);
|
|
18
|
+
}
|
|
19
|
+
function blockValue(value) {
|
|
20
|
+
const b = object(value);
|
|
21
|
+
if (b.type === 'text') {
|
|
22
|
+
if (Object.keys(b).some(k => !['type', 'text', 'citations'].includes(k))
|
|
23
|
+
|| (b.citations != null && (!Array.isArray(b.citations) || b.citations.length)))
|
|
24
|
+
return protocol();
|
|
25
|
+
return { content: bounded(text(b.text)) };
|
|
26
|
+
}
|
|
27
|
+
if (b.type !== 'tool_use' || Object.keys(b).some(k => !['type', 'id', 'name', 'input', 'caller', 'toolset_name'].includes(k))
|
|
28
|
+
|| (b.caller != null && object(b.caller).type !== 'direct') || b.toolset_name != null)
|
|
29
|
+
return protocol();
|
|
30
|
+
return { content: '', call: { id: id(b.id), name: id(b.name), arguments: argumentsObject(object(b.input)) } };
|
|
31
|
+
}
|
|
32
|
+
function finish(root) {
|
|
33
|
+
if (root.type === 'error' || root.error != null)
|
|
34
|
+
failure(root);
|
|
35
|
+
if (root.stop_reason !== 'end_turn' && root.stop_reason !== 'tool_use')
|
|
36
|
+
throw new ModelGatewayError('incomplete');
|
|
37
|
+
if (root.type !== 'message' || root.role !== 'assistant' || !Array.isArray(root.content)
|
|
38
|
+
|| !root.content.length || root.stop_sequence != null || root.container != null || root.stop_details != null)
|
|
39
|
+
return protocol();
|
|
40
|
+
id(root.id);
|
|
41
|
+
const calls = [];
|
|
42
|
+
let content = '';
|
|
43
|
+
for (const raw of root.content) {
|
|
44
|
+
const value = blockValue(raw);
|
|
45
|
+
content = bounded(content + value.content);
|
|
46
|
+
if (value.call)
|
|
47
|
+
calls.push(value.call);
|
|
48
|
+
}
|
|
49
|
+
if ((root.stop_reason === 'tool_use') !== (calls.length > 0))
|
|
50
|
+
return protocol();
|
|
51
|
+
return result(content, calls);
|
|
52
|
+
}
|
|
53
|
+
export function createAnthropicMessagesAdapter() {
|
|
54
|
+
return {
|
|
55
|
+
protocol: 'anthropic-messages',
|
|
56
|
+
encode(request, model, stream, options) {
|
|
57
|
+
if (options?.maxOutputTokens === undefined)
|
|
58
|
+
throw new ModelGatewayError('request', 'none');
|
|
59
|
+
const system = [], messages = [];
|
|
60
|
+
let leading = true, lastWasTool = false;
|
|
61
|
+
for (const m of request.messages) {
|
|
62
|
+
if (m.role === 'system') {
|
|
63
|
+
if (!leading)
|
|
64
|
+
throw new ModelGatewayError('request', 'none');
|
|
65
|
+
system.push({ type: 'text', text: m.content });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
leading = false;
|
|
69
|
+
if (m.role === 'tool') {
|
|
70
|
+
const block = { type: 'tool_result', tool_use_id: m.toolCallId,
|
|
71
|
+
content: JSON.stringify(m.outcome), is_error: !m.outcome.ok };
|
|
72
|
+
if (lastWasTool)
|
|
73
|
+
messages[messages.length - 1].content.push(block);
|
|
74
|
+
else
|
|
75
|
+
messages.push({ role: 'user', content: [block] });
|
|
76
|
+
lastWasTool = true;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
lastWasTool = false;
|
|
80
|
+
const content = m.content ? [{ type: 'text', text: m.content }] : [];
|
|
81
|
+
if (m.role === 'assistant')
|
|
82
|
+
for (const c of m.toolCalls) {
|
|
83
|
+
content.push({ type: 'tool_use', id: c.id, name: c.name, input: c.arguments });
|
|
84
|
+
}
|
|
85
|
+
if (!content.length)
|
|
86
|
+
throw new ModelGatewayError('request', 'none');
|
|
87
|
+
messages.push({ role: m.role, content });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!messages.length || messages[0].role !== 'user' || messages[messages.length - 1].role === 'assistant') {
|
|
91
|
+
// Assistant prefill is a different semantic contract, not ordinary caller history.
|
|
92
|
+
throw new ModelGatewayError('request', 'none');
|
|
93
|
+
}
|
|
94
|
+
return { model, stream, max_tokens: options.maxOutputTokens, messages,
|
|
95
|
+
...(system.length ? { system } : {}),
|
|
96
|
+
...(request.tools.length ? { tools: request.tools.map(t => ({
|
|
97
|
+
name: t.name, description: t.description, input_schema: t.inputSchema,
|
|
98
|
+
})) } : {}) };
|
|
99
|
+
},
|
|
100
|
+
classify,
|
|
101
|
+
async decode(body, streaming, emit) {
|
|
102
|
+
if (!streaming) {
|
|
103
|
+
const root = await jsonBody(body), usage = counts(root.usage, usageKeys);
|
|
104
|
+
if (usage)
|
|
105
|
+
emit({ type: 'usage', usage });
|
|
106
|
+
return { response: finish(root), ...(usage ? { usage } : {}) };
|
|
107
|
+
}
|
|
108
|
+
let root, usage;
|
|
109
|
+
let deltaPhase = false;
|
|
110
|
+
const blocks = new Map();
|
|
111
|
+
const updateUsage = (value) => {
|
|
112
|
+
const next = counts(value, usageKeys);
|
|
113
|
+
if (!next)
|
|
114
|
+
return;
|
|
115
|
+
for (const key of Object.keys(next)) {
|
|
116
|
+
if (usage?.[key] !== undefined && next[key] < usage[key])
|
|
117
|
+
return protocol();
|
|
118
|
+
}
|
|
119
|
+
usage = { ...usage, ...next };
|
|
120
|
+
emit({ type: 'usage', usage });
|
|
121
|
+
};
|
|
122
|
+
for await (const frame of sseFrames(body)) {
|
|
123
|
+
const e = object(parse(frame.data)), type = text(e.type);
|
|
124
|
+
if (frame.event !== type)
|
|
125
|
+
return protocol();
|
|
126
|
+
if (type === 'ping')
|
|
127
|
+
continue;
|
|
128
|
+
if (type === 'error')
|
|
129
|
+
failure(e);
|
|
130
|
+
if (type === 'message_start') {
|
|
131
|
+
if (root)
|
|
132
|
+
return protocol();
|
|
133
|
+
root = object(e.message);
|
|
134
|
+
if (root.type !== 'message' || root.role !== 'assistant' || root.stop_reason != null
|
|
135
|
+
|| root.stop_sequence != null || root.container != null || root.stop_details != null
|
|
136
|
+
|| !Array.isArray(root.content) || root.content.length)
|
|
137
|
+
return protocol();
|
|
138
|
+
id(root.id);
|
|
139
|
+
updateUsage(root.usage);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!root)
|
|
143
|
+
return protocol();
|
|
144
|
+
if (type === 'content_block_start') {
|
|
145
|
+
const i = index(e.index), value = object(e.content_block);
|
|
146
|
+
if (deltaPhase || i !== blocks.size)
|
|
147
|
+
return protocol();
|
|
148
|
+
blockValue(value);
|
|
149
|
+
if (value.type === 'tool_use')
|
|
150
|
+
emit({ type: 'tool_delta', index: i, id: id(value.id), name: id(value.name) });
|
|
151
|
+
blocks.set(i, { value, arguments: '', done: false });
|
|
152
|
+
}
|
|
153
|
+
else if (type === 'content_block_delta' || type === 'content_block_stop') {
|
|
154
|
+
const i = index(e.index), block = blocks.get(i);
|
|
155
|
+
if (deltaPhase || !block || block.done)
|
|
156
|
+
return protocol();
|
|
157
|
+
if (type === 'content_block_stop') {
|
|
158
|
+
if (block.value.type === 'tool_use' && block.arguments) {
|
|
159
|
+
block.value = { ...block.value, input: argumentsObject(block.arguments) };
|
|
160
|
+
}
|
|
161
|
+
blockValue(block.value);
|
|
162
|
+
block.done = true;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const d = object(e.delta);
|
|
166
|
+
if (d.type === 'text_delta' && block.value.type === 'text') {
|
|
167
|
+
const piece = text(d.text);
|
|
168
|
+
block.value = { ...block.value, text: bounded(text(block.value.text) + piece) };
|
|
169
|
+
emit({ type: 'text_delta', text: piece });
|
|
170
|
+
}
|
|
171
|
+
else if (d.type === 'input_json_delta' && block.value.type === 'tool_use') {
|
|
172
|
+
if (Object.keys(object(block.value.input)).length)
|
|
173
|
+
return protocol();
|
|
174
|
+
const piece = text(d.partial_json);
|
|
175
|
+
block.arguments = bounded(block.arguments + piece, 64 * 1024);
|
|
176
|
+
emit({ type: 'tool_delta', index: i, arguments: piece });
|
|
177
|
+
}
|
|
178
|
+
else
|
|
179
|
+
return protocol();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (type === 'message_delta') {
|
|
183
|
+
if (!blocks.size || [...blocks.values()].some(b => !b.done) || root.stop_reason != null)
|
|
184
|
+
return protocol();
|
|
185
|
+
deltaPhase = true;
|
|
186
|
+
const d = object(e.delta);
|
|
187
|
+
if (d.container != null || d.stop_details != null || d.stop_sequence != null)
|
|
188
|
+
return protocol();
|
|
189
|
+
root = { ...root, stop_reason: d.stop_reason, stop_sequence: d.stop_sequence };
|
|
190
|
+
updateUsage(e.usage);
|
|
191
|
+
}
|
|
192
|
+
else if (type === 'message_stop') {
|
|
193
|
+
if (!deltaPhase || !blocks.size || [...blocks.values()].some(b => !b.done))
|
|
194
|
+
return protocol();
|
|
195
|
+
return { response: finish({ ...root, content: [...blocks.values()].map(b => b.value) }),
|
|
196
|
+
...(usage ? { usage } : {}) };
|
|
197
|
+
}
|
|
198
|
+
else
|
|
199
|
+
return protocol();
|
|
200
|
+
}
|
|
201
|
+
throw new ModelGatewayError('incomplete');
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|