blun-king-cli 9.1.527 → 9.1.550
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/LIESMICH.txt +13 -834
- package/README.md +41 -799
- package/bin/agent-resume-snapshot.cjs +31 -0
- package/bin/assistant-message-offload-policy.cjs +23 -2
- package/bin/codebase-search-runtime.cjs +23 -0
- package/bin/context-performance-policy.cjs +2 -5
- package/bin/context-pressure-policy.cjs +20 -0
- package/bin/cron-run-output.cjs +45 -0
- package/bin/cron-run-store.cjs +145 -0
- package/bin/durable-task-resume-policy.cjs +130 -0
- package/bin/durable-task-resume-runtime.cjs +117 -0
- package/bin/durable-task-resume-store.cjs +88 -0
- package/bin/editable-tool-approval-policy.cjs +540 -0
- package/bin/editable-tool-approval-runtime.cjs +99 -0
- package/bin/empty-response-retry-policy.cjs +29 -0
- package/bin/history-offload-pressure-policy.cjs +33 -0
- package/bin/html-to-research-markdown.cjs +146 -0
- package/bin/programmatic-context-isolation.cjs +25 -0
- package/bin/programmatic-tool-runtime.mjs +627 -0
- package/bin/read-continuation-policy.cjs +36 -5
- package/bin/scoped-cron-run-policy.cjs +358 -0
- package/bin/skill-activation-performance-policy.cjs +9 -0
- package/bin/startup-preferences.cjs +3 -3
- package/bin/structured-agent-swarm-output.cjs +325 -0
- package/bin/structured-subagent-output.cjs +252 -0
- package/bin/subagent-context-fork-policy.cjs +155 -0
- package/bin/subagent-skill-policy.cjs +204 -0
- package/bin/telegram-approval-relay.cjs +2 -1
- package/bin/telegram-direct-focus-policy.cjs +25 -1
- package/bin/todo-list-turn-policy.cjs +111 -1
- package/bin/tool-result-offload-policy.cjs +43 -1
- package/bin/turn-thinking-policy.cjs +6 -15
- package/bin/turn-tool-performance-policy.cjs +5 -4
- package/bin/update-notice.js +14 -18
- package/bin/user-message-offload-policy.cjs +12 -1
- package/blun.mjs +1406 -265
- package/codebase-index/README.md +82 -0
- package/codebase-index/codebase_index.py +469 -0
- package/package.json +24 -38
- package/telegram-plugin/bin/telegram-typing-keepalive.cjs +89 -0
- package/telegram-plugin/dist/bridge.mjs +8 -1
- package/CHANGELOG.md +0 -260
- package/agent-spine-plugin/CHANGELOG.md +0 -406
- package/agent-spine-plugin/CONTRIBUTING.md +0 -52
- package/agent-spine-plugin/README.md +0 -344
- package/agent-spine-plugin/SECURITY.md +0 -47
- package/agent-spine-plugin/docs/acceptance.md +0 -61
- package/agent-spine-plugin/docs/architecture.md +0 -183
- package/agent-spine-plugin/docs/attention.md +0 -121
- package/agent-spine-plugin/docs/automatic-continuity.md +0 -79
- package/agent-spine-plugin/docs/channel-runtime.md +0 -92
- package/agent-spine-plugin/docs/coordination.md +0 -138
- package/agent-spine-plugin/docs/feed-transport.md +0 -99
- package/agent-spine-plugin/docs/gateway-runtime.md +0 -116
- package/agent-spine-plugin/docs/harness-reference.md +0 -45
- package/agent-spine-plugin/docs/host-integration.md +0 -129
- package/agent-spine-plugin/docs/https-transport.md +0 -116
- package/agent-spine-plugin/docs/learning.md +0 -133
- package/agent-spine-plugin/docs/object-transport.md +0 -93
- package/agent-spine-plugin/docs/peer-transport.md +0 -88
- package/agent-spine-plugin/docs/preflight-recall.md +0 -69
- package/agent-spine-plugin/docs/preservation-contract.md +0 -53
- package/agent-spine-plugin/docs/quality-gates.md +0 -50
- package/agent-spine-plugin/docs/relationships.md +0 -73
- package/agent-spine-plugin/docs/releasing.md +0 -83
- package/agent-spine-plugin/docs/roadmap.md +0 -307
- package/agent-spine-plugin/docs/selfstarter.md +0 -88
- package/agent-spine-plugin/docs/session-briefing.md +0 -74
- package/agent-spine-plugin/docs/shared-memory.md +0 -259
- package/agent-spine-plugin/docs/source-roots.md +0 -86
- package/agent-spine-plugin/docs/sqlite-transport.md +0 -76
- package/agent-spine-plugin/scripts/check-hosts.js +0 -195
- package/agent-spine-plugin/scripts/check-install.js +0 -569
- package/agent-spine-plugin/scripts/check-syntax.js +0 -29
- package/agent-spine-plugin/scripts/github-actions.js +0 -11
- package/agent-spine-plugin/scripts/release-check.js +0 -128
- package/agent-spine-plugin/scripts/run-acceptance.js +0 -19
- package/agent-spine-plugin/scripts/run-checks.js +0 -46
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +0 -73
- package/agent-spine-plugin/spine-example/1-identity.md +0 -12
- package/agent-spine-plugin/spine-example/2-voice.md +0 -6
- package/agent-spine-plugin/spine-example/3-conduct.md +0 -8
- package/agent-spine-plugin/spine-example/4-history.md +0 -4
- package/bin/package-regression-policy.cjs +0 -77
- package/release-planned-removals.json +0 -15
- package/scripts/check-active-profile-plugin-startup.js +0 -36
- package/scripts/check-approval-observability-regression.js +0 -111
- package/scripts/check-approval-queue-shortcuts-regression.js +0 -65
- package/scripts/check-bundled-agent-spine-regression.js +0 -48
- package/scripts/check-copy-command-regression.js +0 -74
- package/scripts/check-historical-tool-result-preview-regression.js +0 -77
- package/scripts/check-mcp-startup-wait-budget.js +0 -48
- package/scripts/check-package-regression.js +0 -38
- package/scripts/check-plugin-startup-regression.js +0 -53
- package/scripts/check-queue-controls-regression.js +0 -189
- package/scripts/check-release-metadata.js +0 -103
- package/scripts/check-reload-agent-spine-regression.js +0 -76
- package/scripts/check-resume-replay-regression.js +0 -100
- package/scripts/check-session-cancel-regression.js +0 -43
- package/scripts/check-session-picker-resume-metrics-regression.js +0 -97
- package/scripts/check-session-start-hook-context-regression.js +0 -228
- package/scripts/check-shell-terminal-isolation-regression.js +0 -81
- package/scripts/check-slash-escape-regression.js +0 -89
- package/scripts/check-telegram-bridge-watchdog.js +0 -60
- package/scripts/check-telegram-loop-exactly-once-regression.js +0 -71
- package/scripts/check-todo-loop-regression.js +0 -78
- package/scripts/check-todo-recovery-catalog-regression.js +0 -50
- /package/{scripts → bin}/fix-node-pty-perms.js +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MAX_STRUCTURED_SWARM_OUTPUT_BYTES = 256 * 1024;
|
|
4
|
+
const MAX_STRUCTURED_SWARM_FALLBACK_BYTES = 64 * 1024;
|
|
5
|
+
const MAX_STRUCTURED_SWARM_RESULTS = 128;
|
|
6
|
+
const MAX_PROFILE_BYTES = 64;
|
|
7
|
+
const MAX_ERROR_CHARS = 512;
|
|
8
|
+
const RESUME_HINT = 'Resume this agent through AgentSwarm.resume_agent_ids.';
|
|
9
|
+
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
12
|
+
const prototype = Object.getPrototypeOf(value);
|
|
13
|
+
return prototype === Object.prototype || prototype === null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function byteLength(value) {
|
|
17
|
+
return Buffer.byteLength(value, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validBoundedString(value, maxBytes) {
|
|
21
|
+
return typeof value === 'string' && value.length > 0 && byteLength(value) <= maxBytes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function safeUsage(value) {
|
|
25
|
+
if (!isPlainObject(value)) return null;
|
|
26
|
+
const keys = ['input', 'output', 'cache_read', 'cache_write'];
|
|
27
|
+
if (Object.keys(value).length !== keys.length || !keys.every((key) => Object.hasOwn(value, key))) return null;
|
|
28
|
+
if (!keys.every((key) => Number.isSafeInteger(value[key]) && value[key] >= 0)) return null;
|
|
29
|
+
return {
|
|
30
|
+
input: value.input,
|
|
31
|
+
output: value.output,
|
|
32
|
+
cache_read: value.cache_read,
|
|
33
|
+
cache_write: value.cache_write,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function boundedError(value) {
|
|
38
|
+
const text = String(value ?? 'unknown error').replace(/[\r\n\t]+/gu, ' ').trim();
|
|
39
|
+
return text.length > MAX_ERROR_CHARS ? `${text.slice(0, MAX_ERROR_CHARS - 3)}...` : text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseStructuredChildEnvelope(raw, expected) {
|
|
43
|
+
if (typeof raw !== 'string') return { ok: false, error: 'child envelope is not a JSON string' };
|
|
44
|
+
let value;
|
|
45
|
+
try {
|
|
46
|
+
value = JSON.parse(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
return { ok: false, error: 'child envelope is not exactly one JSON object' };
|
|
49
|
+
}
|
|
50
|
+
if (!isPlainObject(value)) return { ok: false, error: 'child envelope is not an object' };
|
|
51
|
+
const keys = ['agent_id', 'actual_subagent_type', 'status', 'response_format', 'result', 'usage'];
|
|
52
|
+
if (Object.keys(value).length !== keys.length || !keys.every((key) => Object.hasOwn(value, key))) {
|
|
53
|
+
return { ok: false, error: 'child envelope shape mismatch' };
|
|
54
|
+
}
|
|
55
|
+
if (value.agent_id !== expected.agentId) return { ok: false, error: 'agent_id mismatch' };
|
|
56
|
+
if (value.actual_subagent_type !== expected.profileName) return { ok: false, error: 'actual_subagent_type mismatch' };
|
|
57
|
+
if (!validBoundedString(value.actual_subagent_type, MAX_PROFILE_BYTES)) {
|
|
58
|
+
return { ok: false, error: 'actual_subagent_type exceeds 64 UTF-8 bytes' };
|
|
59
|
+
}
|
|
60
|
+
if (value.status !== 'completed') return { ok: false, error: 'child status mismatch' };
|
|
61
|
+
if (value.response_format !== expected.responseFormat.name) return { ok: false, error: 'response_format mismatch' };
|
|
62
|
+
const usage = safeUsage(value.usage);
|
|
63
|
+
if (usage === null) return { ok: false, error: 'child usage shape mismatch' };
|
|
64
|
+
try {
|
|
65
|
+
if (expected.responseFormat.validator(value.result) !== true) {
|
|
66
|
+
return { ok: false, error: 'child result failed response_format validation' };
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
return { ok: false, error: 'child result validator failed closed' };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
value: {
|
|
74
|
+
agentId: value.agent_id,
|
|
75
|
+
profileName: value.actual_subagent_type,
|
|
76
|
+
result: value.result,
|
|
77
|
+
usage,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function baseEntry(input, outcome, profileName) {
|
|
83
|
+
const entry = {
|
|
84
|
+
index: input.spec.index,
|
|
85
|
+
mode: input.spec.kind,
|
|
86
|
+
};
|
|
87
|
+
if (input.spec.item !== undefined) entry.item = input.spec.item;
|
|
88
|
+
if (input.agentId !== undefined) entry.agent_id = input.agentId;
|
|
89
|
+
if (profileName !== undefined) entry.actual_subagent_type = profileName;
|
|
90
|
+
entry.outcome = outcome;
|
|
91
|
+
return entry;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function failureEntry(input, error, profileName) {
|
|
95
|
+
const entry = baseEntry(input, 'failed', profileName);
|
|
96
|
+
entry.error = `Structured swarm item failed: ${boundedError(error)}`;
|
|
97
|
+
if (input.agentId !== undefined) entry.resume_hint = RESUME_HINT;
|
|
98
|
+
return entry;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeResult(input, responseFormat) {
|
|
102
|
+
const profileName = validBoundedString(input.profileName, MAX_PROFILE_BYTES) ? input.profileName : undefined;
|
|
103
|
+
if (input.status !== 'completed') {
|
|
104
|
+
const outcome = input.status === 'aborted' ? 'aborted' : 'failed';
|
|
105
|
+
const entry = baseEntry(input, outcome, profileName);
|
|
106
|
+
entry.error = boundedError(input.error);
|
|
107
|
+
if (input.agentId !== undefined) entry.resume_hint = RESUME_HINT;
|
|
108
|
+
return entry;
|
|
109
|
+
}
|
|
110
|
+
if (profileName === undefined) return failureEntry(input, 'authoritative profile is unavailable or invalid');
|
|
111
|
+
const parsed = parseStructuredChildEnvelope(input.result, {
|
|
112
|
+
agentId: input.agentId,
|
|
113
|
+
profileName,
|
|
114
|
+
responseFormat,
|
|
115
|
+
});
|
|
116
|
+
if (!parsed.ok) return failureEntry(input, parsed.error, profileName);
|
|
117
|
+
const entry = baseEntry(input, 'completed', parsed.value.profileName);
|
|
118
|
+
entry.result = parsed.value.result;
|
|
119
|
+
entry.usage = parsed.value.usage;
|
|
120
|
+
return entry;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function summaryFor(results) {
|
|
124
|
+
return {
|
|
125
|
+
completed: results.filter((entry) => entry.outcome === 'completed').length,
|
|
126
|
+
failed: results.filter((entry) => entry.outcome === 'failed').length,
|
|
127
|
+
aborted: results.filter((entry) => entry.outcome === 'aborted').length,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function aggregateStatus(summary) {
|
|
132
|
+
if (summary.completed === summary.completed + summary.failed + summary.aborted) return 'completed';
|
|
133
|
+
if (summary.completed === 0) return 'failed';
|
|
134
|
+
return 'partial';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function metadataOnlyEntry(entry) {
|
|
138
|
+
const result = {
|
|
139
|
+
index: entry.index,
|
|
140
|
+
mode: entry.mode,
|
|
141
|
+
};
|
|
142
|
+
if (entry.agent_id !== undefined) result.agent_id = entry.agent_id;
|
|
143
|
+
if (entry.actual_subagent_type !== undefined) result.actual_subagent_type = entry.actual_subagent_type;
|
|
144
|
+
result.outcome = entry.outcome;
|
|
145
|
+
result.result_omitted = true;
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function minimalOverflowEnvelope(responseFormat, summary) {
|
|
150
|
+
return {
|
|
151
|
+
status: 'failed',
|
|
152
|
+
error_code: 'structured_swarm_output_limit_exceeded',
|
|
153
|
+
response_format: responseFormat.name,
|
|
154
|
+
summary,
|
|
155
|
+
results_omitted: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function validateInputs(inputs) {
|
|
160
|
+
if (inputs.length === 0) throw new Error('structured swarm requires at least one result');
|
|
161
|
+
if (inputs.length > MAX_STRUCTURED_SWARM_RESULTS) {
|
|
162
|
+
throw new Error(`structured swarm supports at most ${String(MAX_STRUCTURED_SWARM_RESULTS)} results`);
|
|
163
|
+
}
|
|
164
|
+
const indexes = inputs.map((input) => input?.spec?.index);
|
|
165
|
+
const expected = Array.from({ length: inputs.length }, (_, index) => index + 1);
|
|
166
|
+
const ordered = [...indexes].sort((left, right) => left - right);
|
|
167
|
+
if (!ordered.every((value, index) => Number.isInteger(value) && value === expected[index])) {
|
|
168
|
+
throw new Error('structured swarm indexes must be unique contiguous integers starting at 1');
|
|
169
|
+
}
|
|
170
|
+
if (!inputs.every((input) => input.spec.kind === 'spawn' || input.spec.kind === 'resume')) {
|
|
171
|
+
throw new Error('structured swarm mode must be spawn or resume');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderStructuredSwarmResultsWithLimits(inputs, responseFormat, limits) {
|
|
176
|
+
if (!Array.isArray(inputs)) throw new TypeError('structured swarm results must be an array');
|
|
177
|
+
if (!responseFormat || typeof responseFormat.name !== 'string' || typeof responseFormat.validator !== 'function') {
|
|
178
|
+
throw new TypeError('prepared response format is required');
|
|
179
|
+
}
|
|
180
|
+
validateInputs(inputs);
|
|
181
|
+
const maxOutputBytes = limits?.maxOutputBytes ?? MAX_STRUCTURED_SWARM_OUTPUT_BYTES;
|
|
182
|
+
const maxFallbackBytes = limits?.maxFallbackBytes ?? MAX_STRUCTURED_SWARM_FALLBACK_BYTES;
|
|
183
|
+
const results = inputs.map((input) => normalizeResult(input, responseFormat)).sort((left, right) => left.index - right.index);
|
|
184
|
+
const summary = summaryFor(results);
|
|
185
|
+
const full = JSON.stringify({
|
|
186
|
+
status: aggregateStatus(summary),
|
|
187
|
+
response_format: responseFormat.name,
|
|
188
|
+
summary,
|
|
189
|
+
results,
|
|
190
|
+
});
|
|
191
|
+
if (byteLength(full) <= maxOutputBytes) return full;
|
|
192
|
+
|
|
193
|
+
const fallback = JSON.stringify({
|
|
194
|
+
status: 'failed',
|
|
195
|
+
error_code: 'structured_swarm_output_limit_exceeded',
|
|
196
|
+
response_format: responseFormat.name,
|
|
197
|
+
summary,
|
|
198
|
+
results: results.map(metadataOnlyEntry),
|
|
199
|
+
});
|
|
200
|
+
if (byteLength(fallback) <= maxFallbackBytes) return fallback;
|
|
201
|
+
return JSON.stringify(minimalOverflowEnvelope(responseFormat, summary));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderStructuredSwarmResults(inputs, responseFormat) {
|
|
205
|
+
return renderStructuredSwarmResultsWithLimits(inputs, responseFormat, undefined);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function safeSummary(value) {
|
|
209
|
+
if (!isPlainObject(value)) return null;
|
|
210
|
+
const keys = ['completed', 'failed', 'aborted'];
|
|
211
|
+
if (Object.keys(value).length !== keys.length || !keys.every((key) => Object.hasOwn(value, key))) return null;
|
|
212
|
+
if (!keys.every((key) => Number.isSafeInteger(value[key]) && value[key] >= 0)) return null;
|
|
213
|
+
return {
|
|
214
|
+
completed: value.completed,
|
|
215
|
+
failed: value.failed,
|
|
216
|
+
aborted: value.aborted,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parseStructuredDisplayEntry(entry) {
|
|
221
|
+
if (!isPlainObject(entry)) return null;
|
|
222
|
+
if (!Number.isSafeInteger(entry.index) || entry.index < 1) return null;
|
|
223
|
+
if (entry.mode !== 'spawn' && entry.mode !== 'resume') return null;
|
|
224
|
+
if (entry.outcome !== 'completed' && entry.outcome !== 'failed' && entry.outcome !== 'aborted') return null;
|
|
225
|
+
|
|
226
|
+
const resultOmitted = entry.result_omitted === true;
|
|
227
|
+
if (Object.hasOwn(entry, 'result_omitted') && !resultOmitted) return null;
|
|
228
|
+
if (resultOmitted && (Object.hasOwn(entry, 'result') || Object.hasOwn(entry, 'error'))) return null;
|
|
229
|
+
if (!resultOmitted && entry.outcome === 'completed' && !Object.hasOwn(entry, 'result')) return null;
|
|
230
|
+
if (!resultOmitted && entry.outcome !== 'completed' && typeof entry.error !== 'string') return null;
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
index: entry.index,
|
|
234
|
+
status: entry.outcome,
|
|
235
|
+
completedText: entry.outcome === 'completed' && !resultOmitted
|
|
236
|
+
? JSON.stringify(entry.result)
|
|
237
|
+
: undefined,
|
|
238
|
+
failureText: entry.outcome !== 'completed' && !resultOmitted
|
|
239
|
+
? boundedError(entry.error)
|
|
240
|
+
: undefined,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseStructuredSwarmDisplay(output) {
|
|
245
|
+
if (typeof output !== 'string') return { recognized: false };
|
|
246
|
+
let value;
|
|
247
|
+
try {
|
|
248
|
+
value = JSON.parse(output);
|
|
249
|
+
} catch {
|
|
250
|
+
return { recognized: false };
|
|
251
|
+
}
|
|
252
|
+
if (!isPlainObject(value)) return { recognized: false };
|
|
253
|
+
if (!validBoundedString(value.response_format, MAX_STRUCTURED_SWARM_FALLBACK_BYTES)) {
|
|
254
|
+
return { recognized: false };
|
|
255
|
+
}
|
|
256
|
+
if (value.status !== 'completed' && value.status !== 'partial' && value.status !== 'failed') {
|
|
257
|
+
return { recognized: false };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const summary = safeSummary(value.summary);
|
|
261
|
+
if (summary === null) return { recognized: false };
|
|
262
|
+
const summaryTotal = summary.completed + summary.failed + summary.aborted;
|
|
263
|
+
if (summaryTotal < 1 || summaryTotal > MAX_STRUCTURED_SWARM_RESULTS) return { recognized: false };
|
|
264
|
+
const overflow = value.error_code === 'structured_swarm_output_limit_exceeded';
|
|
265
|
+
if (Object.hasOwn(value, 'error_code') && !overflow) return { recognized: false };
|
|
266
|
+
if (overflow && value.status !== 'failed') return { recognized: false };
|
|
267
|
+
|
|
268
|
+
if (value.results_omitted === true) {
|
|
269
|
+
const allowedKeys = ['status', 'error_code', 'response_format', 'summary', 'results_omitted'];
|
|
270
|
+
if (!overflow || Object.hasOwn(value, 'results') || Object.keys(value).some((key) => !allowedKeys.includes(key))) {
|
|
271
|
+
return { recognized: false };
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
recognized: true,
|
|
275
|
+
aggregateFailed: true,
|
|
276
|
+
summary,
|
|
277
|
+
statuses: [],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (Object.hasOwn(value, 'results_omitted') || !Array.isArray(value.results)) return { recognized: false };
|
|
282
|
+
const allowedKeys = overflow
|
|
283
|
+
? ['status', 'error_code', 'response_format', 'summary', 'results']
|
|
284
|
+
: ['status', 'response_format', 'summary', 'results'];
|
|
285
|
+
if (Object.keys(value).some((key) => !allowedKeys.includes(key))) return { recognized: false };
|
|
286
|
+
if (value.results.length === 0 || value.results.length > MAX_STRUCTURED_SWARM_RESULTS) return { recognized: false };
|
|
287
|
+
|
|
288
|
+
const statuses = value.results.map(parseStructuredDisplayEntry);
|
|
289
|
+
if (statuses.some((entry) => entry === null)) return { recognized: false };
|
|
290
|
+
statuses.sort((left, right) => left.index - right.index);
|
|
291
|
+
if (!statuses.every((entry, index) => entry.index === index + 1)) return { recognized: false };
|
|
292
|
+
|
|
293
|
+
const actualSummary = {
|
|
294
|
+
completed: statuses.filter((entry) => entry.status === 'completed').length,
|
|
295
|
+
failed: statuses.filter((entry) => entry.status === 'failed').length,
|
|
296
|
+
aborted: statuses.filter((entry) => entry.status === 'aborted').length,
|
|
297
|
+
};
|
|
298
|
+
if (!Object.keys(actualSummary).every((key) => actualSummary[key] === summary[key])) return { recognized: false };
|
|
299
|
+
if (!overflow && aggregateStatus(summary) !== value.status) return { recognized: false };
|
|
300
|
+
if (!overflow && value.results.some((entry) => entry.result_omitted === true)) return { recognized: false };
|
|
301
|
+
if (overflow && !value.results.every((entry) => entry.result_omitted === true)) return { recognized: false };
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
recognized: true,
|
|
305
|
+
aggregateFailed: overflow,
|
|
306
|
+
summary,
|
|
307
|
+
statuses,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function structuredSwarmToolResult(output) {
|
|
312
|
+
const parsed = parseStructuredSwarmDisplay(output);
|
|
313
|
+
return parsed.recognized && parsed.aggregateFailed ? { output, isError: true } : { output };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = {
|
|
317
|
+
MAX_STRUCTURED_SWARM_OUTPUT_BYTES,
|
|
318
|
+
MAX_STRUCTURED_SWARM_FALLBACK_BYTES,
|
|
319
|
+
MAX_STRUCTURED_SWARM_RESULTS,
|
|
320
|
+
parseStructuredChildEnvelope,
|
|
321
|
+
parseStructuredSwarmDisplay,
|
|
322
|
+
renderStructuredSwarmResults,
|
|
323
|
+
renderStructuredSwarmResultsWithLimits,
|
|
324
|
+
structuredSwarmToolResult,
|
|
325
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MAX_SCHEMA_BYTES = 16 * 1024;
|
|
4
|
+
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
5
|
+
const MAX_SCHEMA_DEPTH = 12;
|
|
6
|
+
const MAX_SCHEMA_NODES = 128;
|
|
7
|
+
const MAX_ERROR_BYTES = 2048;
|
|
8
|
+
const STRUCTURED_SUBAGENT_OUTPUT_ERROR = 'Structured subagent output invalid after one repair attempt';
|
|
9
|
+
|
|
10
|
+
const ALLOWED_SCHEMA_KEYS = new Set([
|
|
11
|
+
'type',
|
|
12
|
+
'properties',
|
|
13
|
+
'required',
|
|
14
|
+
'additionalProperties',
|
|
15
|
+
'items',
|
|
16
|
+
'enum',
|
|
17
|
+
'const',
|
|
18
|
+
'minimum',
|
|
19
|
+
'maximum',
|
|
20
|
+
'exclusiveMinimum',
|
|
21
|
+
'exclusiveMaximum',
|
|
22
|
+
'minLength',
|
|
23
|
+
'maxLength',
|
|
24
|
+
'minItems',
|
|
25
|
+
'maxItems',
|
|
26
|
+
'description',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const ALLOWED_TYPES = new Set([
|
|
30
|
+
'object',
|
|
31
|
+
'array',
|
|
32
|
+
'string',
|
|
33
|
+
'number',
|
|
34
|
+
'integer',
|
|
35
|
+
'boolean',
|
|
36
|
+
'null',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
function isPlainObject(value) {
|
|
40
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
41
|
+
const prototype = Object.getPrototypeOf(value);
|
|
42
|
+
return prototype === Object.prototype || prototype === null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function boundedText(value, maxBytes = MAX_ERROR_BYTES) {
|
|
46
|
+
const text = String(value);
|
|
47
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
48
|
+
if (bytes.byteLength <= maxBytes) return text;
|
|
49
|
+
return `${bytes.subarray(0, Math.max(0, maxBytes - 3)).toString('utf8')}...`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertFiniteNumber(value, path) {
|
|
53
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
54
|
+
throw new Error(`${path} must be a finite number`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertNonNegativeInteger(value, path) {
|
|
59
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
60
|
+
throw new Error(`${path} must be a non-negative integer`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function inspectSchemaNode(node, path, depth, state) {
|
|
65
|
+
if (!isPlainObject(node)) throw new Error(`${path} must be a JSON Schema object`);
|
|
66
|
+
if (depth > MAX_SCHEMA_DEPTH) {
|
|
67
|
+
throw new Error(`response_format.schema exceeds maximum depth ${MAX_SCHEMA_DEPTH}`);
|
|
68
|
+
}
|
|
69
|
+
state.nodes += 1;
|
|
70
|
+
if (state.nodes > MAX_SCHEMA_NODES) {
|
|
71
|
+
throw new Error(`response_format.schema exceeds maximum node count ${MAX_SCHEMA_NODES}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const key of Object.keys(node)) {
|
|
75
|
+
if (!ALLOWED_SCHEMA_KEYS.has(key)) {
|
|
76
|
+
throw new Error(`${path}.${key} is not supported in response_format.schema`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (node.type !== undefined && (!ALLOWED_TYPES.has(node.type) || typeof node.type !== 'string')) {
|
|
81
|
+
throw new Error(`${path}.type must be one supported scalar JSON Schema type`);
|
|
82
|
+
}
|
|
83
|
+
if (node.description !== undefined && typeof node.description !== 'string') {
|
|
84
|
+
throw new Error(`${path}.description must be a string`);
|
|
85
|
+
}
|
|
86
|
+
if (node.properties !== undefined) {
|
|
87
|
+
if (!isPlainObject(node.properties)) throw new Error(`${path}.properties must be an object`);
|
|
88
|
+
for (const [name, child] of Object.entries(node.properties)) {
|
|
89
|
+
inspectSchemaNode(child, `${path}.properties[${JSON.stringify(name)}]`, depth + 1, state);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (node.required !== undefined) {
|
|
93
|
+
if (!Array.isArray(node.required) || node.required.some((item) => typeof item !== 'string')) {
|
|
94
|
+
throw new Error(`${path}.required must be an array of property names`);
|
|
95
|
+
}
|
|
96
|
+
if (new Set(node.required).size !== node.required.length) {
|
|
97
|
+
throw new Error(`${path}.required must not contain duplicate property names`);
|
|
98
|
+
}
|
|
99
|
+
if (node.required.some((name) => !isPlainObject(node.properties) || !(name in node.properties))) {
|
|
100
|
+
throw new Error(`${path}.required may reference only declared properties`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (node.additionalProperties !== undefined && typeof node.additionalProperties !== 'boolean') {
|
|
104
|
+
throw new Error(`${path}.additionalProperties must be a boolean`);
|
|
105
|
+
}
|
|
106
|
+
if (node.items !== undefined) {
|
|
107
|
+
inspectSchemaNode(node.items, `${path}.items`, depth + 1, state);
|
|
108
|
+
}
|
|
109
|
+
if (node.enum !== undefined && (!Array.isArray(node.enum) || node.enum.length === 0)) {
|
|
110
|
+
throw new Error(`${path}.enum must be a non-empty array`);
|
|
111
|
+
}
|
|
112
|
+
for (const key of ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum']) {
|
|
113
|
+
if (node[key] !== undefined) assertFiniteNumber(node[key], `${path}.${key}`);
|
|
114
|
+
}
|
|
115
|
+
for (const key of ['minLength', 'maxLength', 'minItems', 'maxItems']) {
|
|
116
|
+
if (node[key] !== undefined) assertNonNegativeInteger(node[key], `${path}.${key}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function prepareStructuredResponseFormat(responseFormat, compileValidator) {
|
|
121
|
+
if (responseFormat === undefined) return undefined;
|
|
122
|
+
if (!isPlainObject(responseFormat)) throw new Error('response_format must be an object');
|
|
123
|
+
const keys = Object.keys(responseFormat);
|
|
124
|
+
if (keys.some((key) => key !== 'name' && key !== 'schema')) {
|
|
125
|
+
throw new Error('response_format accepts only name and schema');
|
|
126
|
+
}
|
|
127
|
+
if (typeof responseFormat.name !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(responseFormat.name)) {
|
|
128
|
+
throw new Error('response_format.name must start with a letter and contain at most 64 letters, digits, underscores, or hyphens');
|
|
129
|
+
}
|
|
130
|
+
if (!isPlainObject(responseFormat.schema) || responseFormat.schema.type !== 'object') {
|
|
131
|
+
throw new Error('response_format.schema root must have type "object"');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let serializedSchema;
|
|
135
|
+
try {
|
|
136
|
+
serializedSchema = JSON.stringify(responseFormat.schema);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new Error(`response_format.schema must be JSON-serializable: ${boundedText(error)}`);
|
|
139
|
+
}
|
|
140
|
+
if (Buffer.byteLength(serializedSchema, 'utf8') > MAX_SCHEMA_BYTES) {
|
|
141
|
+
throw new Error(`response_format.schema exceeds ${MAX_SCHEMA_BYTES} bytes`);
|
|
142
|
+
}
|
|
143
|
+
const schema = JSON.parse(serializedSchema);
|
|
144
|
+
inspectSchemaNode(schema, 'response_format.schema', 1, { nodes: 0 });
|
|
145
|
+
if (typeof compileValidator !== 'function') throw new Error('response_format validator compiler is unavailable');
|
|
146
|
+
|
|
147
|
+
let validator;
|
|
148
|
+
try {
|
|
149
|
+
validator = compileValidator(schema);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new Error(`response_format.schema could not be compiled: ${boundedText(error)}`);
|
|
152
|
+
}
|
|
153
|
+
if (typeof validator !== 'function') throw new Error('response_format validator compiler returned no validator');
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
name: responseFormat.name,
|
|
156
|
+
schema: Object.freeze(schema),
|
|
157
|
+
serializedSchema,
|
|
158
|
+
validator,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatValidatorErrors(errors) {
|
|
163
|
+
if (!Array.isArray(errors) || errors.length === 0) return 'value does not match response_format.schema';
|
|
164
|
+
return boundedText(errors.map((error) => {
|
|
165
|
+
const path = typeof error?.instancePath === 'string' && error.instancePath.length > 0
|
|
166
|
+
? error.instancePath
|
|
167
|
+
: '$';
|
|
168
|
+
if (error?.keyword === 'required' && typeof error?.params?.missingProperty === 'string') {
|
|
169
|
+
return `${path} must have required property ${JSON.stringify(error.params.missingProperty)}`;
|
|
170
|
+
}
|
|
171
|
+
if (error?.keyword === 'additionalProperties' && typeof error?.params?.additionalProperty === 'string') {
|
|
172
|
+
return `${path} must not have additional property ${JSON.stringify(error.params.additionalProperty)}`;
|
|
173
|
+
}
|
|
174
|
+
return `${path} ${error?.message || 'is invalid'}`;
|
|
175
|
+
}).join('; '));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateStructuredSubagentOutput(text, prepared) {
|
|
179
|
+
if (typeof text !== 'string') return { ok: false, error: 'final response is not text' };
|
|
180
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_OUTPUT_BYTES) {
|
|
181
|
+
return { ok: false, error: `final response exceeds ${MAX_OUTPUT_BYTES} bytes` };
|
|
182
|
+
}
|
|
183
|
+
let value;
|
|
184
|
+
try {
|
|
185
|
+
value = JSON.parse(text);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return { ok: false, error: `final response is not exactly one JSON value: ${boundedText(error)}` };
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
if (!prepared.validator(value)) {
|
|
191
|
+
return { ok: false, error: formatValidatorErrors(prepared.validator.errors) };
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
validatorException: true,
|
|
197
|
+
error: `response_format validator exception: ${boundedText(error)}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, value };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildStructuredResponseFormatReminder(prepared) {
|
|
204
|
+
return [
|
|
205
|
+
`Your final assistant message for this run must be exactly one JSON object matching response format ${JSON.stringify(prepared.name)}.`,
|
|
206
|
+
'Do not wrap it in Markdown, a code fence, XML, or explanatory prose.',
|
|
207
|
+
'Tool use and intermediate work remain unchanged. This schema grants no additional capability.',
|
|
208
|
+
`JSON Schema: ${prepared.serializedSchema}`,
|
|
209
|
+
].join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildStructuredResponseRepairPrompt(prepared, error) {
|
|
213
|
+
return [
|
|
214
|
+
'Your previous final response did not satisfy the required response format.',
|
|
215
|
+
`Validation error: ${boundedText(error)}`,
|
|
216
|
+
`Return exactly one corrected JSON object for ${JSON.stringify(prepared.name)} and nothing else.`,
|
|
217
|
+
`JSON Schema: ${prepared.serializedSchema}`,
|
|
218
|
+
].join('\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function safeTokenCount(value) {
|
|
222
|
+
return Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildStructuredSubagentEnvelope({ agentId, profileName, responseFormat, result, usage }) {
|
|
226
|
+
return JSON.stringify({
|
|
227
|
+
agent_id: agentId,
|
|
228
|
+
actual_subagent_type: profileName,
|
|
229
|
+
status: 'completed',
|
|
230
|
+
response_format: responseFormat.name,
|
|
231
|
+
result,
|
|
232
|
+
usage: {
|
|
233
|
+
input: safeTokenCount(usage?.inputOther),
|
|
234
|
+
output: safeTokenCount(usage?.output),
|
|
235
|
+
cache_read: safeTokenCount(usage?.inputCacheRead),
|
|
236
|
+
cache_write: safeTokenCount(usage?.inputCacheCreation),
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
MAX_SCHEMA_BYTES,
|
|
243
|
+
MAX_OUTPUT_BYTES,
|
|
244
|
+
MAX_SCHEMA_DEPTH,
|
|
245
|
+
MAX_SCHEMA_NODES,
|
|
246
|
+
STRUCTURED_SUBAGENT_OUTPUT_ERROR,
|
|
247
|
+
prepareStructuredResponseFormat,
|
|
248
|
+
validateStructuredSubagentOutput,
|
|
249
|
+
buildStructuredResponseFormatReminder,
|
|
250
|
+
buildStructuredResponseRepairPrompt,
|
|
251
|
+
buildStructuredSubagentEnvelope,
|
|
252
|
+
};
|