blun-king-cli 9.1.536 → 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 -869
- package/README.md +41 -833
- package/bin/assistant-message-offload-policy.cjs +3 -1
- 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/html-to-research-markdown.cjs +146 -0
- package/bin/programmatic-tool-runtime.mjs +330 -4
- package/bin/read-continuation-policy.cjs +36 -5
- package/bin/scoped-cron-run-policy.cjs +358 -0
- package/bin/startup-preferences.cjs +3 -3
- package/bin/structured-agent-swarm-output.cjs +325 -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/tool-result-offload-policy.cjs +14 -1
- package/bin/update-notice.js +14 -18
- package/bin/user-message-offload-policy.cjs +3 -1
- package/blun.mjs +1089 -477
- package/codebase-index/README.md +12 -0
- package/codebase-index/codebase_index.py +129 -18
- package/package.json +23 -58
- package/telegram-plugin/bin/telegram-typing-keepalive.cjs +89 -0
- package/telegram-plugin/dist/bridge.mjs +8 -1
- package/CHANGELOG.md +0 -321
- 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/fredrik-glm-provider.cjs +0 -256
- package/bin/package-regression-policy.cjs +0 -77
- package/fredrik-glm-profile.toml.example +0 -26
- package/release-planned-removals.json +0 -15
- package/scripts/check-active-profile-plugin-startup.js +0 -36
- package/scripts/check-active-work-steer-regression.js +0 -46
- 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-codebase-search-packaging-regression.js +0 -92
- package/scripts/check-copy-command-regression.js +0 -74
- package/scripts/check-current-turn-read-pin-mutation-regression.js +0 -72
- package/scripts/check-current-turn-read-pin-regression.js +0 -94
- package/scripts/check-deepseek-native-max-regression.js +0 -49
- package/scripts/check-empty-response-effort-downgrade-regression.js +0 -48
- package/scripts/check-fredrik-glm-mutation-regression.js +0 -18
- package/scripts/check-fredrik-glm-regression.js +0 -169
- package/scripts/check-historical-tool-result-preview-regression.js +0 -77
- package/scripts/check-history-pressure-offload-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-programmatic-context-isolation-regression.js +0 -193
- package/scripts/check-programmatic-tool-regression.js +0 -294
- 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 -102
- 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-startup-swarm-command-regression.js +0 -24
- package/scripts/check-structured-subagent-output-regression.js +0 -331
- package/scripts/check-telegram-bridge-watchdog.js +0 -60
- package/scripts/check-telegram-direct-work-resume-regression.js +0 -53
- package/scripts/check-telegram-loop-exactly-once-regression.js +0 -71
- package/scripts/check-todo-loop-regression.js +0 -78
- package/scripts/check-todo-progress-regression.js +0 -416
- package/scripts/check-todo-recovery-catalog-regression.js +0 -50
- package/scripts/check-tool-schema-capacity-regression.js +0 -40
- package/scripts/programmatic-tool-runtime.test.mjs +0 -365
- package/scripts/structured-subagent-output.test.cjs +0 -170
- /package/{scripts → bin}/fix-node-pty-perms.js +0 -0
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const assert = require('node:assert/strict');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const vm = require('node:vm');
|
|
7
|
-
const { spawn } = require('node:child_process');
|
|
8
|
-
|
|
9
|
-
const packageRoot = path.resolve(__dirname, '..');
|
|
10
|
-
const bundlePath = process.argv[2] === undefined
|
|
11
|
-
? path.join(packageRoot, 'blun.mjs')
|
|
12
|
-
: path.resolve(process.argv[2]);
|
|
13
|
-
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
14
|
-
const source = /function buildLocalSpawnOptions\(isWindows, cwd, env\) \{[\s\S]*?\n\}/.exec(bundle)?.[0];
|
|
15
|
-
|
|
16
|
-
assert(source, 'SHELL_TERMINAL_ISOLATION: buildLocalSpawnOptions is missing');
|
|
17
|
-
|
|
18
|
-
const context = {};
|
|
19
|
-
vm.runInNewContext(`${source}; globalThis.buildLocalSpawnOptions = buildLocalSpawnOptions;`, context);
|
|
20
|
-
const buildLocalSpawnOptions = context.buildLocalSpawnOptions;
|
|
21
|
-
const cwd = packageRoot;
|
|
22
|
-
const env = { ...process.env, BLUN_SHELL_ISOLATION_PROBE: '1' };
|
|
23
|
-
|
|
24
|
-
for (const isWindows of [false, true]) {
|
|
25
|
-
const options = buildLocalSpawnOptions(isWindows, cwd, env);
|
|
26
|
-
assert.deepEqual(Array.from(options.stdio), ['pipe', 'pipe', 'pipe']);
|
|
27
|
-
assert.equal(options.detached, true,
|
|
28
|
-
`local shell must start in a separate process group when isWindows=${String(isWindows)}`);
|
|
29
|
-
assert.equal(options.windowsHide, true);
|
|
30
|
-
assert.equal(options.cwd, cwd);
|
|
31
|
-
assert.equal(options.env, env);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function runCapturedProcessProbe() {
|
|
35
|
-
const options = buildLocalSpawnOptions(process.platform === 'win32', cwd, env);
|
|
36
|
-
const child = spawn(process.execPath, [
|
|
37
|
-
'-e',
|
|
38
|
-
[
|
|
39
|
-
"process.stdin.setEncoding('utf8')",
|
|
40
|
-
"process.stdin.on('data', () => { process.exitCode = 90 })",
|
|
41
|
-
"process.stdin.on('end', () => {",
|
|
42
|
-
" process.stdout.write('stdout-ok')",
|
|
43
|
-
" process.stderr.write('stderr-ok')",
|
|
44
|
-
'})',
|
|
45
|
-
].join(';'),
|
|
46
|
-
], options);
|
|
47
|
-
|
|
48
|
-
let stdout = '';
|
|
49
|
-
let stderr = '';
|
|
50
|
-
child.stdout.setEncoding('utf8');
|
|
51
|
-
child.stderr.setEncoding('utf8');
|
|
52
|
-
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
53
|
-
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
54
|
-
child.stdin.end();
|
|
55
|
-
|
|
56
|
-
const exit = await new Promise((resolve, reject) => {
|
|
57
|
-
const timer = setTimeout(() => {
|
|
58
|
-
child.kill();
|
|
59
|
-
reject(new Error('captured child did not observe stdin EOF'));
|
|
60
|
-
}, 10_000);
|
|
61
|
-
child.once('error', (error) => {
|
|
62
|
-
clearTimeout(timer);
|
|
63
|
-
reject(error);
|
|
64
|
-
});
|
|
65
|
-
child.once('close', (code, signal) => {
|
|
66
|
-
clearTimeout(timer);
|
|
67
|
-
resolve({ code, signal });
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
assert.deepEqual(exit, { code: 0, signal: null });
|
|
72
|
-
assert.equal(stdout, 'stdout-ok');
|
|
73
|
-
assert.equal(stderr, 'stderr-ok');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
runCapturedProcessProbe().then(() => {
|
|
77
|
-
process.stdout.write('shell-terminal-isolation-regression PASS\n');
|
|
78
|
-
}).catch((error) => {
|
|
79
|
-
console.error(error);
|
|
80
|
-
process.exitCode = 1;
|
|
81
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
const assert = require('node:assert/strict');
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
|
|
5
|
-
const packageRoot = path.resolve(__dirname, '..');
|
|
6
|
-
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
7
|
-
const match = /editor\.onEscape = \(autocompleteCancelled = false\) => \{([\s\S]*?)\n\t\t\};\n\t\teditor\.onShiftTab =/.exec(bundle);
|
|
8
|
-
assert(match, 'SLASH_ESCAPE_REGRESSION: editor.onEscape handler is missing');
|
|
9
|
-
|
|
10
|
-
const runEscape = Function('host', 'editor', 'autocompleteCancelled', match[1]);
|
|
11
|
-
const ctrlSMatch = /editor\.onCtrlS = \(\) => \{([\s\S]*?)\n\t\t\};\n\t\teditor\.onCtrlB =/.exec(bundle);
|
|
12
|
-
assert(ctrlSMatch, 'SLASH_ESCAPE_REGRESSION: editor.onCtrlS handler is missing');
|
|
13
|
-
const runCtrlS = Function('host', 'editor', ctrlSMatch[1]);
|
|
14
|
-
|
|
15
|
-
function probe(initialText, autocompleteCancelled) {
|
|
16
|
-
let text = initialText;
|
|
17
|
-
const editor = {
|
|
18
|
-
inputMode: 'prompt',
|
|
19
|
-
getText: () => text,
|
|
20
|
-
setText: (value) => { text = value; },
|
|
21
|
-
};
|
|
22
|
-
const host = {
|
|
23
|
-
cancelInFlight: undefined,
|
|
24
|
-
queueSteerInFlight: undefined,
|
|
25
|
-
state: {
|
|
26
|
-
activeDialog: null,
|
|
27
|
-
appState: {
|
|
28
|
-
isCompacting: false,
|
|
29
|
-
streamingPhase: 'idle',
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
streamingUI: { hasActiveTurn: () => false },
|
|
33
|
-
btwPanelController: { closeOrCancel: () => false },
|
|
34
|
-
};
|
|
35
|
-
const controller = {
|
|
36
|
-
pendingExit: null,
|
|
37
|
-
pendingUndoEsc: null,
|
|
38
|
-
clearPendingExit() {},
|
|
39
|
-
clearPendingUndoEsc() {},
|
|
40
|
-
armPendingUndoEsc() {},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
runEscape.call(controller, host, editor, autocompleteCancelled);
|
|
44
|
-
return text;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
assert.equal(
|
|
48
|
-
probe('/reload', true),
|
|
49
|
-
'',
|
|
50
|
-
'SLASH_ESCAPE_REGRESSION: Escape must cancel the open slash-command draft together with its menu',
|
|
51
|
-
);
|
|
52
|
-
assert.equal(
|
|
53
|
-
probe('ordinary draft', true),
|
|
54
|
-
'ordinary draft',
|
|
55
|
-
'SLASH_ESCAPE_REGRESSION: cancelling ordinary autocomplete must preserve the draft',
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
let flushed = null;
|
|
59
|
-
const idleHost = {
|
|
60
|
-
streamingUI: { hasActiveTurn: () => false },
|
|
61
|
-
state: {
|
|
62
|
-
appState: { streamingPhase: 'idle' },
|
|
63
|
-
ui: { requestRender() {} },
|
|
64
|
-
},
|
|
65
|
-
flushQueuedMessages: (draft, mode) => { flushed = { draft, mode }; },
|
|
66
|
-
updateQueueDisplay() {},
|
|
67
|
-
};
|
|
68
|
-
const slashEditor = {
|
|
69
|
-
inputMode: 'prompt',
|
|
70
|
-
getText: () => '/reload',
|
|
71
|
-
};
|
|
72
|
-
runCtrlS(idleHost, slashEditor);
|
|
73
|
-
assert.deepEqual(
|
|
74
|
-
flushed,
|
|
75
|
-
{ draft: '/reload', mode: 'prompt' },
|
|
76
|
-
'SLASH_ESCAPE_REGRESSION: Ctrl+S must hand an idle slash draft to the existing queue drain',
|
|
77
|
-
);
|
|
78
|
-
flushed = null;
|
|
79
|
-
runCtrlS(idleHost, {
|
|
80
|
-
inputMode: 'prompt',
|
|
81
|
-
getText: () => 'ordinary draft',
|
|
82
|
-
});
|
|
83
|
-
assert.equal(
|
|
84
|
-
flushed,
|
|
85
|
-
null,
|
|
86
|
-
'SLASH_ESCAPE_REGRESSION: the existing idle behavior for ordinary drafts must stay unchanged',
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
console.log('slash-escape-regression PASS (Escape + Ctrl+S)');
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
|
|
8
|
-
const packageRoot = process.env.BLUN_PACKAGE_UNDER_TEST
|
|
9
|
-
? path.resolve(process.env.BLUN_PACKAGE_UNDER_TEST)
|
|
10
|
-
: path.resolve(__dirname, '..');
|
|
11
|
-
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
12
|
-
|
|
13
|
-
for (const needle of [
|
|
14
|
-
'.option("--swarm", "Start in swarm mode.", false)',
|
|
15
|
-
'swarm: raw["swarm"] === true,',
|
|
16
|
-
'thinking: startup.effort,',
|
|
17
|
-
'if (startup.effort !== void 0) await session.setThinking(startup.effort);',
|
|
18
|
-
'if (startup.swarm && !(await session.getStatus()).swarmMode) await session.setSwarmMode(true, "effort");',
|
|
19
|
-
'if (startup.swarm) this.setAppState({ swarmMode: true, swarmModeEntry: "effort" });',
|
|
20
|
-
]) {
|
|
21
|
-
assert.ok(bundle.includes(needle), `startup swarm command wiring missing: ${needle}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
process.stdout.write('startup-swarm-command-regression PASS\n');
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const assert = require('node:assert/strict');
|
|
4
|
-
const { spawnSync } = require('node:child_process');
|
|
5
|
-
const { readFileSync } = require('node:fs');
|
|
6
|
-
const { join } = require('node:path');
|
|
7
|
-
|
|
8
|
-
const root = join(__dirname, '..');
|
|
9
|
-
const bundlePath = join(root, 'blun.mjs');
|
|
10
|
-
|
|
11
|
-
function extractBetween(source, startNeedle, endNeedle) {
|
|
12
|
-
const start = source.indexOf(startNeedle);
|
|
13
|
-
assert.notEqual(start, -1, `missing source start: ${startNeedle}`);
|
|
14
|
-
const end = source.indexOf(endNeedle, start + startNeedle.length);
|
|
15
|
-
assert.notEqual(end, -1, `missing source end: ${endNeedle}`);
|
|
16
|
-
return source.slice(start, end);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function assertBundleContract(source) {
|
|
20
|
-
assert.match(source, /response_format: object\(\{/u, 'Agent input must expose response_format');
|
|
21
|
-
assert.match(
|
|
22
|
-
source,
|
|
23
|
-
/prepareStructuredResponseFormat\(args\.response_format, compileToolArgsValidator\)/u,
|
|
24
|
-
'response_format must reuse BLUN AJV compilation',
|
|
25
|
-
);
|
|
26
|
-
const preparationIndex = source.indexOf('prepareStructuredResponseFormat(args.response_format, compileToolArgsValidator)');
|
|
27
|
-
const launchIndex = source.indexOf('await this.subagentHost.spawn({', preparationIndex);
|
|
28
|
-
assert.ok(preparationIndex >= 0 && launchIndex > preparationIndex, 'schema compilation must precede child creation');
|
|
29
|
-
assert.match(source, /response_format is supported for foreground Agent runs only/u, 'structured background runs must be rejected');
|
|
30
|
-
assert.match(
|
|
31
|
-
source,
|
|
32
|
-
/const runInBackground = responseFormat === void 0 \? resolvedRunMode\.runInBackground : false/u,
|
|
33
|
-
'structured output must select a foreground run',
|
|
34
|
-
);
|
|
35
|
-
assert.match(
|
|
36
|
-
source,
|
|
37
|
-
/appendSystemReminder\(buildStructuredResponseFormatReminder\(options\.responseFormat\)/u,
|
|
38
|
-
'the schema reminder must remain child-local',
|
|
39
|
-
);
|
|
40
|
-
assert.match(
|
|
41
|
-
source,
|
|
42
|
-
/completeStructuredSubagentResult\(child, childId, profileName, options\.responseFormat, options\.signal\)/u,
|
|
43
|
-
'structured completion must validate before returning',
|
|
44
|
-
);
|
|
45
|
-
assert.match(
|
|
46
|
-
source,
|
|
47
|
-
/buildStructuredResponseRepairPrompt\(responseFormat, validation\.error\)[\s\S]{0,500}completeChildTurnWithMaxTokensHandoff\(child, signal\)/u,
|
|
48
|
-
'one repair turn must use the normal child completion path',
|
|
49
|
-
);
|
|
50
|
-
assert.match(
|
|
51
|
-
source,
|
|
52
|
-
/signal\.throwIfAborted\(\);[\s\S]{0,180}buildStructuredResponseRepairPrompt/u,
|
|
53
|
-
'cancellation must be checked before repair',
|
|
54
|
-
);
|
|
55
|
-
assert.match(source, /responseFormat: this\.responseFormat/u, 'timeout continuation must preserve the response format');
|
|
56
|
-
assert.match(
|
|
57
|
-
source,
|
|
58
|
-
/responseFormat === void 0 \? formatForegroundAgentSuccess\(handle, output\) : output/u,
|
|
59
|
-
'validated structured output must not be wrapped in free-form text',
|
|
60
|
-
);
|
|
61
|
-
assert.match(
|
|
62
|
-
source,
|
|
63
|
-
/message\.includes\(STRUCTURED_SUBAGENT_OUTPUT_ERROR\)/u,
|
|
64
|
-
'a second invalid response must preserve the resume hint',
|
|
65
|
-
);
|
|
66
|
-
const swarmRegion = extractBetween(
|
|
67
|
-
source,
|
|
68
|
-
'//#region ../../packages/agent-core/src/tools/builtin/collaboration/agent-swarm.ts',
|
|
69
|
-
'//#region ../../packages/agent-core/src/tools/builtin/collaboration/ask-user.md?raw',
|
|
70
|
-
);
|
|
71
|
-
assert.doesNotMatch(swarmRegion, /response_format/u, 'AgentSwarm must not silently accept response_format');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function buildAgentToolClass(source) {
|
|
75
|
-
const assignment = extractBetween(source, 'AgentTool = class {', '\n\t};\n\tUSER_INTERRUPTED_SUBAGENT_MESSAGE');
|
|
76
|
-
const classSource = `${assignment.slice('AgentTool = '.length)}\n\t}`;
|
|
77
|
-
return Function(
|
|
78
|
-
'AgentToolInputSchema',
|
|
79
|
-
'toInputJsonSchema',
|
|
80
|
-
'buildSubagentDescriptions',
|
|
81
|
-
'agent_default',
|
|
82
|
-
'agent_background_enabled_default',
|
|
83
|
-
'agent_background_disabled_default',
|
|
84
|
-
'resolveSubagentRunMode',
|
|
85
|
-
'ToolAccesses',
|
|
86
|
-
'matchesGlobRuleSubject',
|
|
87
|
-
'prepareStructuredResponseFormat',
|
|
88
|
-
'compileToolArgsValidator',
|
|
89
|
-
'BACKGROUND_AGENT_UNAVAILABLE',
|
|
90
|
-
'formatForegroundAgentSuccess',
|
|
91
|
-
'formatForegroundAgentFailure',
|
|
92
|
-
'USER_INTERRUPTED_SUBAGENT_MESSAGE',
|
|
93
|
-
'launchErrorMessage',
|
|
94
|
-
`"use strict"; return (${classSource});`,
|
|
95
|
-
)(
|
|
96
|
-
{},
|
|
97
|
-
() => ({}),
|
|
98
|
-
() => '',
|
|
99
|
-
'agent',
|
|
100
|
-
'background enabled',
|
|
101
|
-
'background disabled',
|
|
102
|
-
({ allowBackground }) => ({ runInBackground: allowBackground === true }),
|
|
103
|
-
{ none: () => ({}) },
|
|
104
|
-
() => true,
|
|
105
|
-
require('../bin/structured-subagent-output.cjs').prepareStructuredResponseFormat,
|
|
106
|
-
() => () => true,
|
|
107
|
-
'background unavailable',
|
|
108
|
-
(handle, result) => [
|
|
109
|
-
`agent_id: ${handle.agentId}`,
|
|
110
|
-
`actual_subagent_type: ${handle.profileName}`,
|
|
111
|
-
'status: completed',
|
|
112
|
-
'',
|
|
113
|
-
'[summary]',
|
|
114
|
-
result,
|
|
115
|
-
].join('\n'),
|
|
116
|
-
() => 'failed',
|
|
117
|
-
'interrupted',
|
|
118
|
-
(error) => String(error),
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function exerciseBundledAgentTool(source) {
|
|
123
|
-
const AgentTool = buildAgentToolClass(source);
|
|
124
|
-
const backgroundManager = {
|
|
125
|
-
getTask: () => ({ status: 'completed' }),
|
|
126
|
-
readOutput: async () => 'free text',
|
|
127
|
-
};
|
|
128
|
-
const tool = new AgentTool({}, backgroundManager, {}, { allowBackground: true });
|
|
129
|
-
const unstructured = await tool.resolveExecution({ prompt: 'x', description: 'free' });
|
|
130
|
-
assert.equal(unstructured.display.background, true);
|
|
131
|
-
const structuredArgs = {
|
|
132
|
-
prompt: 'x',
|
|
133
|
-
description: 'structured',
|
|
134
|
-
response_format: { name: 'answer', schema: { type: 'object' } },
|
|
135
|
-
};
|
|
136
|
-
const structured = await tool.resolveExecution(structuredArgs);
|
|
137
|
-
assert.equal(structured.display.background, false);
|
|
138
|
-
|
|
139
|
-
const handle = { agentId: 'agent-1', profileName: 'coder' };
|
|
140
|
-
assert.equal(
|
|
141
|
-
(await tool.formatForegroundResult('task-1', handle, undefined)).output,
|
|
142
|
-
'agent_id: agent-1\nactual_subagent_type: coder\nstatus: completed\n\n[summary]\nfree text',
|
|
143
|
-
'free-form output must remain byte-compatible',
|
|
144
|
-
);
|
|
145
|
-
backgroundManager.readOutput = async () => '{"status":"completed"}';
|
|
146
|
-
assert.equal(
|
|
147
|
-
(await tool.formatForegroundResult('task-2', handle, {})).output,
|
|
148
|
-
'{"status":"completed"}',
|
|
149
|
-
'structured output must remain canonical JSON without a text wrapper',
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
let spawned = false;
|
|
153
|
-
tool.subagentHost = { spawn: async () => { spawned = true; throw new Error('must not spawn'); } };
|
|
154
|
-
const invalid = await tool.execution({
|
|
155
|
-
...structuredArgs,
|
|
156
|
-
response_format: { name: 'answer', schema: { type: 'array' } },
|
|
157
|
-
}, { toolCallId: 'call-1', signal: new AbortController().signal });
|
|
158
|
-
assert.equal(invalid.isError, true);
|
|
159
|
-
assert.match(invalid.output, /Invalid response_format/u);
|
|
160
|
-
assert.equal(spawned, false, 'invalid schema must fail before child creation');
|
|
161
|
-
|
|
162
|
-
const background = await tool.execution({ ...structuredArgs, run_in_background: true }, {
|
|
163
|
-
toolCallId: 'call-2',
|
|
164
|
-
signal: new AbortController().signal,
|
|
165
|
-
});
|
|
166
|
-
assert.equal(background.isError, true);
|
|
167
|
-
assert.match(background.output, /foreground Agent runs only/u);
|
|
168
|
-
assert.equal(spawned, false);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function exerciseBundledRepairFlow(source) {
|
|
172
|
-
const functionSource = extractBetween(
|
|
173
|
-
source,
|
|
174
|
-
'async function completeStructuredSubagentResult(',
|
|
175
|
-
'function shouldSuppressQueuedAttemptFailureEvent(',
|
|
176
|
-
);
|
|
177
|
-
const runtime = require('../bin/structured-subagent-output.cjs');
|
|
178
|
-
let currentText = '';
|
|
179
|
-
let completionCalls = 0;
|
|
180
|
-
const completeStructuredSubagentResult = Function(
|
|
181
|
-
'validateStructuredSubagentOutput',
|
|
182
|
-
'buildStructuredResponseRepairPrompt',
|
|
183
|
-
'SUBAGENT_PROMPT_ORIGIN',
|
|
184
|
-
'completeChildTurnWithMaxTokensHandoff',
|
|
185
|
-
'lastAssistantText',
|
|
186
|
-
'STRUCTURED_SUBAGENT_OUTPUT_ERROR',
|
|
187
|
-
`"use strict"; ${functionSource}; return completeStructuredSubagentResult;`,
|
|
188
|
-
)(
|
|
189
|
-
runtime.validateStructuredSubagentOutput,
|
|
190
|
-
runtime.buildStructuredResponseRepairPrompt,
|
|
191
|
-
{ kind: 'system_trigger', name: 'subagent' },
|
|
192
|
-
async () => { completionCalls += 1; },
|
|
193
|
-
() => currentText,
|
|
194
|
-
runtime.STRUCTURED_SUBAGENT_OUTPUT_ERROR,
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
const prepared = runtime.prepareStructuredResponseFormat({
|
|
198
|
-
name: 'answer',
|
|
199
|
-
schema: {
|
|
200
|
-
type: 'object',
|
|
201
|
-
properties: { value: { type: 'string' } },
|
|
202
|
-
required: ['value'],
|
|
203
|
-
additionalProperties: false,
|
|
204
|
-
},
|
|
205
|
-
}, () => {
|
|
206
|
-
const validator = (value) => {
|
|
207
|
-
const ok = value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
208
|
-
&& typeof value.value === 'string' && Object.keys(value).length === 1;
|
|
209
|
-
validator.errors = ok ? [] : [{ keyword: 'required', instancePath: '', params: { missingProperty: 'value' } }];
|
|
210
|
-
return ok;
|
|
211
|
-
};
|
|
212
|
-
return validator;
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const prompts = [];
|
|
216
|
-
const child = { turn: { prompt: (parts) => {
|
|
217
|
-
prompts.push(parts[0].text);
|
|
218
|
-
currentText = '{"value":"repaired"}';
|
|
219
|
-
return 'turn-2';
|
|
220
|
-
} } };
|
|
221
|
-
currentText = '{"wrong":true}';
|
|
222
|
-
const repaired = await completeStructuredSubagentResult(
|
|
223
|
-
child,
|
|
224
|
-
'agent-2',
|
|
225
|
-
'coder',
|
|
226
|
-
prepared,
|
|
227
|
-
new AbortController().signal,
|
|
228
|
-
);
|
|
229
|
-
assert.deepEqual(repaired, { value: 'repaired' });
|
|
230
|
-
assert.equal(prompts.length, 1);
|
|
231
|
-
assert.equal(completionCalls, 1);
|
|
232
|
-
|
|
233
|
-
currentText = '{"wrong":true}';
|
|
234
|
-
child.turn.prompt = (parts) => {
|
|
235
|
-
prompts.push(parts[0].text);
|
|
236
|
-
currentText = '{"still_wrong":true}';
|
|
237
|
-
return 'turn-3';
|
|
238
|
-
};
|
|
239
|
-
await assert.rejects(
|
|
240
|
-
completeStructuredSubagentResult(child, 'agent-3', 'coder', prepared, new AbortController().signal),
|
|
241
|
-
/Structured subagent output invalid after one repair attempt[\s\S]*Resume with Agent/u,
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
const controller = new AbortController();
|
|
245
|
-
controller.abort(new Error('cancelled'));
|
|
246
|
-
currentText = '{"wrong":true}';
|
|
247
|
-
const promptCountBeforeCancel = prompts.length;
|
|
248
|
-
await assert.rejects(
|
|
249
|
-
completeStructuredSubagentResult(child, 'agent-4', 'coder', prepared, controller.signal),
|
|
250
|
-
/cancelled/u,
|
|
251
|
-
);
|
|
252
|
-
assert.equal(prompts.length, promptCountBeforeCancel, 'cancellation must prevent the repair turn');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function exerciseBundledStructuredFailure(source) {
|
|
256
|
-
const functionSource = extractBetween(
|
|
257
|
-
source,
|
|
258
|
-
'function formatForegroundAgentFailure(',
|
|
259
|
-
'function launchErrorMessage(',
|
|
260
|
-
);
|
|
261
|
-
const runtime = require('../bin/structured-subagent-output.cjs');
|
|
262
|
-
const formatForegroundAgentFailure = Function(
|
|
263
|
-
'SUBAGENT_MAX_TOKENS_ERROR',
|
|
264
|
-
'STRUCTURED_SUBAGENT_OUTPUT_ERROR',
|
|
265
|
-
`"use strict"; ${functionSource}; return formatForegroundAgentFailure;`,
|
|
266
|
-
)('SUBAGENT_MAX_TOKENS', runtime.STRUCTURED_SUBAGENT_OUTPUT_ERROR);
|
|
267
|
-
const output = formatForegroundAgentFailure(
|
|
268
|
-
{ agentId: 'agent-failed', profileName: 'coder' },
|
|
269
|
-
`${runtime.STRUCTURED_SUBAGENT_OUTPUT_ERROR}: missing value`,
|
|
270
|
-
false,
|
|
271
|
-
);
|
|
272
|
-
assert.match(output, /agent_id: agent-failed/u);
|
|
273
|
-
assert.match(output, /actual_subagent_type: coder/u);
|
|
274
|
-
assert.match(output, /missing value/u);
|
|
275
|
-
assert.match(output, /resume_hint: Continue with Agent\(resume="agent-failed"/u);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
async function main() {
|
|
279
|
-
const suite = spawnSync(process.execPath, ['--test', join(__dirname, 'structured-subagent-output.test.cjs')], {
|
|
280
|
-
stdio: 'inherit',
|
|
281
|
-
});
|
|
282
|
-
assert.equal(suite.status, 0, 'structured subagent helper tests must pass');
|
|
283
|
-
const source = readFileSync(bundlePath, 'utf8');
|
|
284
|
-
assertBundleContract(source);
|
|
285
|
-
await exerciseBundledAgentTool(source);
|
|
286
|
-
await exerciseBundledRepairFlow(source);
|
|
287
|
-
exerciseBundledStructuredFailure(source);
|
|
288
|
-
|
|
289
|
-
assert.throws(
|
|
290
|
-
() => assertBundleContract(source.replace(
|
|
291
|
-
'prepareStructuredResponseFormat(args.response_format, compileToolArgsValidator)',
|
|
292
|
-
'prepareStructuredResponseFormat(args.response_format, () => () => true)',
|
|
293
|
-
)),
|
|
294
|
-
/reuse BLUN AJV/u,
|
|
295
|
-
);
|
|
296
|
-
assert.throws(
|
|
297
|
-
() => assertBundleContract(source.replace(
|
|
298
|
-
'const runInBackground = responseFormat === void 0 ? resolvedRunMode.runInBackground : false',
|
|
299
|
-
'const runInBackground = resolvedRunMode.runInBackground',
|
|
300
|
-
)),
|
|
301
|
-
/foreground run/u,
|
|
302
|
-
);
|
|
303
|
-
assert.throws(
|
|
304
|
-
() => {
|
|
305
|
-
const region = extractBetween(
|
|
306
|
-
source,
|
|
307
|
-
'async function completeStructuredSubagentResult(',
|
|
308
|
-
'function shouldSuppressQueuedAttemptFailureEvent(',
|
|
309
|
-
);
|
|
310
|
-
const mutatedRegion = region.replace(
|
|
311
|
-
/\t\tsignal\.throwIfAborted\(\);\r?\n(?=\t\tif \(child\.turn\.prompt)/u,
|
|
312
|
-
'',
|
|
313
|
-
);
|
|
314
|
-
assertBundleContract(source.replace(region, mutatedRegion));
|
|
315
|
-
},
|
|
316
|
-
/cancellation/u,
|
|
317
|
-
);
|
|
318
|
-
assert.throws(
|
|
319
|
-
() => assertBundleContract(source.replace(
|
|
320
|
-
'responseFormat === void 0 ? formatForegroundAgentSuccess(handle, output) : output',
|
|
321
|
-
'formatForegroundAgentSuccess(handle, output)',
|
|
322
|
-
)),
|
|
323
|
-
/free-form text/u,
|
|
324
|
-
);
|
|
325
|
-
process.stdout.write('structured subagent output regression: ok\n');
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
main().catch((error) => {
|
|
329
|
-
console.error(error.stack || error);
|
|
330
|
-
process.exitCode = 1;
|
|
331
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
|
|
8
|
-
const bundle = fs.readFileSync(path.join(__dirname, '..', 'blun.mjs'), 'utf8');
|
|
9
|
-
|
|
10
|
-
function between(start, end) {
|
|
11
|
-
const startIndex = bundle.indexOf(start);
|
|
12
|
-
assert.notEqual(startIndex, -1, `missing bundle marker: ${start}`);
|
|
13
|
-
const endIndex = bundle.indexOf(end, startIndex + start.length);
|
|
14
|
-
assert.notEqual(endIndex, -1, `missing bundle marker: ${end}`);
|
|
15
|
-
return bundle.slice(startIndex, endIndex);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function method(name, nextName) {
|
|
19
|
-
const source = between(`\n\t${name}(`, `\n\t${nextName}(`).trim();
|
|
20
|
-
return Function(`"use strict"; return ({${source}}).${name};`)();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function main() {
|
|
24
|
-
const scheduleBridgeRestart = method('scheduleBridgeRestart', 'ensureBridgeRunning');
|
|
25
|
-
const host = {
|
|
26
|
-
stopped: false,
|
|
27
|
-
activeOwner: true,
|
|
28
|
-
bridgeRestartTimer: undefined,
|
|
29
|
-
bridgeRestartDelayMs: 5,
|
|
30
|
-
ownsChannel() { return true; },
|
|
31
|
-
starts: 0,
|
|
32
|
-
ensureBridgeRunning() { this.starts += 1; },
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
scheduleBridgeRestart.call(host);
|
|
36
|
-
const firstTimer = host.bridgeRestartTimer;
|
|
37
|
-
assert.notEqual(firstTimer, undefined);
|
|
38
|
-
scheduleBridgeRestart.call(host);
|
|
39
|
-
assert.equal(host.bridgeRestartTimer, firstTimer, 'restart scheduling must be deduplicated');
|
|
40
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
41
|
-
assert.equal(host.starts, 1, 'a dead owned bridge must be restarted once');
|
|
42
|
-
assert.equal(host.bridgeRestartTimer, undefined);
|
|
43
|
-
|
|
44
|
-
host.stopped = true;
|
|
45
|
-
scheduleBridgeRestart.call(host);
|
|
46
|
-
assert.equal(host.bridgeRestartTimer, undefined, 'stopped controllers must not restart bridges');
|
|
47
|
-
|
|
48
|
-
const ensureBridgeRunning = between('\n\tensureBridgeRunning() {', '\n\tresolveBridgeEntry() {');
|
|
49
|
-
assert.match(ensureBridgeRunning, /child\.once\("exit",[\s\S]*this\.scheduleBridgeRestart\(\)/);
|
|
50
|
-
assert.match(ensureBridgeRunning, /child\.once\("error",[\s\S]*this\.scheduleBridgeRestart\(\)/);
|
|
51
|
-
const beginStop = between('\n\tbeginStop() {', '\n\tasync stopOwnedBridge(');
|
|
52
|
-
assert.match(beginStop, /clearTimeout\(this\.bridgeRestartTimer\)/);
|
|
53
|
-
|
|
54
|
-
process.stdout.write('telegram-bridge-watchdog PASS\n');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
main().catch((error) => {
|
|
58
|
-
console.error(error);
|
|
59
|
-
process.exitCode = 1;
|
|
60
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
const packageRoot = path.resolve(process.env.BLUN_PACKAGE_ROOT || path.join(__dirname, '..'));
|
|
8
|
-
const {
|
|
9
|
-
createDirectReplyTurnStop,
|
|
10
|
-
directMessageAllowsWorkContinuation,
|
|
11
|
-
directMessageStartsOrResumesWork,
|
|
12
|
-
directTelegramPayloadText,
|
|
13
|
-
} = require(path.join(packageRoot, 'bin', 'telegram-direct-focus-policy.cjs'));
|
|
14
|
-
|
|
15
|
-
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
16
|
-
const direct = (payload) => [
|
|
17
|
-
'A private Telegram DM has priority over background, group, and loop work.',
|
|
18
|
-
'<channel priority="direct" source="telegram" chat_id="1605241602">',
|
|
19
|
-
payload,
|
|
20
|
-
'</channel>',
|
|
21
|
-
].join('\n');
|
|
22
|
-
|
|
23
|
-
const status = direct('Wie weit bist du?');
|
|
24
|
-
assert.equal(directTelegramPayloadText(status), 'Wie weit bist du?');
|
|
25
|
-
assert.equal(directMessageAllowsWorkContinuation(status), true);
|
|
26
|
-
assert.equal(directMessageStartsOrResumesWork(status), false);
|
|
27
|
-
const statusStop = createDirectReplyTurnStop(status);
|
|
28
|
-
assert.equal(statusStop.noteToolResult('mcp__plugin-telegram_telegram__reply', false), true);
|
|
29
|
-
assert.equal(statusStop.shouldStop(), true, 'standalone DM chat should stop after its reply');
|
|
30
|
-
|
|
31
|
-
for (const payload of [
|
|
32
|
-
'Auftrag: Lies die SPEC und baue die Datei.',
|
|
33
|
-
'Feuer frei',
|
|
34
|
-
'Bitte weiterarbeiten und danach melden.',
|
|
35
|
-
]) {
|
|
36
|
-
const input = direct(payload);
|
|
37
|
-
assert.equal(directMessageAllowsWorkContinuation(input), true, payload);
|
|
38
|
-
assert.equal(directMessageStartsOrResumesWork(input), true, payload);
|
|
39
|
-
const stop = createDirectReplyTurnStop(input);
|
|
40
|
-
stop.noteToolResult('mcp__plugin-telegram_telegram__reply', false);
|
|
41
|
-
assert.equal(stop.shouldStop(), false, `work DM must continue after reply: ${payload}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const pause = direct('Bitte stop, warte auf mein Zeichen.');
|
|
45
|
-
assert.equal(directMessageAllowsWorkContinuation(pause), false);
|
|
46
|
-
assert.equal(directMessageStartsOrResumesWork(pause), false);
|
|
47
|
-
|
|
48
|
-
assert.match(bundle, /let directSteerContinuationPending = false;/u);
|
|
49
|
-
assert.match(bundle, /pendingSteers\.some\(\(steer\) => directMessageAllowsWorkContinuation/u);
|
|
50
|
-
assert.match(bundle, /name: "telegram_direct_work_resume"/u);
|
|
51
|
-
assert.match(bundle, /perform the next unfinished action now\./u);
|
|
52
|
-
|
|
53
|
-
console.log('telegram-direct-work-resume-regression PASS');
|