@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,263 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { createChatCompletionsAdapter } from './chatCompletions.js';
|
|
3
|
+
import { ModelGatewayError, object, parse, protocol } from './errors.js';
|
|
4
|
+
import { argumentsObject, bounded, counts, id, index, jsonBody, result, sseFrames, text } from './wire.js';
|
|
5
|
+
const classify = createChatCompletionsAdapter().classify;
|
|
6
|
+
function failure(error) {
|
|
7
|
+
const e = object(error);
|
|
8
|
+
const code = classify(429, { error: { code: e.code, type: e.type === 'error' ? undefined : e.type } });
|
|
9
|
+
// A 200/in-band failure is not a proven rejection; the gateway never replays it.
|
|
10
|
+
throw new ModelGatewayError(code === 'http' ? 'incomplete' : code);
|
|
11
|
+
}
|
|
12
|
+
function itemSemantics(item) {
|
|
13
|
+
if (item.type === 'function_call') {
|
|
14
|
+
if (Object.keys(item).some(k => !['type', 'id', 'status', 'call_id', 'name', 'arguments',
|
|
15
|
+
'async', 'caller', 'namespace', 'created_by'].includes(k)))
|
|
16
|
+
return protocol();
|
|
17
|
+
if ((item.async !== undefined && item.async !== false) || item.namespace != null
|
|
18
|
+
|| (item.caller != null && object(item.caller).type !== 'direct'))
|
|
19
|
+
return protocol();
|
|
20
|
+
}
|
|
21
|
+
else if (item.type !== 'message' || item.phase != null
|
|
22
|
+
|| Object.keys(item).some(k => !['type', 'id', 'status', 'role', 'content', 'phase', 'created_by'].includes(k))) {
|
|
23
|
+
// The kernel does not store assistant phase or server execution context.
|
|
24
|
+
return protocol();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function usage(value) {
|
|
28
|
+
const base = counts(value, { input_tokens: 'inputTokens', output_tokens: 'outputTokens', total_tokens: 'totalTokens' });
|
|
29
|
+
if (value == null)
|
|
30
|
+
return base;
|
|
31
|
+
const details = counts(object(value).input_tokens_details, { cached_tokens: 'cachedInputTokens', cache_write_tokens: 'cacheWriteInputTokens' });
|
|
32
|
+
return base || details ? { ...base, ...details } : undefined;
|
|
33
|
+
}
|
|
34
|
+
function outputText(value) {
|
|
35
|
+
const part = object(value);
|
|
36
|
+
if (Object.keys(part).some(k => !['type', 'text', 'annotations', 'logprobs'].includes(k))
|
|
37
|
+
|| part.type !== 'output_text' || (part.annotations != null
|
|
38
|
+
&& (!Array.isArray(part.annotations) || part.annotations.length)))
|
|
39
|
+
return protocol();
|
|
40
|
+
return bounded(text(part.text));
|
|
41
|
+
}
|
|
42
|
+
function itemValue(raw) {
|
|
43
|
+
const item = object(raw);
|
|
44
|
+
id(item.id);
|
|
45
|
+
itemSemantics(item);
|
|
46
|
+
// Function-call status is optional; omission does not replace the response terminal.
|
|
47
|
+
if (item.status !== 'completed' && !(item.type === 'function_call' && item.status === undefined)) {
|
|
48
|
+
throw new ModelGatewayError('incomplete');
|
|
49
|
+
}
|
|
50
|
+
if (item.type === 'function_call') {
|
|
51
|
+
return { content: '', call: { id: id(item.call_id), name: id(item.name),
|
|
52
|
+
arguments: argumentsObject(text(item.arguments)) } };
|
|
53
|
+
}
|
|
54
|
+
if (item.type !== 'message' || item.role !== 'assistant' || !Array.isArray(item.content)
|
|
55
|
+
|| !item.content.length)
|
|
56
|
+
return protocol();
|
|
57
|
+
return { content: bounded(item.content.map(outputText).join('')) };
|
|
58
|
+
}
|
|
59
|
+
function finish(root) {
|
|
60
|
+
if (root.error != null)
|
|
61
|
+
failure(root.error);
|
|
62
|
+
if (root.status !== 'completed')
|
|
63
|
+
throw new ModelGatewayError('incomplete');
|
|
64
|
+
if (root.error != null || root.incomplete_details != null || !Array.isArray(root.output) || !root.output.length)
|
|
65
|
+
return protocol();
|
|
66
|
+
id(root.id);
|
|
67
|
+
const ids = new Set(), calls = [];
|
|
68
|
+
let content = '';
|
|
69
|
+
for (const raw of root.output) {
|
|
70
|
+
const item = object(raw), key = id(item.id);
|
|
71
|
+
if (ids.has(key))
|
|
72
|
+
return protocol();
|
|
73
|
+
ids.add(key);
|
|
74
|
+
const value = itemValue(item);
|
|
75
|
+
content = bounded(content + value.content);
|
|
76
|
+
if (value.call)
|
|
77
|
+
calls.push(value.call);
|
|
78
|
+
}
|
|
79
|
+
return result(content, calls);
|
|
80
|
+
}
|
|
81
|
+
export function createResponsesAdapter() {
|
|
82
|
+
return {
|
|
83
|
+
protocol: 'responses',
|
|
84
|
+
classify,
|
|
85
|
+
encode(request, model, stream, options) {
|
|
86
|
+
const input = [];
|
|
87
|
+
for (const m of request.messages) {
|
|
88
|
+
if (m.role === 'tool')
|
|
89
|
+
input.push({ type: 'function_call_output', call_id: m.toolCallId,
|
|
90
|
+
output: JSON.stringify(m.outcome) });
|
|
91
|
+
else if (m.role === 'assistant') {
|
|
92
|
+
if (m.content)
|
|
93
|
+
input.push({ role: 'assistant', content: m.content });
|
|
94
|
+
for (const c of m.toolCalls)
|
|
95
|
+
input.push({ type: 'function_call', call_id: c.id, name: c.name,
|
|
96
|
+
arguments: JSON.stringify(c.arguments) });
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
input.push({ role: m.role, content: [{ type: 'input_text', text: m.content }] });
|
|
100
|
+
}
|
|
101
|
+
return { model, stream, store: false, truncation: 'disabled', input,
|
|
102
|
+
...(options?.maxOutputTokens !== undefined ? { max_output_tokens: options.maxOutputTokens } : {}),
|
|
103
|
+
...(request.tools.length ? { tools: request.tools.map(t => ({
|
|
104
|
+
type: 'function', name: t.name, description: t.description, parameters: t.inputSchema, strict: false,
|
|
105
|
+
})) } : {}) };
|
|
106
|
+
},
|
|
107
|
+
async decode(body, streaming, emit) {
|
|
108
|
+
if (!streaming) {
|
|
109
|
+
const root = await jsonBody(body);
|
|
110
|
+
const reported = usage(root.usage);
|
|
111
|
+
if (reported)
|
|
112
|
+
emit({ type: 'usage', usage: reported });
|
|
113
|
+
return { response: finish(root), ...(reported ? { usage: reported } : {}) };
|
|
114
|
+
}
|
|
115
|
+
let responseId, sequence;
|
|
116
|
+
const slots = new Map();
|
|
117
|
+
const slotFor = (e) => {
|
|
118
|
+
const slot = slots.get(index(e.output_index));
|
|
119
|
+
if (!slot || slot.done || (e.item_id !== undefined && e.item_id !== slot.item.id))
|
|
120
|
+
return protocol();
|
|
121
|
+
return slot;
|
|
122
|
+
};
|
|
123
|
+
for await (const frame of sseFrames(body)) {
|
|
124
|
+
const e = object(parse(frame.data)), type = text(e.type);
|
|
125
|
+
if (frame.event !== undefined && frame.event !== type)
|
|
126
|
+
return protocol();
|
|
127
|
+
if (e.sequence_number !== undefined) {
|
|
128
|
+
const n = index(e.sequence_number, Number.MAX_SAFE_INTEGER);
|
|
129
|
+
if (sequence !== undefined && n !== sequence + 1)
|
|
130
|
+
return protocol();
|
|
131
|
+
sequence = n;
|
|
132
|
+
}
|
|
133
|
+
if (type === 'error' || type === 'response.failed' || type === 'response.incomplete') {
|
|
134
|
+
if (type !== 'error' && (!responseId || object(e.response).id !== responseId))
|
|
135
|
+
return protocol();
|
|
136
|
+
const reported = type === 'error' ? undefined : usage(object(e.response).usage);
|
|
137
|
+
if (reported)
|
|
138
|
+
emit({ type: 'usage', usage: reported });
|
|
139
|
+
if (type === 'error')
|
|
140
|
+
failure(e);
|
|
141
|
+
if (object(e.response).error != null)
|
|
142
|
+
failure(object(e.response).error);
|
|
143
|
+
throw new ModelGatewayError('incomplete');
|
|
144
|
+
}
|
|
145
|
+
if (type === 'response.created') {
|
|
146
|
+
if (responseId)
|
|
147
|
+
return protocol();
|
|
148
|
+
const r = object(e.response);
|
|
149
|
+
if (r.status !== 'in_progress' || !Array.isArray(r.output) || r.output.length || r.error != null)
|
|
150
|
+
return protocol();
|
|
151
|
+
responseId = id(r.id);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!responseId || (e.response_id !== undefined && e.response_id !== responseId))
|
|
155
|
+
return protocol();
|
|
156
|
+
if (type === 'response.in_progress') {
|
|
157
|
+
const r = object(e.response);
|
|
158
|
+
if (r.id !== responseId || r.status !== 'in_progress' || r.error != null)
|
|
159
|
+
return protocol();
|
|
160
|
+
}
|
|
161
|
+
else if (type === 'response.output_item.added') {
|
|
162
|
+
const i = index(e.output_index), item = object(e.item);
|
|
163
|
+
if (i !== slots.size || slots.has(i) || [...slots.values()].some(s => s.item.id === item.id))
|
|
164
|
+
return protocol();
|
|
165
|
+
id(item.id);
|
|
166
|
+
itemSemantics(item);
|
|
167
|
+
if (item.status !== 'in_progress' && !(item.type === 'function_call' && item.status === undefined))
|
|
168
|
+
return protocol();
|
|
169
|
+
if (item.type === 'function_call') {
|
|
170
|
+
id(item.call_id);
|
|
171
|
+
id(item.name);
|
|
172
|
+
bounded(text(item.arguments), 64 * 1024);
|
|
173
|
+
emit({ type: 'tool_delta', index: i, id: item.call_id, name: item.name });
|
|
174
|
+
}
|
|
175
|
+
else if (item.type !== 'message' || item.role !== 'assistant' || !Array.isArray(item.content) || item.content.length)
|
|
176
|
+
return protocol();
|
|
177
|
+
slots.set(i, { item, arguments: item.type === 'function_call' ? text(item.arguments) : '',
|
|
178
|
+
argumentsDone: false, parts: new Map() });
|
|
179
|
+
}
|
|
180
|
+
else if (type === 'response.function_call_arguments.delta' || type === 'response.function_call_arguments.done') {
|
|
181
|
+
const slot = slotFor(e);
|
|
182
|
+
if (slot.item.type !== 'function_call' || slot.argumentsDone)
|
|
183
|
+
return protocol();
|
|
184
|
+
if (type.endsWith('.delta')) {
|
|
185
|
+
const delta = text(e.delta);
|
|
186
|
+
slot.arguments = bounded(slot.arguments + delta, 64 * 1024);
|
|
187
|
+
emit({ type: 'tool_delta', index: index(e.output_index), arguments: delta });
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
if (slot.arguments !== text(e.arguments))
|
|
191
|
+
return protocol();
|
|
192
|
+
slot.argumentsDone = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (type === 'response.content_part.added') {
|
|
196
|
+
const slot = slotFor(e), i = index(e.content_index);
|
|
197
|
+
if (slot.item.type !== 'message' || i !== slot.parts.size)
|
|
198
|
+
return protocol();
|
|
199
|
+
slot.parts.set(i, { text: outputText(e.part), textDone: false, done: false });
|
|
200
|
+
}
|
|
201
|
+
else if (type === 'response.output_text.delta' || type === 'response.output_text.done' || type === 'response.content_part.done') {
|
|
202
|
+
const slot = slotFor(e), part = slot.parts.get(index(e.content_index));
|
|
203
|
+
if (slot.item.type !== 'message' || !part || part.done)
|
|
204
|
+
return protocol();
|
|
205
|
+
if (type === 'response.output_text.delta') {
|
|
206
|
+
if (part.textDone)
|
|
207
|
+
return protocol();
|
|
208
|
+
const delta = text(e.delta);
|
|
209
|
+
part.text = bounded(part.text + delta);
|
|
210
|
+
emit({ type: 'text_delta', text: delta });
|
|
211
|
+
}
|
|
212
|
+
else if (type === 'response.output_text.done') {
|
|
213
|
+
if (part.textDone || part.text !== text(e.text))
|
|
214
|
+
return protocol();
|
|
215
|
+
part.textDone = true;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
if (!part.textDone || part.text !== outputText(e.part))
|
|
219
|
+
return protocol();
|
|
220
|
+
part.done = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (type === 'response.output_item.done') {
|
|
224
|
+
const slot = slotFor(e), item = object(e.item);
|
|
225
|
+
if (slot.item.id !== item.id || slot.item.type !== item.type)
|
|
226
|
+
return protocol();
|
|
227
|
+
itemValue(item);
|
|
228
|
+
if (item.type === 'function_call') {
|
|
229
|
+
if (!slot.argumentsDone || slot.arguments !== item.arguments
|
|
230
|
+
|| slot.item.call_id !== item.call_id || slot.item.name !== item.name)
|
|
231
|
+
return protocol();
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
if (!slot.parts.size || [...slot.parts.values()].some(p => !p.done)
|
|
235
|
+
|| !Array.isArray(item.content) || item.content.length !== slot.parts.size
|
|
236
|
+
|| item.content.some((p, i) => outputText(p) !== slot.parts.get(i)?.text))
|
|
237
|
+
return protocol();
|
|
238
|
+
}
|
|
239
|
+
slot.done = item;
|
|
240
|
+
}
|
|
241
|
+
else if (type === 'response.completed') {
|
|
242
|
+
const root = object(e.response);
|
|
243
|
+
if (root.id !== responseId)
|
|
244
|
+
return protocol();
|
|
245
|
+
const reported = usage(root.usage);
|
|
246
|
+
if (reported)
|
|
247
|
+
emit({ type: 'usage', usage: reported });
|
|
248
|
+
if (root.id !== responseId || !slots.size || !Array.isArray(root.output)
|
|
249
|
+
|| root.output.length !== slots.size || root.output.some((item, i) => {
|
|
250
|
+
const done = slots.get(i)?.done;
|
|
251
|
+
return !done || !isDeepStrictEqual(itemValue(item), itemValue(done))
|
|
252
|
+
|| object(item).id !== done.id;
|
|
253
|
+
}))
|
|
254
|
+
return protocol();
|
|
255
|
+
return { response: finish(root), ...(reported ? { usage: reported } : {}) };
|
|
256
|
+
}
|
|
257
|
+
else
|
|
258
|
+
return protocol();
|
|
259
|
+
}
|
|
260
|
+
throw new ModelGatewayError('incomplete');
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { object, parse, protocol } from './errors.js';
|
|
2
|
+
export function text(value) { return typeof value === 'string' ? value : protocol(); }
|
|
3
|
+
export function id(value) { const s = text(value); return s.trim() ? s : protocol(); }
|
|
4
|
+
export function index(value, limit = 1024) {
|
|
5
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value < limit ? value : protocol();
|
|
6
|
+
}
|
|
7
|
+
export function bounded(value, limit = 512 * 1024) {
|
|
8
|
+
return Buffer.byteLength(value) <= limit ? value : protocol();
|
|
9
|
+
}
|
|
10
|
+
export function argumentsObject(value) {
|
|
11
|
+
const args = object(typeof value === 'string' ? parse(bounded(value, 64 * 1024)) : value);
|
|
12
|
+
// The gateway additionally validates depth, JSON values and total response size.
|
|
13
|
+
if (Buffer.byteLength(JSON.stringify(args)) > 64 * 1024)
|
|
14
|
+
return protocol();
|
|
15
|
+
return args;
|
|
16
|
+
}
|
|
17
|
+
export function result(content, calls) {
|
|
18
|
+
if (calls.length > 8 || new Set(calls.map(c => c.id)).size !== calls.length)
|
|
19
|
+
return protocol();
|
|
20
|
+
return calls.length ? { kind: 'tool_calls', content, calls } : { kind: 'final', content };
|
|
21
|
+
}
|
|
22
|
+
export function counts(value, mapping) {
|
|
23
|
+
if (value == null)
|
|
24
|
+
return undefined;
|
|
25
|
+
const source = object(value), usage = {};
|
|
26
|
+
for (const [wire, key] of Object.entries(mapping)) {
|
|
27
|
+
const n = source[wire];
|
|
28
|
+
if (n == null)
|
|
29
|
+
continue;
|
|
30
|
+
if (typeof n !== 'number' || !Number.isSafeInteger(n) || n < 0)
|
|
31
|
+
return protocol();
|
|
32
|
+
usage[key] = n;
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(usage).length ? usage : undefined;
|
|
35
|
+
}
|
|
36
|
+
export async function jsonBody(body) {
|
|
37
|
+
let source = '';
|
|
38
|
+
for await (const part of body)
|
|
39
|
+
source = bounded(source + part, 4 * 1024 * 1024);
|
|
40
|
+
return object(parse(source));
|
|
41
|
+
}
|
|
42
|
+
/** Framing only: native lifecycle remains the responsibility of each codec. */
|
|
43
|
+
export async function* sseFrames(body) {
|
|
44
|
+
let line = '', data = [], event, afterCR = false;
|
|
45
|
+
for await (const part of body) {
|
|
46
|
+
for (const char of part) {
|
|
47
|
+
if (afterCR) {
|
|
48
|
+
afterCR = false;
|
|
49
|
+
if (char === '\n')
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (char === '\r' || char === '\n') {
|
|
53
|
+
if (!line) {
|
|
54
|
+
if (data.length)
|
|
55
|
+
yield { ...(event ? { event } : {}), data: data.join('\n') };
|
|
56
|
+
data = [];
|
|
57
|
+
event = undefined;
|
|
58
|
+
}
|
|
59
|
+
else if (line === 'data')
|
|
60
|
+
data.push('');
|
|
61
|
+
else if (line.startsWith('data:'))
|
|
62
|
+
data.push(line.slice(5).replace(/^ /, ''));
|
|
63
|
+
else if (line.startsWith('event:'))
|
|
64
|
+
event = line.slice(6).replace(/^ /, '');
|
|
65
|
+
line = '';
|
|
66
|
+
afterCR = char === '\r';
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
line += char;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Unterminated frames are not a native completion.
|
|
73
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const integer = (value, max = Number.MAX_SAFE_INTEGER) => {
|
|
2
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > max)
|
|
3
|
+
throw new Error('Invalid observation number');
|
|
4
|
+
return value;
|
|
5
|
+
};
|
|
6
|
+
const label = (value) => {
|
|
7
|
+
// Opaque correlation labels, never user text, paths, credentials or error details.
|
|
8
|
+
if (typeof value !== 'string' || !value.length || Buffer.byteLength(value) > 256) {
|
|
9
|
+
throw new Error('Invalid observation label');
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
function choice(value, allowed) {
|
|
14
|
+
if (!allowed.includes(value))
|
|
15
|
+
throw new Error('Invalid observation tag');
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
const statuses = ['completed', 'error', 'cancelled', 'unknown'];
|
|
19
|
+
const reasons = ['completed', 'error', 'cancelled', 'budget_exhausted'];
|
|
20
|
+
const effects = ['none', 'unknown'];
|
|
21
|
+
const scopeOf = (value) => ({ sessionId: label(value.sessionId), turnId: label(value.turnId) });
|
|
22
|
+
function usageOf(usage) {
|
|
23
|
+
const result = {};
|
|
24
|
+
for (const key of ['inputTokens', 'outputTokens', 'cachedInputTokens', 'cacheWriteInputTokens', 'totalTokens']) {
|
|
25
|
+
if (usage[key] !== undefined)
|
|
26
|
+
result[key] = integer(usage[key]);
|
|
27
|
+
}
|
|
28
|
+
return Object.freeze(result);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A bounded local projection, not a durable database. The injected clock should
|
|
32
|
+
* be monotonic milliseconds; durations describe observation arrival, not provider
|
|
33
|
+
* wall time. Replay/cache never acquire synthetic durations.
|
|
34
|
+
*/
|
|
35
|
+
export function createLocalObserver(options = {}) {
|
|
36
|
+
const capacity = integer(options.capacity ?? 512, 10_000);
|
|
37
|
+
if (!capacity)
|
|
38
|
+
throw new Error('Observation capacity must be positive');
|
|
39
|
+
const clock = options.clock ?? (() => performance.now());
|
|
40
|
+
const records = [];
|
|
41
|
+
const subscribers = new Set();
|
|
42
|
+
const pending = new Set();
|
|
43
|
+
let cursor = 0, evicted = 0, rejected = 0, consumerDropped = 0, consumerFailures = 0;
|
|
44
|
+
let closed = false;
|
|
45
|
+
function duration(draft, source, now) {
|
|
46
|
+
if (source !== 'live')
|
|
47
|
+
return;
|
|
48
|
+
const startType = draft.type === 'turn_ended' ? 'turn_started'
|
|
49
|
+
: draft.type === 'step_ended' ? 'step_started'
|
|
50
|
+
: draft.kind === 'model' && draft.type === 'ended' ? 'started'
|
|
51
|
+
: draft.kind === 'event' && draft.type === 'message_appended' && draft.role === 'tool' ? 'tool_started' : undefined;
|
|
52
|
+
if (!startType)
|
|
53
|
+
return;
|
|
54
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
55
|
+
const prior = records[i];
|
|
56
|
+
if (prior.source !== 'live' || prior.kind !== draft.kind ||
|
|
57
|
+
prior.sessionId !== draft.sessionId || prior.turnId !== draft.turnId ||
|
|
58
|
+
prior.step !== draft.step || prior.requestId !== draft.requestId || prior.attempt !== draft.attempt ||
|
|
59
|
+
prior.toolCallId !== draft.toolCallId)
|
|
60
|
+
continue;
|
|
61
|
+
// Repeated terminal reports cannot create another measured interval.
|
|
62
|
+
if (prior.type === draft.type)
|
|
63
|
+
return;
|
|
64
|
+
if (prior.type === startType)
|
|
65
|
+
return now >= prior.observedAt ? now - prior.observedAt : undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function append(make, source) {
|
|
69
|
+
if (closed)
|
|
70
|
+
return false;
|
|
71
|
+
try {
|
|
72
|
+
if (!['live', 'replay', 'cached'].includes(source))
|
|
73
|
+
throw new Error('Invalid source');
|
|
74
|
+
const draft = make();
|
|
75
|
+
const now = clock();
|
|
76
|
+
if (!Number.isFinite(now))
|
|
77
|
+
throw new Error('Invalid clock');
|
|
78
|
+
const elapsed = duration(draft, source, now);
|
|
79
|
+
const record = Object.freeze({
|
|
80
|
+
...draft, source, observedAt: now, cursor: ++cursor,
|
|
81
|
+
...(elapsed === undefined ? {} : { durationMs: elapsed }),
|
|
82
|
+
});
|
|
83
|
+
if (records.length === capacity) {
|
|
84
|
+
records.shift();
|
|
85
|
+
evicted++;
|
|
86
|
+
}
|
|
87
|
+
records.push(record);
|
|
88
|
+
for (const subscriber of subscribers) {
|
|
89
|
+
if (subscriber.busy || pending.size >= 32) {
|
|
90
|
+
consumerDropped++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
subscriber.busy = true;
|
|
94
|
+
pending.add(subscriber.controller);
|
|
95
|
+
void Promise.resolve().then(() => {
|
|
96
|
+
if (!subscriber.controller.signal.aborted) {
|
|
97
|
+
return subscriber.consumer.export(record, subscriber.controller.signal);
|
|
98
|
+
}
|
|
99
|
+
}).catch(() => { consumerFailures++; }).finally(() => {
|
|
100
|
+
subscriber.busy = false;
|
|
101
|
+
pending.delete(subscriber.controller);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Never leak exception text (which may contain credentials), nor reject the
|
|
108
|
+
// kernel's required fact sink. Failure remains visible in health().
|
|
109
|
+
rejected++;
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const observer = {
|
|
114
|
+
observeEvent(event, source = 'live') {
|
|
115
|
+
return append(() => {
|
|
116
|
+
const data = event.data;
|
|
117
|
+
const record = {
|
|
118
|
+
...scopeOf(event), kind: 'event', completeness: 'event',
|
|
119
|
+
type: choice(data.type, ['turn_started', 'step_started', 'message_appended', 'tool_started', 'step_ended', 'turn_ended']),
|
|
120
|
+
eventSeq: integer(event.seq),
|
|
121
|
+
...('step' in data && data.step !== undefined ? { step: integer(data.step) } : {}),
|
|
122
|
+
};
|
|
123
|
+
if (data.type === 'turn_ended') {
|
|
124
|
+
return { ...record, status: choice(data.reason, reasons), ...(data.errorCode ? { errorCode: label(data.errorCode) } : {}) };
|
|
125
|
+
}
|
|
126
|
+
if (data.type === 'tool_started') {
|
|
127
|
+
return { ...record, toolCallId: label(data.toolCallId), name: label(data.name) };
|
|
128
|
+
}
|
|
129
|
+
if (data.type === 'message_appended') {
|
|
130
|
+
const message = data.message;
|
|
131
|
+
if (message.role === 'tool') {
|
|
132
|
+
return { ...record, role: message.role, toolCallId: label(message.toolCallId), name: label(message.name),
|
|
133
|
+
...(message.outcome.ok ? { status: 'completed' } : {
|
|
134
|
+
status: 'error', effect: choice(message.outcome.error.effect, effects), errorCode: label(message.outcome.error.code),
|
|
135
|
+
}) };
|
|
136
|
+
}
|
|
137
|
+
return { ...record, role: choice(message.role, ['system', 'user', 'assistant']) };
|
|
138
|
+
}
|
|
139
|
+
return record;
|
|
140
|
+
}, source);
|
|
141
|
+
},
|
|
142
|
+
observeSnapshot(result, source = 'live') {
|
|
143
|
+
return append(() => ({
|
|
144
|
+
...scopeOf(result), kind: 'snapshot', completeness: 'snapshot', type: 'turn_result',
|
|
145
|
+
status: choice(result.reason, reasons), messageCount: result.messages.length,
|
|
146
|
+
unknownEffects: result.messages.filter(m => m.role === 'tool' && !m.outcome.ok && m.outcome.error.effect === 'unknown').length,
|
|
147
|
+
...(result.error ? { errorCode: label(result.error.code) } : {}),
|
|
148
|
+
}), source);
|
|
149
|
+
},
|
|
150
|
+
observeModel(model, source = 'live') {
|
|
151
|
+
return append(() => ({
|
|
152
|
+
...scopeOf(model), kind: 'model', completeness: 'event', type: choice(model.phase, ['started', 'ended', 'retry']),
|
|
153
|
+
step: integer(model.step), requestId: label(model.requestId), attempt: integer(model.attempt),
|
|
154
|
+
...(model.status ? { status: choice(model.status, statuses) } : {}),
|
|
155
|
+
...(model.effect ? { effect: choice(model.effect, effects) } : {}),
|
|
156
|
+
...(model.errorCode ? { errorCode: label(model.errorCode) } : {}),
|
|
157
|
+
...(model.usage ? { usage: usageOf(model.usage) } : {}),
|
|
158
|
+
...(model.retryAfterMs === undefined ? {} : { retryAfterMs: integer(model.retryAfterMs) }),
|
|
159
|
+
...(model.clientRequestId === undefined ? {} : { clientRequestId: label(model.clientRequestId) }),
|
|
160
|
+
...(model.providerRequestId === undefined ? {} : { providerRequestId: label(model.providerRequestId) }),
|
|
161
|
+
...(model.httpStatus === undefined ? {} : { httpStatus: integer(model.httpStatus, 599) }),
|
|
162
|
+
...(model.elapsedMs === undefined ? {} : { elapsedMs: (() => {
|
|
163
|
+
if (!Number.isFinite(model.elapsedMs) || model.elapsedMs < 0)
|
|
164
|
+
throw new Error('Invalid elapsed time');
|
|
165
|
+
return model.elapsedMs;
|
|
166
|
+
})() }),
|
|
167
|
+
}), source);
|
|
168
|
+
},
|
|
169
|
+
observeStream(stream, source = 'live') {
|
|
170
|
+
return append(() => ({
|
|
171
|
+
...scopeOf(stream), kind: 'stream', completeness: 'provisional', type: 'text_delta',
|
|
172
|
+
step: integer(stream.step), textCharacters: stream.text.length,
|
|
173
|
+
...(stream.requestId ? { requestId: label(stream.requestId) } : {}),
|
|
174
|
+
}), source);
|
|
175
|
+
},
|
|
176
|
+
observe(event) { observer.observeEvent(event); },
|
|
177
|
+
query(query = {}) {
|
|
178
|
+
const after = integer(query.after ?? 0);
|
|
179
|
+
const limit = integer(query.limit ?? 100, 1000);
|
|
180
|
+
if (!limit)
|
|
181
|
+
throw new Error('Query limit must be positive');
|
|
182
|
+
const matches = records.filter(record => record.cursor > after &&
|
|
183
|
+
(query.sessionId === undefined || record.sessionId === query.sessionId) &&
|
|
184
|
+
(query.turnId === undefined || record.turnId === query.turnId) &&
|
|
185
|
+
(query.requestId === undefined || record.requestId === query.requestId) &&
|
|
186
|
+
(query.toolCallId === undefined || record.toolCallId === query.toolCallId));
|
|
187
|
+
const page = Object.freeze(matches.slice(0, limit));
|
|
188
|
+
return { records: page, nextCursor: matches.length > limit ? page[page.length - 1].cursor : Math.max(cursor, after),
|
|
189
|
+
throughCursor: cursor, evicted, gap: after < (records[0]?.cursor ?? 1) - 1, closed };
|
|
190
|
+
},
|
|
191
|
+
health() {
|
|
192
|
+
return Object.freeze({ retained: records.length, evicted, rejected, closed, subscribers: subscribers.size,
|
|
193
|
+
inFlight: pending.size, consumerDropped, consumerFailures });
|
|
194
|
+
},
|
|
195
|
+
subscribe(consumer) {
|
|
196
|
+
if (closed || subscribers.size >= 32)
|
|
197
|
+
throw new Error('Observer closed or subscriber limit reached');
|
|
198
|
+
const subscriber = { consumer, controller: new AbortController(), busy: false };
|
|
199
|
+
subscribers.add(subscriber);
|
|
200
|
+
return () => { subscribers.delete(subscriber); subscriber.controller.abort(); };
|
|
201
|
+
},
|
|
202
|
+
close() {
|
|
203
|
+
closed = true;
|
|
204
|
+
for (const subscriber of subscribers)
|
|
205
|
+
subscriber.controller.abort();
|
|
206
|
+
subscribers.clear();
|
|
207
|
+
// Unsubscribed but unsettled consumers are still owned cancellation targets.
|
|
208
|
+
for (const controller of pending)
|
|
209
|
+
controller.abort();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
return observer;
|
|
213
|
+
}
|
|
214
|
+
/** Caller owns the destination and its lifecycle; errors are isolated by subscribe. */
|
|
215
|
+
export function createJsonlExporter(write) {
|
|
216
|
+
return { async export(record, signal) {
|
|
217
|
+
if (!signal.aborted)
|
|
218
|
+
await write(`${JSON.stringify(record)}\n`, signal);
|
|
219
|
+
} };
|
|
220
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { ProductError, containsCredential, resolveCredentialReference } from './config.js';
|
|
3
|
+
import { openProductStore, storageFailure } from './storage.js';
|
|
4
|
+
export const catalogCommands = ['sessions', 'session-info', 'rename', 'history'];
|
|
5
|
+
export const catalogCommandOptions = (command) => ['--state-dir', '--credential-ref',
|
|
6
|
+
...(command === 'sessions' || command === 'history' ? ['--limit', '--cursor'] : []),
|
|
7
|
+
...(command !== 'sessions' ? ['--session'] : []),
|
|
8
|
+
...(command === 'rename' ? ['--title', '--expected-metadata-revision'] : [])];
|
|
9
|
+
/** Data-only catalog command: no provider, owner, tools, account fallback or cwd. */
|
|
10
|
+
export async function runCatalogCommand(args, env, cwd) {
|
|
11
|
+
const [command, ...tail] = args;
|
|
12
|
+
const invalid = () => {
|
|
13
|
+
throw new ProductError('agent_config', 'catalog', 'Use explicit --state-dir, exact --session ID, opaque --cursor and bounded --limit; rename requires JSON --title and --expected-metadata-revision.');
|
|
14
|
+
};
|
|
15
|
+
if (!catalogCommands.some(name => name === command))
|
|
16
|
+
return invalid();
|
|
17
|
+
const allowed = catalogCommandOptions(command);
|
|
18
|
+
const values = new Map();
|
|
19
|
+
for (let i = 0; i < tail.length; i++) {
|
|
20
|
+
const key = tail[i];
|
|
21
|
+
if (key === '--json' && !values.has(key)) {
|
|
22
|
+
values.set(key, 'true');
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const value = tail[++i];
|
|
26
|
+
if (!allowed.includes(key) || values.has(key) || value === undefined
|
|
27
|
+
|| Buffer.byteLength(value) > 4096 || /[\x00-\x1f\x7f]/u.test(value))
|
|
28
|
+
return invalid();
|
|
29
|
+
values.set(key, value);
|
|
30
|
+
}
|
|
31
|
+
const directory = values.get('--state-dir') ?? env.NATIVE_AGENT_STATE_DIR;
|
|
32
|
+
if (!directory?.trim() || Buffer.byteLength(directory) > 4096 || /[\x00-\x1f\x7f]/u.test(directory)
|
|
33
|
+
|| (!values.has('--state-dir') && !isAbsolute(directory)))
|
|
34
|
+
return invalid();
|
|
35
|
+
const id = values.get('--session');
|
|
36
|
+
if (command !== 'sessions' && (!id || !/^[A-Za-z0-9_-]{1,128}$/u.test(id)))
|
|
37
|
+
return invalid();
|
|
38
|
+
const integer = (text, maximum, minimum) => {
|
|
39
|
+
if (!text || !/^\d+$/u.test(text) || !Number.isSafeInteger(Number(text))
|
|
40
|
+
|| Number(text) < minimum || Number(text) > maximum)
|
|
41
|
+
return invalid();
|
|
42
|
+
return Number(text);
|
|
43
|
+
};
|
|
44
|
+
const limit = integer(values.get('--limit') ?? '20', 100, 1);
|
|
45
|
+
const options = { limit, ...(values.has('--cursor') ? { cursor: values.get('--cursor') } : {}) };
|
|
46
|
+
let title = null, revision = 0;
|
|
47
|
+
if (command === 'rename') {
|
|
48
|
+
try {
|
|
49
|
+
title = JSON.parse(values.get('--title') ?? '');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return invalid();
|
|
53
|
+
}
|
|
54
|
+
if (title !== null && (typeof title !== 'string' || !title.trim()
|
|
55
|
+
|| [...title.trim()].length > 200 || /[\p{Cc}\p{Cs}]/u.test(title)))
|
|
56
|
+
return invalid();
|
|
57
|
+
revision = integer(values.get('--expected-metadata-revision'), Number.MAX_SAFE_INTEGER, 0);
|
|
58
|
+
}
|
|
59
|
+
const ref = values.get('--credential-ref') ?? env.NATIVE_AGENT_CREDENTIAL_REF;
|
|
60
|
+
const credential = ref === undefined ? undefined : resolveCredentialReference(ref, env);
|
|
61
|
+
const guard = (value) => {
|
|
62
|
+
if (credential && containsCredential(value, credential))
|
|
63
|
+
throw new ProductError('agent_execution', 'credential', 'Configured credential found in catalog data; no output or automatic replay.');
|
|
64
|
+
};
|
|
65
|
+
guard({ directory, id, title, options });
|
|
66
|
+
const store = await openProductStore(resolve(cwd, directory), false);
|
|
67
|
+
try {
|
|
68
|
+
const result = command === 'sessions' ? await store.listSessions(options)
|
|
69
|
+
: command === 'session-info' ? await store.getSessionInfo(id)
|
|
70
|
+
: command === 'history' ? await store.readHistory(id, options)
|
|
71
|
+
: await store.renameSession(id, title, revision);
|
|
72
|
+
guard(result);
|
|
73
|
+
await new Promise((done, fail) => process.stdout.write(`${JSON.stringify(result)}\n`, error => error ? fail(new ProductError('agent_output', 'stdout', 'Read saved facts; do not repeat an uncertain rename.')) : done()));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw storageFailure(error);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await store.close();
|
|
81
|
+
}
|
|
82
|
+
}
|