blun-king-cli 9.1.527 → 9.1.536
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/CHANGELOG.md +61 -0
- package/LIESMICH.txt +36 -1
- package/README.md +35 -1
- package/bin/agent-resume-snapshot.cjs +31 -0
- package/bin/assistant-message-offload-policy.cjs +21 -2
- package/bin/codebase-search-runtime.cjs +23 -0
- package/bin/empty-response-retry-policy.cjs +29 -0
- package/bin/fredrik-glm-provider.cjs +256 -0
- package/bin/history-offload-pressure-policy.cjs +33 -0
- package/bin/programmatic-context-isolation.cjs +25 -0
- package/bin/programmatic-tool-runtime.mjs +301 -0
- package/bin/skill-activation-performance-policy.cjs +9 -0
- package/bin/structured-subagent-output.cjs +252 -0
- 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 +29 -0
- package/bin/turn-thinking-policy.cjs +6 -15
- package/bin/turn-tool-performance-policy.cjs +5 -4
- package/bin/user-message-offload-policy.cjs +10 -1
- package/blun.mjs +656 -127
- package/codebase-index/README.md +70 -0
- package/codebase-index/codebase_index.py +358 -0
- package/fredrik-glm-profile.toml.example +26 -0
- package/package.json +24 -3
- package/scripts/check-active-work-steer-regression.js +46 -0
- package/scripts/check-codebase-search-packaging-regression.js +92 -0
- package/scripts/check-current-turn-read-pin-mutation-regression.js +72 -0
- package/scripts/check-current-turn-read-pin-regression.js +94 -0
- package/scripts/check-deepseek-native-max-regression.js +49 -0
- package/scripts/check-empty-response-effort-downgrade-regression.js +48 -0
- package/scripts/check-fredrik-glm-mutation-regression.js +18 -0
- package/scripts/check-fredrik-glm-regression.js +169 -0
- package/scripts/check-history-pressure-offload-regression.js +77 -0
- package/scripts/check-programmatic-context-isolation-regression.js +193 -0
- package/scripts/check-programmatic-tool-regression.js +294 -0
- package/scripts/check-resume-replay-regression.js +2 -0
- package/scripts/check-startup-swarm-command-regression.js +24 -0
- package/scripts/check-structured-subagent-output-regression.js +331 -0
- package/scripts/check-telegram-direct-work-resume-regression.js +53 -0
- package/scripts/check-todo-progress-regression.js +416 -0
- package/scripts/check-tool-schema-capacity-regression.js +40 -0
- package/scripts/programmatic-tool-runtime.test.mjs +365 -0
- package/scripts/structured-subagent-output.test.cjs +170 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PROGRAMMATIC_WIRE_ONLY = 'programmatic_nested';
|
|
4
|
+
|
|
5
|
+
function isNestedToolEvent(event) {
|
|
6
|
+
return event?.type === 'tool.call' || event?.type === 'tool.result';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function markProgrammaticNestedEvent(event) {
|
|
10
|
+
if (!isNestedToolEvent(event)) return event;
|
|
11
|
+
return {
|
|
12
|
+
...event,
|
|
13
|
+
contextVisibility: PROGRAMMATIC_WIRE_ONLY,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isWireOnlyProgrammaticEvent(event) {
|
|
18
|
+
return isNestedToolEvent(event) && event.contextVisibility === PROGRAMMATIC_WIRE_ONLY;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
PROGRAMMATIC_WIRE_ONLY,
|
|
23
|
+
isWireOnlyProgrammaticEvent,
|
|
24
|
+
markProgrammaticNestedEvent,
|
|
25
|
+
};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RELEASE_ASYNC,
|
|
3
|
+
newQuickJSAsyncWASMModule,
|
|
4
|
+
shouldInterruptAfterDeadline,
|
|
5
|
+
} from 'quickjs-emscripten';
|
|
6
|
+
|
|
7
|
+
const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/u;
|
|
8
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
9
|
+
memoryBytes: 16 * 1024 * 1024,
|
|
10
|
+
stackBytes: 512 * 1024,
|
|
11
|
+
timeoutMs: 120_000,
|
|
12
|
+
guestExecutionMs: 3_000,
|
|
13
|
+
maxCodeChars: 32_000,
|
|
14
|
+
maxStateChars: 65_536,
|
|
15
|
+
maxOutputChars: 65_536,
|
|
16
|
+
maxToolArgumentChars: 32_768,
|
|
17
|
+
maxToolResultChars: 131_072,
|
|
18
|
+
maxToolCalls: 64,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let callSequence = 0;
|
|
22
|
+
|
|
23
|
+
export function createProgrammaticToolRuntime(options) {
|
|
24
|
+
if (typeof options?.invokeTool !== 'function') {
|
|
25
|
+
throw new TypeError('invokeTool must be a function');
|
|
26
|
+
}
|
|
27
|
+
const allowedTools = normalizeAllowedTools(options.allowedTools);
|
|
28
|
+
const limits = normalizeLimits(options.limits);
|
|
29
|
+
const emit = typeof options.onEvent === 'function' ? options.onEvent : () => {};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
async run(input) {
|
|
33
|
+
const code = String(input?.code ?? '');
|
|
34
|
+
if (code.length > limits.maxCodeChars) {
|
|
35
|
+
throw new Error(`program exceeds ${limits.maxCodeChars} characters`);
|
|
36
|
+
}
|
|
37
|
+
const initialState = normalizeJsonObject(input?.state ?? {}, 'state');
|
|
38
|
+
const initialStateJson = JSON.stringify(initialState);
|
|
39
|
+
if (initialStateJson.length > limits.maxStateChars) {
|
|
40
|
+
throw new Error(`state exceeds ${limits.maxStateChars} characters`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const startedAt = Date.now();
|
|
44
|
+
const wallDeadline = startedAt + limits.timeoutMs;
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeout = setTimeout(() => controller.abort(new Error('execution deadline exceeded')), limits.timeoutMs);
|
|
47
|
+
const detachOuterAbort = forwardAbort(input?.signal, controller);
|
|
48
|
+
let callCount = 0;
|
|
49
|
+
let module;
|
|
50
|
+
let context;
|
|
51
|
+
emit({ type: 'interpreter.started', toolCount: allowedTools.length });
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
module = await newQuickJSAsyncWASMModule(RELEASE_ASYNC);
|
|
55
|
+
context = module.newContext();
|
|
56
|
+
const runtime = context.runtime;
|
|
57
|
+
let guestDeadline = Date.now() + limits.guestExecutionMs;
|
|
58
|
+
runtime.setMemoryLimit(limits.memoryBytes);
|
|
59
|
+
runtime.setMaxStackSize(limits.stackBytes);
|
|
60
|
+
runtime.setInterruptHandler(() => (
|
|
61
|
+
controller.signal.aborted || shouldInterruptAfterDeadline(guestDeadline)()
|
|
62
|
+
));
|
|
63
|
+
for (const [index, tool] of allowedTools.entries()) {
|
|
64
|
+
const bridgeName = `__blun_ptc_${index}`;
|
|
65
|
+
const bridge = context.newAsyncifiedFunction(bridgeName, async (rawHandle) => {
|
|
66
|
+
if (callCount >= limits.maxToolCalls) {
|
|
67
|
+
return context.newString(errorEnvelope(`tool-call limit of ${limits.maxToolCalls} exceeded`));
|
|
68
|
+
}
|
|
69
|
+
const raw = rawHandle === undefined ? '{}' : context.getString(rawHandle);
|
|
70
|
+
if (raw.length > limits.maxToolArgumentChars) {
|
|
71
|
+
return context.newString(errorEnvelope(`tool arguments exceed ${limits.maxToolArgumentChars} characters`));
|
|
72
|
+
}
|
|
73
|
+
let args;
|
|
74
|
+
try {
|
|
75
|
+
args = normalizeJsonObject(JSON.parse(raw), 'tool arguments');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return context.newString(errorEnvelope(errorMessage(error)));
|
|
78
|
+
}
|
|
79
|
+
callCount += 1;
|
|
80
|
+
const callId = nextCallId(tool.name);
|
|
81
|
+
emit({
|
|
82
|
+
type: 'interpreter.tool_call',
|
|
83
|
+
callId,
|
|
84
|
+
toolName: tool.name,
|
|
85
|
+
args,
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
const hostStartedAt = Date.now();
|
|
89
|
+
let value;
|
|
90
|
+
try {
|
|
91
|
+
value = await raceHostCall(
|
|
92
|
+
options.invokeTool({ name: tool.name, args, callId, signal: controller.signal }),
|
|
93
|
+
controller.signal,
|
|
94
|
+
wallDeadline,
|
|
95
|
+
);
|
|
96
|
+
} finally {
|
|
97
|
+
guestDeadline += Date.now() - hostStartedAt;
|
|
98
|
+
}
|
|
99
|
+
const valueJson = jsonStringify(value, `tool ${tool.name} result`);
|
|
100
|
+
if (valueJson.length > limits.maxToolResultChars) {
|
|
101
|
+
throw new Error(`tool result exceeds ${limits.maxToolResultChars} characters`);
|
|
102
|
+
}
|
|
103
|
+
emit({
|
|
104
|
+
type: 'interpreter.tool_result',
|
|
105
|
+
callId,
|
|
106
|
+
toolName: tool.name,
|
|
107
|
+
isError: false,
|
|
108
|
+
});
|
|
109
|
+
return context.newString(JSON.stringify({ ok: true, value }));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const message = errorMessage(error);
|
|
112
|
+
emit({
|
|
113
|
+
type: 'interpreter.tool_result',
|
|
114
|
+
callId,
|
|
115
|
+
toolName: tool.name,
|
|
116
|
+
isError: true,
|
|
117
|
+
error: message,
|
|
118
|
+
});
|
|
119
|
+
return context.newString(errorEnvelope(message));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
context.setProp(context.global, bridgeName, bridge);
|
|
123
|
+
bridge.dispose();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
evaluateBootstrap(context, renderBootstrap(allowedTools, initialStateJson));
|
|
127
|
+
const evaluated = await context.evalCodeAsync(renderProgram(code), 'blun-ptc.js');
|
|
128
|
+
if (evaluated.error) {
|
|
129
|
+
const dumped = context.dump(evaluated.error);
|
|
130
|
+
evaluated.error.dispose();
|
|
131
|
+
throw guestError(dumped, controller.signal, guestDeadline);
|
|
132
|
+
}
|
|
133
|
+
runtime.setMemoryLimit(-1);
|
|
134
|
+
const payloadJson = context.getString(evaluated.value);
|
|
135
|
+
evaluated.value.dispose();
|
|
136
|
+
const payload = JSON.parse(payloadJson);
|
|
137
|
+
const resultJson = jsonStringify(payload.result, 'result');
|
|
138
|
+
const stateJson = jsonStringify(payload.state, 'state');
|
|
139
|
+
if (resultJson.length > limits.maxOutputChars) {
|
|
140
|
+
throw new Error(`result exceeds ${limits.maxOutputChars} characters`);
|
|
141
|
+
}
|
|
142
|
+
if (stateJson.length > limits.maxStateChars) {
|
|
143
|
+
throw new Error(`state exceeds ${limits.maxStateChars} characters`);
|
|
144
|
+
}
|
|
145
|
+
const outcome = { result: payload.result, state: payload.state, callCount };
|
|
146
|
+
emit({ type: 'interpreter.completed', callCount, durationMs: Date.now() - startedAt });
|
|
147
|
+
return outcome;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const normalized = normalizeExecutionError(error, controller.signal);
|
|
150
|
+
emit({
|
|
151
|
+
type: 'interpreter.failed',
|
|
152
|
+
callCount,
|
|
153
|
+
durationMs: Date.now() - startedAt,
|
|
154
|
+
error: normalized.message,
|
|
155
|
+
});
|
|
156
|
+
throw normalized;
|
|
157
|
+
} finally {
|
|
158
|
+
clearTimeout(timeout);
|
|
159
|
+
detachOuterAbort();
|
|
160
|
+
context?.dispose();
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeAllowedTools(value) {
|
|
167
|
+
if (!Array.isArray(value)) throw new TypeError('allowedTools must be an array');
|
|
168
|
+
const names = new Set();
|
|
169
|
+
const aliases = new Set();
|
|
170
|
+
return value.map((entry) => {
|
|
171
|
+
const name = String(entry?.name ?? '').trim();
|
|
172
|
+
const alias = String(entry?.alias ?? '').trim();
|
|
173
|
+
if (!name) throw new Error('tool name must not be empty');
|
|
174
|
+
if (!IDENTIFIER_RE.test(alias)) throw new Error(`${alias || '[empty]'} is not a valid JavaScript identifier`);
|
|
175
|
+
if (names.has(name)) throw new Error(`duplicate tool name: ${name}`);
|
|
176
|
+
if (aliases.has(alias)) throw new Error(`duplicate tool alias: ${alias}`);
|
|
177
|
+
names.add(name);
|
|
178
|
+
aliases.add(alias);
|
|
179
|
+
return Object.freeze({ name, alias });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeLimits(value = {}) {
|
|
184
|
+
const limits = { ...DEFAULT_LIMITS, ...value };
|
|
185
|
+
for (const [name, number] of Object.entries(limits)) {
|
|
186
|
+
if (!Number.isSafeInteger(number) || number <= 0) throw new Error(`${name} must be a positive integer`);
|
|
187
|
+
}
|
|
188
|
+
return Object.freeze(limits);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeJsonObject(value, label) {
|
|
192
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
193
|
+
throw new Error(`${label} must be a JSON object`);
|
|
194
|
+
}
|
|
195
|
+
return JSON.parse(jsonStringify(value, label));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function jsonStringify(value, label) {
|
|
199
|
+
try {
|
|
200
|
+
const result = JSON.stringify(value);
|
|
201
|
+
if (result === undefined) throw new Error(`${label} is not JSON serializable`);
|
|
202
|
+
return result;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new Error(`${label} is not JSON serializable: ${errorMessage(error)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderBootstrap(tools, stateJson) {
|
|
209
|
+
const entries = tools.map((tool, index) => {
|
|
210
|
+
const bridge = `globalThis.__blun_ptc_${index}`;
|
|
211
|
+
return `${JSON.stringify(tool.alias)}: (() => { const call = ${bridge}; delete ${bridge}; return (args = {}) => { const envelope = JSON.parse(call(JSON.stringify(args))); if (!envelope.ok) throw new Error(envelope.error); return envelope.value; }; })()`;
|
|
212
|
+
});
|
|
213
|
+
return `
|
|
214
|
+
globalThis.state = JSON.parse(${JSON.stringify(stateJson)});
|
|
215
|
+
globalThis.tools = Object.freeze({ ${entries.join(',')} });
|
|
216
|
+
delete globalThis.process;
|
|
217
|
+
delete globalThis.require;
|
|
218
|
+
delete globalThis.fetch;
|
|
219
|
+
delete globalThis.Date;
|
|
220
|
+
delete globalThis.performance;
|
|
221
|
+
delete globalThis.eval;
|
|
222
|
+
delete globalThis.Function;
|
|
223
|
+
undefined;
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function renderProgram(code) {
|
|
228
|
+
return `JSON.stringify({ result: (() => { "use strict"; ${code}\n})(), state: globalThis.state })`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function evaluateBootstrap(context, source) {
|
|
232
|
+
const result = context.evalCode(source, 'blun-ptc-bootstrap.js');
|
|
233
|
+
if (result.error) {
|
|
234
|
+
const dumped = context.dump(result.error);
|
|
235
|
+
result.error.dispose();
|
|
236
|
+
throw new Error(`interpreter bootstrap failed: ${errorMessage(dumped)}`);
|
|
237
|
+
}
|
|
238
|
+
result.value.dispose();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function errorEnvelope(message) {
|
|
242
|
+
return JSON.stringify({ ok: false, error: String(message) });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function nextCallId(toolName) {
|
|
246
|
+
callSequence += 1;
|
|
247
|
+
return `ptc_${toolName.replace(/[^A-Za-z0-9_]/gu, '_')}_${callSequence}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function forwardAbort(signal, controller) {
|
|
251
|
+
if (!signal) return () => {};
|
|
252
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
253
|
+
if (signal.aborted) onAbort();
|
|
254
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
255
|
+
return () => signal.removeEventListener('abort', onAbort);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function raceHostCall(value, signal, deadline) {
|
|
259
|
+
if (signal.aborted) throw signal.reason ?? new Error('program aborted');
|
|
260
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
261
|
+
let timer;
|
|
262
|
+
let onAbort;
|
|
263
|
+
const blocked = new Promise((_, reject) => {
|
|
264
|
+
timer = setTimeout(() => reject(new Error('execution deadline exceeded')), remaining);
|
|
265
|
+
onAbort = () => reject(signal.reason ?? new Error('program aborted'));
|
|
266
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
267
|
+
});
|
|
268
|
+
try {
|
|
269
|
+
return await Promise.race([Promise.resolve(value), blocked]);
|
|
270
|
+
} finally {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
signal.removeEventListener('abort', onAbort);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function guestError(value, signal, deadline) {
|
|
277
|
+
if (signal.aborted) return abortReason(signal);
|
|
278
|
+
if (Date.now() >= deadline || value?.message === 'interrupted') {
|
|
279
|
+
return new Error('execution deadline exceeded');
|
|
280
|
+
}
|
|
281
|
+
return new Error(value?.message ?? errorMessage(value));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function normalizeExecutionError(error, signal) {
|
|
285
|
+
if (signal.aborted) return abortReason(signal);
|
|
286
|
+
if (errorMessage(error).includes('interrupted')) {
|
|
287
|
+
return new Error('guest execution deadline exceeded');
|
|
288
|
+
}
|
|
289
|
+
return error instanceof Error ? error : new Error(errorMessage(error));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function abortReason(signal) {
|
|
293
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
294
|
+
return new Error(signal.reason === undefined ? 'program aborted' : String(signal.reason));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function errorMessage(error) {
|
|
298
|
+
if (error instanceof Error) return error.message;
|
|
299
|
+
if (error && typeof error === 'object' && typeof error.message === 'string') return error.message;
|
|
300
|
+
return String(error);
|
|
301
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
hasHistoricalOffloadPressure,
|
|
5
|
+
historyTextChars,
|
|
6
|
+
} = require('./history-offload-pressure-policy.cjs');
|
|
7
|
+
|
|
3
8
|
const SKILL_ACTIVATION_KEEP_RECENT_MESSAGES = 20;
|
|
4
9
|
const SKILL_ACTIVATION_MIN_CHARS = 4_000;
|
|
5
10
|
const SKILL_ACTIVATION_MARKER = '[Earlier skill activation compacted]';
|
|
@@ -40,6 +45,10 @@ function compactSkillActivationMessage(message) {
|
|
|
40
45
|
|
|
41
46
|
function compactHistoricalSkillActivations(messages) {
|
|
42
47
|
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
48
|
+
if (!hasHistoricalOffloadPressure({
|
|
49
|
+
historyLength: messages.length,
|
|
50
|
+
historyChars: historyTextChars(messages),
|
|
51
|
+
})) return messages;
|
|
43
52
|
const recentStart = Math.max(0, messages.length - SKILL_ACTIVATION_KEEP_RECENT_MESSAGES);
|
|
44
53
|
let changed = false;
|
|
45
54
|
const projected = messages.map((message, index) => {
|
|
@@ -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
|
+
};
|
|
@@ -10,6 +10,9 @@ const CONVERSATION_CLOSE = /^(?:danke(?: dir)?|dankesch(?:oe|\u00f6)n|alles klar
|
|
|
10
10
|
const EXPLICIT_PAUSE = /^(?:(?:bitte\s+)?(?:pause|pausier(?:e)?|stop|stopp|warte|halt(?:e)?\s+an|nicht\s+weiter(?:machen)?))(?:[\s.!?,].*)?$/iu;
|
|
11
11
|
const DIRECT_TELEGRAM_CHANNEL = /<channel\b(?=[^>]*\bsource=["']telegram["'])(?=[^>]*\bpriority=["']direct["'])[^>]*>/iu;
|
|
12
12
|
const TELEGRAM_REPLY_TOOL = /^mcp__[^\s]*telegram[^\s]*__reply$/iu;
|
|
13
|
+
const DIRECT_CHANNEL_PAYLOAD = /<channel\b(?=[^>]*\bsource=["']telegram["'])(?=[^>]*\bpriority=["']direct["'])[^>]*>([\s\S]*?)<\/channel>/giu;
|
|
14
|
+
const DIRECT_WORK_INTENT = /\b(?:auftrag|arbeite|bau(?:e|en)?|build|check|continue|edit|feuer\s+frei|fix|fortsetzen|implement|install|lies|mach(?:e)?\s+weiter|mess|patch|pruef|resume|run|schreib|starte?|test|update|weiterarbeiten|weiterbauen|write)\b/iu;
|
|
15
|
+
const DIRECT_WORK_PAUSE = /\b(?:halt(?:e)?\s+an|nicht\s+weiter(?:machen)?|pause|pausier(?:e)?|stop|stopp|warte)\b/iu;
|
|
13
16
|
|
|
14
17
|
function isPrivateTelegramChat(chatId) {
|
|
15
18
|
return /^[1-9]\d*$/u.test(String(chatId ?? '').trim());
|
|
@@ -55,8 +58,26 @@ function isExplicitPauseRequest(text) {
|
|
|
55
58
|
return EXPLICIT_PAUSE.test(String(text ?? '').trim());
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
function directTelegramPayloadText(inputText) {
|
|
62
|
+
const matches = [...String(inputText ?? '').matchAll(DIRECT_CHANNEL_PAYLOAD)];
|
|
63
|
+
return String(matches.at(-1)?.[1] ?? '').trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function directMessageAllowsWorkContinuation(inputText) {
|
|
67
|
+
if (!DIRECT_TELEGRAM_CHANNEL.test(String(inputText ?? ''))) return false;
|
|
68
|
+
const payload = directTelegramPayloadText(inputText);
|
|
69
|
+
return payload.length > 0 && !DIRECT_WORK_PAUSE.test(payload);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function directMessageStartsOrResumesWork(inputText) {
|
|
73
|
+
if (!directMessageAllowsWorkContinuation(inputText)) return false;
|
|
74
|
+
const payload = directTelegramPayloadText(inputText);
|
|
75
|
+
return isResumeApproval(payload) || DIRECT_WORK_INTENT.test(payload);
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
function createDirectReplyTurnStop(inputText) {
|
|
59
|
-
const direct = DIRECT_TELEGRAM_CHANNEL.test(String(inputText ?? ''))
|
|
79
|
+
const direct = DIRECT_TELEGRAM_CHANNEL.test(String(inputText ?? ''))
|
|
80
|
+
&& !directMessageStartsOrResumesWork(inputText);
|
|
60
81
|
let delivered = false;
|
|
61
82
|
return {
|
|
62
83
|
noteToolResult(toolName, isError) {
|
|
@@ -236,6 +257,9 @@ module.exports = {
|
|
|
236
257
|
DEFAULT_SILENCE_MS,
|
|
237
258
|
createDirectFocusController,
|
|
238
259
|
createDirectReplyTurnStop,
|
|
260
|
+
directMessageAllowsWorkContinuation,
|
|
261
|
+
directMessageStartsOrResumesWork,
|
|
262
|
+
directTelegramPayloadText,
|
|
239
263
|
directFocusCheckpointPath,
|
|
240
264
|
enqueueTelegramDirect,
|
|
241
265
|
isConversationClose,
|