@platless/axom-cli 0.3.13 → 0.5.9
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/dist/axom-patch/cli/README.md +30 -8
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
- package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
- package/dist/axom-patch/cli/qoder-steering.json +15 -15
- package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
- package/dist/axom-patch/cli/runtime.json +34 -8
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +33 -6
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2163 -611
- package/dist/workstation/acp/acpClient.js +60 -497
- package/dist/workstation/acp/adoption.js +10 -396
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +76 -160
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +20 -3
- package/dist/workstation/acp/permission.js +42 -10
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +1 -1
- package/dist/workstation/acp/registry/catalog.js +154 -32
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1324 -474
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +84 -4
- package/dist/workstation/daemon.js +288 -7
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/{fileOps.js → files/text.js} +117 -38
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +48 -4
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
- package/dist/workstation/qoderAdapter/history.js +1 -3
- package/dist/workstation/qoderAdapter/main.js +4 -2
- package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +1 -1
- package/dist/workstation/qoderAdapter/subwork.js +15 -7
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/package.json +10 -7
- package/dist/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
export const MAX_TASKS = 512;
|
|
2
|
+
export const MAX_RESULT = 128 * 1024;
|
|
3
|
+
export const MAX_RESULTS = 4 * 1024 * 1024;
|
|
4
|
+
/** Child transcript rows remembered per background Agent (`BackgroundProcess`' `seen` bound). */
|
|
5
|
+
export const CHILD_ROW_LIMIT = 4096;
|
|
6
|
+
const TERMINALS = new Set(['completed', 'failed', 'cancelled']);
|
|
7
|
+
export function object(value) {
|
|
8
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
9
|
+
}
|
|
10
|
+
export function address(value) {
|
|
11
|
+
return typeof value === 'string' && /^[a-zA-Z0-9_-]{1,160}$/.test(value) ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
function own(record, key) {
|
|
14
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
15
|
+
}
|
|
16
|
+
function put(record, key, value) {
|
|
17
|
+
Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
|
|
18
|
+
}
|
|
19
|
+
/** UTF-8 length of a string, as `Buffer.byteLength` counts it (a lone surrogate is 3 bytes). */
|
|
20
|
+
export function utf8Length(text) {
|
|
21
|
+
let bytes = 0;
|
|
22
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
23
|
+
const code = text.charCodeAt(index);
|
|
24
|
+
if (code < 0x80)
|
|
25
|
+
bytes += 1;
|
|
26
|
+
else if (code < 0x800)
|
|
27
|
+
bytes += 2;
|
|
28
|
+
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length
|
|
29
|
+
&& text.charCodeAt(index + 1) >= 0xdc00 && text.charCodeAt(index + 1) <= 0xdfff) {
|
|
30
|
+
bytes += 4;
|
|
31
|
+
index += 1;
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
bytes += 3;
|
|
35
|
+
}
|
|
36
|
+
return bytes;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The facts in one root transcript row — `BackgroundTerminalRepair.consume` up to the point where it
|
|
40
|
+
* consulted its own memory (that half is {@link TerminalRepairFold.apply}).
|
|
41
|
+
*/
|
|
42
|
+
export function rootRowFacts(row, context) {
|
|
43
|
+
if (row.sessionId !== context.nativeSessionId || row.cwd !== context.cwd || row.isSidechain !== false)
|
|
44
|
+
return [];
|
|
45
|
+
const content = object(row.message).content;
|
|
46
|
+
if (!Array.isArray(content))
|
|
47
|
+
return [];
|
|
48
|
+
const facts = [];
|
|
49
|
+
// resume deliberately sends no historical ACP openings. Recover only certified root Agent
|
|
50
|
+
// background inputs; no opening is emitted and the existing exact-ID notification rules apply.
|
|
51
|
+
if (context.restoreCalls && row.type === 'assistant') {
|
|
52
|
+
for (const block of content.map(object)) {
|
|
53
|
+
const input = object(block.input), id = address(block.id);
|
|
54
|
+
if (block.type === 'tool_use' && block.name === 'Agent' && id && input.run_in_background === true &&
|
|
55
|
+
typeof input.prompt === 'string' && (input.subagent_type == null || typeof input.subagent_type === 'string')) {
|
|
56
|
+
facts.push({ kind: 'call', toolUseId: id, input });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return facts;
|
|
60
|
+
}
|
|
61
|
+
if (row.type !== 'user')
|
|
62
|
+
return facts;
|
|
63
|
+
const output = object(row.toolUseResult);
|
|
64
|
+
if (output.isAsync === true && output.status === 'async_launched') {
|
|
65
|
+
const results = content.map(object).filter(c => c.type === 'tool_result');
|
|
66
|
+
const id = results.length === 1 ? address(results[0].tool_use_id) : undefined;
|
|
67
|
+
const agent = address(output.agentId);
|
|
68
|
+
if (id)
|
|
69
|
+
facts.push({ kind: 'launch', toolUseId: id, ...(agent ? { agentId: agent } : {}) });
|
|
70
|
+
return facts;
|
|
71
|
+
}
|
|
72
|
+
if (row.isMeta !== true || object(row.origin).kind !== 'task-notification')
|
|
73
|
+
return facts;
|
|
74
|
+
for (const block of content.map(object)) {
|
|
75
|
+
if (block.type !== 'text' || typeof block.text !== 'string' ||
|
|
76
|
+
!block.text.startsWith('[SYSTEM NOTIFICATION - NOT USER INPUT]\n'))
|
|
77
|
+
continue;
|
|
78
|
+
// Match the authoritative envelope before result text; tags inside a result cannot change IDs.
|
|
79
|
+
const match = block.text.match(/<task-notification>\n<task-id>([a-zA-Z0-9_-]{1,160})<\/task-id>\n<tool-use-id>([a-zA-Z0-9_-]{1,160})<\/tool-use-id>\n(?:<output-file>[^\n]*<\/output-file>\n)?<status>(completed|failed|cancelled)<\/status>\n<summary>[^\n]*<\/summary>\n<result>([\s\S]*)<\/result>\n(?:<usage>[^\n]*<\/usage>\n)?<\/task-notification>\s*$/);
|
|
80
|
+
if (match && utf8Length(match[4]) <= MAX_RESULT) {
|
|
81
|
+
facts.push({ kind: 'terminal', toolUseId: match[2], taskId: match[1], state: match[3], result: match[4] });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return facts;
|
|
85
|
+
}
|
|
86
|
+
export function newTerminalRepairData(options) {
|
|
87
|
+
return {
|
|
88
|
+
nativeSessionId: options.nativeSessionId, restoreCalls: options.restoreCalls === true, includeProcess: options.includeProcess === true,
|
|
89
|
+
calls: {}, callCount: 0, launches: [], terminals: [], delivered: {}, pendingProcesses: {}, resultBytes: 0, childRows: {}, polledProcesses: {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export class TerminalRepairFold {
|
|
93
|
+
data;
|
|
94
|
+
constructor(data) {
|
|
95
|
+
this.data = data;
|
|
96
|
+
}
|
|
97
|
+
call(id) { return own(this.data.calls, id); }
|
|
98
|
+
agentOf(id) { return this.data.launches.find(([toolUseId]) => toolUseId === id)?.[1]; }
|
|
99
|
+
/** Every ACP event the session routes or emits (live or load history): learns the certified Agent calls. */
|
|
100
|
+
observe(event) {
|
|
101
|
+
if ((event.acpSessionId && event.acpSessionId !== this.data.nativeSessionId) ||
|
|
102
|
+
!['tool_call', 'tool_call_update'].includes(event.eventName))
|
|
103
|
+
return;
|
|
104
|
+
const payload = event.payload;
|
|
105
|
+
const id = address(payload.toolCallId);
|
|
106
|
+
if (!id)
|
|
107
|
+
return;
|
|
108
|
+
let call = this.call(id);
|
|
109
|
+
const input = object(payload.rawInput);
|
|
110
|
+
if (!call && input.run_in_background === true && typeof input.prompt === 'string' &&
|
|
111
|
+
(input.subagent_type == null || typeof input.subagent_type === 'string') && payload.kind === 'think' &&
|
|
112
|
+
object(object(payload._meta).qoder).toolName === 'Agent' && this.data.callCount < MAX_TASKS) {
|
|
113
|
+
const meta = object(object(payload._meta).qoder);
|
|
114
|
+
call = { input, invocationComplete: false, nativeTerminal: false,
|
|
115
|
+
process: this.data.includeProcess && meta.version === 1 && meta.source === 'native-acp-patch' };
|
|
116
|
+
put(this.data.calls, id, call);
|
|
117
|
+
this.data.callCount += 1;
|
|
118
|
+
}
|
|
119
|
+
if (!call)
|
|
120
|
+
return;
|
|
121
|
+
if (payload.status === 'completed')
|
|
122
|
+
call.invocationComplete = true;
|
|
123
|
+
const output = object(payload.rawOutput);
|
|
124
|
+
if (output.kind === 'agent-result' && (TERMINALS.has(String(output.state)) || output.state === 'error')) {
|
|
125
|
+
call.nativeTerminal = true; // Upstream's structured evidence always wins.
|
|
126
|
+
// Never published now: its input, child rows and status are dead (checkpointed state).
|
|
127
|
+
this.forget(id, call);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** A known call has no result yet: the reader keeps polling the root transcript. */
|
|
131
|
+
active() {
|
|
132
|
+
return Object.entries(this.data.calls).some(([id, call]) => !Object.hasOwn(this.data.delivered, id) && !call.nativeTerminal);
|
|
133
|
+
}
|
|
134
|
+
/** This call's child transcript is still followed. */
|
|
135
|
+
wantsProcess(toolUseId) {
|
|
136
|
+
const call = this.call(toolUseId);
|
|
137
|
+
return call !== undefined && call.process && !Object.hasOwn(this.data.delivered, toolUseId) && !call.nativeTerminal;
|
|
138
|
+
}
|
|
139
|
+
/** Fold one fact; returns the ACP events to emit now (a child row's, or the results a poll's end publishes). */
|
|
140
|
+
apply(fact) {
|
|
141
|
+
const data = this.data;
|
|
142
|
+
switch (fact.kind) {
|
|
143
|
+
case 'call':
|
|
144
|
+
if (data.restoreCalls && !this.call(fact.toolUseId) && data.callCount < MAX_TASKS) {
|
|
145
|
+
put(data.calls, fact.toolUseId, { input: fact.input, invocationComplete: false, nativeTerminal: false, process: false });
|
|
146
|
+
data.callCount += 1;
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
case 'launch': {
|
|
150
|
+
const call = this.call(fact.toolUseId);
|
|
151
|
+
if (data.restoreCalls && call)
|
|
152
|
+
call.invocationComplete = true;
|
|
153
|
+
if (fact.agentId && data.launches.length < MAX_TASKS && this.agentOf(fact.toolUseId) === undefined) {
|
|
154
|
+
data.launches.push([fact.toolUseId, fact.agentId]);
|
|
155
|
+
}
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
case 'terminal': {
|
|
159
|
+
const bytes = utf8Length(fact.result);
|
|
160
|
+
if (bytes <= MAX_RESULT && data.resultBytes + bytes <= MAX_RESULTS && data.terminals.length < MAX_TASKS &&
|
|
161
|
+
!data.terminals.some((terminal) => terminal.toolUseId === fact.toolUseId)) {
|
|
162
|
+
data.resultBytes += bytes;
|
|
163
|
+
data.terminals.push({ toolUseId: fact.toolUseId, taskId: fact.taskId, state: fact.state, result: fact.result });
|
|
164
|
+
}
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
case 'child': {
|
|
168
|
+
// Only a transcript the repair still follows (the reader asked the same question before reading).
|
|
169
|
+
if (!this.wantsProcess(fact.toolUseId) || this.agentOf(fact.toolUseId) === undefined)
|
|
170
|
+
return [];
|
|
171
|
+
let seen = own(data.childRows, fact.toolUseId);
|
|
172
|
+
if (!seen)
|
|
173
|
+
put(data.childRows, fact.toolUseId, seen = []);
|
|
174
|
+
if (seen.includes(fact.uuid) || seen.length >= CHILD_ROW_LIMIT)
|
|
175
|
+
return [];
|
|
176
|
+
seen.push(fact.uuid);
|
|
177
|
+
return [...fact.events];
|
|
178
|
+
}
|
|
179
|
+
case 'child_status':
|
|
180
|
+
if (!this.wantsProcess(fact.toolUseId) || this.agentOf(fact.toolUseId) === undefined)
|
|
181
|
+
return [];
|
|
182
|
+
if (fact.caughtUp)
|
|
183
|
+
delete data.pendingProcesses[fact.toolUseId];
|
|
184
|
+
else
|
|
185
|
+
put(data.pendingProcesses, fact.toolUseId, true);
|
|
186
|
+
put(data.polledProcesses, fact.toolUseId, true);
|
|
187
|
+
return [];
|
|
188
|
+
case 'poll_end':
|
|
189
|
+
return this.publish();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
publish() {
|
|
193
|
+
const data = this.data;
|
|
194
|
+
const events = [];
|
|
195
|
+
for (const terminal of data.terminals) {
|
|
196
|
+
const id = terminal.toolUseId;
|
|
197
|
+
const call = this.call(id), agentId = this.agentOf(id);
|
|
198
|
+
if (call?.nativeTerminal)
|
|
199
|
+
terminal.result = '';
|
|
200
|
+
if (Object.hasOwn(data.delivered, id) || Object.hasOwn(data.pendingProcesses, id) || !call?.invocationComplete ||
|
|
201
|
+
call.nativeTerminal || !agentId || (call.process && !Object.hasOwn(data.polledProcesses, id)))
|
|
202
|
+
continue;
|
|
203
|
+
put(data.delivered, id, true);
|
|
204
|
+
events.push({ eventName: 'tool_call_update', payload: {
|
|
205
|
+
name: 'Agent', kind: 'think', toolCallId: id, rawInput: call.input, status: 'completed',
|
|
206
|
+
rawOutput: { kind: 'agent-result', state: terminal.state, agentId, result: terminal.result },
|
|
207
|
+
content: [{ type: 'content', content: { type: 'text', text: terminal.result } }],
|
|
208
|
+
_meta: { qoder: { toolName: 'Agent' }, axomNativeTerminalRepair: {
|
|
209
|
+
patch: 'qoder-background-terminal', nativeSessionId: data.nativeSessionId,
|
|
210
|
+
taskId: terminal.taskId, source: 'native-task-notification',
|
|
211
|
+
} },
|
|
212
|
+
} });
|
|
213
|
+
// Delivered results are never published again; their text need not stay in a checkpoint.
|
|
214
|
+
terminal.result = '';
|
|
215
|
+
this.forget(id, call);
|
|
216
|
+
}
|
|
217
|
+
return events;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A delivered or natively finished call is never published and its child never followed again
|
|
221
|
+
* (`wantsProcess` is false for good), so what only those two read goes: the input (read by
|
|
222
|
+
* `publish` alone), the child rows' uuids and status. The call entry itself stays — `observe` must
|
|
223
|
+
* not register the same id afresh — and so do the launch and result entries, whose presence is what
|
|
224
|
+
* keeps a restarted reader's repeats from being taken as new.
|
|
225
|
+
*/
|
|
226
|
+
forget(id, call) {
|
|
227
|
+
call.input = {};
|
|
228
|
+
delete this.data.childRows[id];
|
|
229
|
+
delete this.data.pendingProcesses[id];
|
|
230
|
+
delete this.data.polledProcesses[id];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// ---- notes ------------------------------------------------------------------------------------
|
|
234
|
+
/**
|
|
235
|
+
* The note kinds (spec §5.4: "Qoder 侧文件推导出的事件" enter the log as notes):
|
|
236
|
+
* - `qoder_terminal_repair{nativeSessionId, restoreCalls?}` — the daemon enabled the certified repair
|
|
237
|
+
* for this session (where today's path calls `enableBackgroundTerminalRepair`).
|
|
238
|
+
* - `qoder_side{fact}`, or for a fact too large for one note, consecutive
|
|
239
|
+
* `qoder_side{chunk: {index, count, text}}` notes whose `text`s concatenate to the fact's JSON.
|
|
240
|
+
*/
|
|
241
|
+
export const REPAIR_NOTE = 'qoder_terminal_repair';
|
|
242
|
+
export const SIDE_NOTE = 'qoder_side';
|
|
243
|
+
/** Equal to the processor's `NOTE_MAX_BYTES` (asserted in a test; this module does not import the processor). */
|
|
244
|
+
export const SIDE_NOTE_MAX_BYTES = 64 * 1024;
|
|
245
|
+
/**
|
|
246
|
+
* A fact larger than this (serialized) is not recorded (the reader counts it). A child row's line is
|
|
247
|
+
* ≤ 2 MiB and re-serializes no larger, but its text can appear twice (`rawOutput` and `content`), so a
|
|
248
|
+
* fact stays ≤ ~4 MiB; 8 MiB keeps every fact the live driver would have emitted. Its chunk buffer can be
|
|
249
|
+
* that large while a chunked fact is half-read: T19 does not cut a checkpoint then (`sideChunk !== null`).
|
|
250
|
+
*/
|
|
251
|
+
export const MAX_SIDE_FACT_BYTES = 8 * 1024 * 1024;
|
|
252
|
+
/** Enough for a {@link MAX_SIDE_FACT_BYTES} fact even when chunking doubles it (escaped quotes). */
|
|
253
|
+
export const MAX_SIDE_CHUNKS = 512;
|
|
254
|
+
/** Bytes `JSON.stringify` spends on one code unit (or pair) inside a string literal. */
|
|
255
|
+
function escapedBytes(text, index) {
|
|
256
|
+
const code = text.charCodeAt(index);
|
|
257
|
+
if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x0c || code === 0x0a || code === 0x0d || code === 0x09)
|
|
258
|
+
return { bytes: 2, units: 1 };
|
|
259
|
+
if (code < 0x20)
|
|
260
|
+
return { bytes: 6, units: 1 };
|
|
261
|
+
if (code < 0x80)
|
|
262
|
+
return { bytes: 1, units: 1 };
|
|
263
|
+
if (code < 0x800)
|
|
264
|
+
return { bytes: 2, units: 1 };
|
|
265
|
+
if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length) {
|
|
266
|
+
const next = text.charCodeAt(index + 1);
|
|
267
|
+
if (next >= 0xdc00 && next <= 0xdfff)
|
|
268
|
+
return { bytes: 4, units: 2 };
|
|
269
|
+
}
|
|
270
|
+
if (code >= 0xd800 && code <= 0xdfff)
|
|
271
|
+
return { bytes: 6, units: 1 };
|
|
272
|
+
return { bytes: 3, units: 1 };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The notes that record one fact: one `{fact}` note when it fits {@link SIDE_NOTE_MAX_BYTES}, otherwise
|
|
276
|
+
* consecutive `{chunk}` notes, each within the limit. `[]` for a fact over {@link MAX_SIDE_FACT_BYTES}.
|
|
277
|
+
*/
|
|
278
|
+
export function sideNotes(fact) {
|
|
279
|
+
const whole = { type: SIDE_NOTE, fact };
|
|
280
|
+
if (utf8Length(JSON.stringify(whole)) <= SIDE_NOTE_MAX_BYTES)
|
|
281
|
+
return [whole];
|
|
282
|
+
const json = JSON.stringify(fact);
|
|
283
|
+
if (utf8Length(json) > MAX_SIDE_FACT_BYTES)
|
|
284
|
+
return [];
|
|
285
|
+
const overhead = utf8Length(JSON.stringify({ type: SIDE_NOTE, chunk: { index: MAX_SIDE_CHUNKS, count: MAX_SIDE_CHUNKS, text: '' } }));
|
|
286
|
+
const budget = SIDE_NOTE_MAX_BYTES - overhead;
|
|
287
|
+
const pieces = [];
|
|
288
|
+
let start = 0, used = 0;
|
|
289
|
+
for (let index = 0; index < json.length;) {
|
|
290
|
+
const { bytes, units } = escapedBytes(json, index);
|
|
291
|
+
if (used + bytes > budget) {
|
|
292
|
+
pieces.push(json.slice(start, index));
|
|
293
|
+
start = index;
|
|
294
|
+
used = 0;
|
|
295
|
+
}
|
|
296
|
+
used += bytes;
|
|
297
|
+
index += units;
|
|
298
|
+
}
|
|
299
|
+
pieces.push(json.slice(start));
|
|
300
|
+
if (pieces.length > MAX_SIDE_CHUNKS)
|
|
301
|
+
return [];
|
|
302
|
+
return pieces.map((text, index) => ({ type: SIDE_NOTE, chunk: { index, count: pieces.length, text } }));
|
|
303
|
+
}
|
|
304
|
+
function isEvent(value) {
|
|
305
|
+
const event = object(value);
|
|
306
|
+
return value === event && typeof event.eventName === 'string' && object(event.payload) === event.payload
|
|
307
|
+
&& (event.acpSessionId === undefined || typeof event.acpSessionId === 'string');
|
|
308
|
+
}
|
|
309
|
+
/** A fact read back from a note, shape-checked (the log is the daemon's own, but a note is still data). */
|
|
310
|
+
export function parseSideFact(value) {
|
|
311
|
+
const fact = object(value);
|
|
312
|
+
if (value !== fact)
|
|
313
|
+
return undefined;
|
|
314
|
+
const id = address(fact.toolUseId);
|
|
315
|
+
switch (fact.kind) {
|
|
316
|
+
case 'call':
|
|
317
|
+
return id && object(fact.input) === fact.input ? { kind: 'call', toolUseId: id, input: fact.input } : undefined;
|
|
318
|
+
case 'launch':
|
|
319
|
+
if (!id || (fact.agentId !== undefined && address(fact.agentId) === undefined))
|
|
320
|
+
return undefined;
|
|
321
|
+
return { kind: 'launch', toolUseId: id, ...(fact.agentId !== undefined ? { agentId: fact.agentId } : {}) };
|
|
322
|
+
case 'terminal':
|
|
323
|
+
return id && address(fact.taskId) && TERMINALS.has(String(fact.state)) && typeof fact.result === 'string'
|
|
324
|
+
? { kind: 'terminal', toolUseId: id, taskId: fact.taskId, state: fact.state, result: fact.result }
|
|
325
|
+
: undefined;
|
|
326
|
+
case 'child':
|
|
327
|
+
return id && address(fact.uuid) && Array.isArray(fact.events) && fact.events.every(isEvent)
|
|
328
|
+
? { kind: 'child', toolUseId: id, uuid: fact.uuid, events: fact.events }
|
|
329
|
+
: undefined;
|
|
330
|
+
case 'child_status':
|
|
331
|
+
return id && typeof fact.caughtUp === 'boolean' ? { kind: 'child_status', toolUseId: id, caughtUp: fact.caughtUp } : undefined;
|
|
332
|
+
case 'poll_end':
|
|
333
|
+
return { kind: 'poll_end' };
|
|
334
|
+
default:
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Read one `qoder_side` note against the reassembly buffer. A `{fact}` note (or a chunk with index 0)
|
|
340
|
+
* abandons any half-read chunked fact — a reader that restarted re-reads from the start, so the
|
|
341
|
+
* fact comes again. `malformed` when the note could not be used (shape, order, or JSON).
|
|
342
|
+
*/
|
|
343
|
+
export function readSideNote(buffer, note) {
|
|
344
|
+
if (Object.hasOwn(note, 'fact')) {
|
|
345
|
+
const fact = parseSideFact(note.fact);
|
|
346
|
+
return fact ? { buffer: null, fact, malformed: false } : { buffer: null, malformed: true };
|
|
347
|
+
}
|
|
348
|
+
const chunk = object(note.chunk);
|
|
349
|
+
const { index, count, text } = chunk;
|
|
350
|
+
if (note.chunk !== chunk || !Number.isSafeInteger(index) || !Number.isSafeInteger(count) || typeof text !== 'string'
|
|
351
|
+
|| count < 2 || count > MAX_SIDE_CHUNKS || index < 0 || index >= count) {
|
|
352
|
+
return { buffer, malformed: true };
|
|
353
|
+
}
|
|
354
|
+
let next;
|
|
355
|
+
if (index === 0)
|
|
356
|
+
next = { count: count, next: 1, text };
|
|
357
|
+
else if (buffer && buffer.count === count && buffer.next === index)
|
|
358
|
+
next = { count: buffer.count, next: buffer.next + 1, text: buffer.text + text };
|
|
359
|
+
else
|
|
360
|
+
return { buffer: null, malformed: true };
|
|
361
|
+
if (next.next < next.count)
|
|
362
|
+
return { buffer: next, malformed: false };
|
|
363
|
+
let parsed;
|
|
364
|
+
try {
|
|
365
|
+
parsed = JSON.parse(next.text);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return { buffer: null, malformed: true };
|
|
369
|
+
}
|
|
370
|
+
const fact = parseSideFact(parsed);
|
|
371
|
+
return fact ? { buffer: null, fact, malformed: false } : { buffer: null, malformed: true };
|
|
372
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** A permission card whose tool call carries the SDK adapter's callback metadata. */
|
|
2
|
+
export function isSdkCallbackCard(toolCall) {
|
|
3
|
+
const meta = toolCall && typeof toolCall === 'object' && !Array.isArray(toolCall)
|
|
4
|
+
? toolCall._meta : undefined;
|
|
5
|
+
return !!meta && typeof meta === 'object' && !Array.isArray(meta) &&
|
|
6
|
+
Object.prototype.hasOwnProperty.call(meta, 'qoderSdk');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The callback snapshot's `connectionGeneration` on the processor path: one ACP session per adapter
|
|
10
|
+
* process, so it derives from the keeper's `spawnId` (spec §5.4), distinct from `hostGeneration`
|
|
11
|
+
* (= the `spawnId` itself).
|
|
12
|
+
*/
|
|
13
|
+
export function connectionGenerationOf(spawnId) {
|
|
14
|
+
return `session:${spawnId}`;
|
|
15
|
+
}
|
|
16
|
+
/** `AcpSession.callbackContextFor`, as a pure function. `undefined` = the card cannot carry a callback. */
|
|
17
|
+
export function permissionCallbackContext(input) {
|
|
18
|
+
const { rootSessionId, diagnostics, requestId, toolCall, pendingRpcRequestId } = input;
|
|
19
|
+
if (input.origin !== rootSessionId)
|
|
20
|
+
return;
|
|
21
|
+
const profile = diagnostics.profile;
|
|
22
|
+
const callbacks = diagnostics.callbacks;
|
|
23
|
+
if (!diagnostics.available || profile?.mode !== 'sdk-adapter' || profile.initialized !== true ||
|
|
24
|
+
profile.rootConfirmed !== true || profile.rootSessionId !== rootSessionId ||
|
|
25
|
+
!profile.runtimeGeneration || callbacks?.complete !== true ||
|
|
26
|
+
callbacks.rootSessionId !== rootSessionId ||
|
|
27
|
+
typeof requestId !== 'string' || pendingRpcRequestId === undefined ||
|
|
28
|
+
!toolCall || typeof toolCall !== 'object' || Array.isArray(toolCall))
|
|
29
|
+
return;
|
|
30
|
+
const call = toolCall;
|
|
31
|
+
const sdkMeta = call._meta && typeof call._meta === 'object' && !Array.isArray(call._meta)
|
|
32
|
+
? call._meta.qoderSdk : undefined;
|
|
33
|
+
if (!sdkMeta || typeof sdkMeta !== 'object' || Array.isArray(sdkMeta))
|
|
34
|
+
return;
|
|
35
|
+
const sdk = sdkMeta;
|
|
36
|
+
const origin = sdk.origin;
|
|
37
|
+
if (sdk.version !== 1 || sdk.blockingScope !== 'root' || sdk.callbackOnly !== true ||
|
|
38
|
+
typeof sdk.requestId !== 'string' || !/^[0-9a-f-]{36}$/i.test(sdk.requestId) ||
|
|
39
|
+
!origin || typeof origin !== 'object' || Array.isArray(origin))
|
|
40
|
+
return;
|
|
41
|
+
const source = origin;
|
|
42
|
+
const toolUseId = call.toolCallId;
|
|
43
|
+
const agentId = source.agentId;
|
|
44
|
+
if (source.rootSessionId !== rootSessionId || source.runtimeGeneration !== profile.runtimeGeneration ||
|
|
45
|
+
typeof toolUseId !== 'string' || source.toolUseId !== toolUseId ||
|
|
46
|
+
!toolUseId || utf8Length(toolUseId) > 256 ||
|
|
47
|
+
(agentId !== undefined && (typeof agentId !== 'string' || !agentId || utf8Length(agentId) > 256)))
|
|
48
|
+
return;
|
|
49
|
+
const pending = callbacks.pending.find((entry) => entry.requestId === requestId &&
|
|
50
|
+
entry.originNativeSessionId === rootSessionId);
|
|
51
|
+
if (!pending || callbacks.pending.length > 64 || pendingRpcRequestId !== pending.rpcRequestId)
|
|
52
|
+
return;
|
|
53
|
+
return { rootSessionId, hostGeneration: diagnostics.generation,
|
|
54
|
+
connectionGeneration: callbacks.connectionGeneration, requestId,
|
|
55
|
+
runtimeGeneration: profile.runtimeGeneration, toolUseId,
|
|
56
|
+
...(agentId ? { agentId } : {}), independent: false };
|
|
57
|
+
}
|
|
58
|
+
/** `Buffer.byteLength(text, 'utf8')` in plain JS (a lone surrogate is 3 bytes). */
|
|
59
|
+
function utf8Length(text) {
|
|
60
|
+
let bytes = 0;
|
|
61
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
62
|
+
const code = text.charCodeAt(index);
|
|
63
|
+
if (code < 0x80)
|
|
64
|
+
bytes += 1;
|
|
65
|
+
else if (code < 0x800)
|
|
66
|
+
bytes += 2;
|
|
67
|
+
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length
|
|
68
|
+
&& text.charCodeAt(index + 1) >= 0xdc00 && text.charCodeAt(index + 1) <= 0xdfff) {
|
|
69
|
+
bytes += 4;
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
bytes += 3;
|
|
74
|
+
}
|
|
75
|
+
return bytes;
|
|
76
|
+
}
|
|
@@ -4,27 +4,33 @@ export function certifiedRuntime(identity) {
|
|
|
4
4
|
function record(value) {
|
|
5
5
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
6
6
|
}
|
|
7
|
+
export function newForegroundOutputData() {
|
|
8
|
+
return { pending: {} };
|
|
9
|
+
}
|
|
7
10
|
export class ForegroundOutputBoundary {
|
|
8
11
|
nativeSessionId;
|
|
9
|
-
|
|
10
|
-
constructor(nativeSessionId) {
|
|
12
|
+
data;
|
|
13
|
+
constructor(nativeSessionId, data = newForegroundOutputData()) {
|
|
11
14
|
this.nativeSessionId = nativeSessionId;
|
|
15
|
+
this.data = data;
|
|
12
16
|
}
|
|
13
17
|
accept(event) {
|
|
14
18
|
if (event.acpSessionId && event.acpSessionId !== this.nativeSessionId)
|
|
15
19
|
return true;
|
|
16
|
-
const p = event.payload, input = record(p.rawInput);
|
|
20
|
+
const p = event.payload, input = record(p.rawInput), pending = this.data.pending;
|
|
17
21
|
if (event.eventName === 'tool_call' && p.kind === 'think' && record(record(p._meta).qoder).toolName === 'Agent' &&
|
|
18
22
|
typeof p.toolCallId === 'string' && typeof input.prompt === 'string' &&
|
|
19
|
-
(input.run_in_background === undefined || input.run_in_background === false))
|
|
20
|
-
|
|
23
|
+
(input.run_in_background === undefined || input.run_in_background === false)) {
|
|
24
|
+
Object.defineProperty(pending, p.toolCallId, { value: true, enumerable: true, writable: true, configurable: true });
|
|
25
|
+
}
|
|
21
26
|
if (event.eventName === 'tool_call_update' && typeof p.toolCallId === 'string' &&
|
|
22
|
-
['completed', 'failed', 'cancelled'].includes(String(p.status)))
|
|
23
|
-
|
|
27
|
+
['completed', 'failed', 'cancelled'].includes(String(p.status)) && Object.hasOwn(pending, p.toolCallId))
|
|
28
|
+
delete pending[p.toolCallId];
|
|
24
29
|
// Identified/draft child streams and controls keep their existing handling. Never assign an
|
|
25
30
|
// unaddressed chunk to whichever child happens to finish first.
|
|
26
|
-
return
|
|
31
|
+
return Object.keys(pending).length === 0 || !['agent_message_chunk', 'agent_thought_chunk'].includes(event.eventName) ||
|
|
27
32
|
typeof p.messageId === 'string' || p._meta != null;
|
|
28
33
|
}
|
|
29
|
-
endTurn() { this.pending
|
|
34
|
+
endTurn() { for (const id of Object.keys(this.data.pending))
|
|
35
|
+
delete this.data.pending[id]; }
|
|
30
36
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getSubagentMessages, listSubagents } from '@qoder-ai/qoder-agent-sdk';
|
|
2
1
|
const DEFAULT_LIMIT = 50;
|
|
3
2
|
const MAX_LIMIT = 200;
|
|
4
3
|
const MAX_OFFSET = 100_000;
|
|
@@ -7,7 +6,6 @@ const DEFAULT_TIMEOUT_MS = 5_000;
|
|
|
7
6
|
const MAX_TIMEOUT_MS = 30_000;
|
|
8
7
|
const DEFAULT_MAX_BYTES = 4 * 1024 * 1024;
|
|
9
8
|
const MAX_BYTES = 8 * 1024 * 1024;
|
|
10
|
-
const sdkReader = { listSubagents, getSubagentMessages };
|
|
11
9
|
const safeIdentity = (value) => typeof value === 'string' &&
|
|
12
10
|
value.length > 0 && value.length <= 256 && !/[\x00-\x1f\x7f\\/]/.test(value) &&
|
|
13
11
|
/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value);
|
|
@@ -43,7 +41,7 @@ function normalizeMessage(rootSessionId, agentId, message) {
|
|
|
43
41
|
* `offset` is a page offset, never a durable tail cursor. The SDK may read the full
|
|
44
42
|
* transcript before applying it; maxBytes bounds returned data, not bytes read on disk.
|
|
45
43
|
*/
|
|
46
|
-
export async function readChildHistory(rootSessionId, agentId, page, reader
|
|
44
|
+
export async function readChildHistory(rootSessionId, agentId, page, reader) {
|
|
47
45
|
const offset = page?.offset ?? 0;
|
|
48
46
|
const limit = page?.limit ?? DEFAULT_LIMIT;
|
|
49
47
|
const timeoutMs = page?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -4,6 +4,7 @@ import { mkdtemp, rm } from 'node:fs/promises';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { loadSdkDriver } from './sdkDriver.js';
|
|
7
|
+
import { loadPreparedSdkRuntime } from './sdkRuntime.js';
|
|
7
8
|
import { QoderAdapterSession } from './session.js';
|
|
8
9
|
import { supportsQoderSdkSteering } from './sdkDriver.js';
|
|
9
10
|
const stopRequest = (value) => {
|
|
@@ -79,9 +80,10 @@ export function createQoderAgent(options) {
|
|
|
79
80
|
return { app, close };
|
|
80
81
|
}
|
|
81
82
|
export async function runQoderAdapter(executable) {
|
|
82
|
-
const
|
|
83
|
+
const { sdk } = await loadPreparedSdkRuntime();
|
|
84
|
+
const driver = await loadSdkDriver(executable, sdk);
|
|
83
85
|
const runDir = await mkdtemp(join(tmpdir(), 'axom-qoder-acp-'));
|
|
84
|
-
const server = createQoderAgent({ driver, runDir });
|
|
86
|
+
const server = createQoderAgent({ driver, runDir, historyReader: sdk });
|
|
85
87
|
try {
|
|
86
88
|
const connection = server.app.connect(ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin)));
|
|
87
89
|
await connection.closed;
|
|
@@ -14,7 +14,7 @@ export const QODER_SDK_STEERING_CLI_VERSIONS = ['1.1.61', '1.1.64'];
|
|
|
14
14
|
export function supportsQoderSdkSteering(cliVersion) {
|
|
15
15
|
return QODER_SDK_STEERING_CLI_VERSIONS.includes(cliVersion);
|
|
16
16
|
}
|
|
17
|
-
export async function loadSdkDriver(executable, queryFactory) {
|
|
17
|
+
export async function loadSdkDriver(executable, sdk, queryFactory) {
|
|
18
18
|
if (!isAbsolute(executable))
|
|
19
19
|
throw new Error('Qoder executable must be an absolute local path');
|
|
20
20
|
let selected;
|
|
@@ -36,7 +36,6 @@ export async function loadSdkDriver(executable, queryFactory) {
|
|
|
36
36
|
}
|
|
37
37
|
if (!supportsQoderSdkSteering(version))
|
|
38
38
|
throw new Error(`Qoder executable must report a supported version (${QODER_SDK_STEERING_CLI_VERSIONS.join(' or ')})`);
|
|
39
|
-
const sdk = await import('@qoder-ai/qoder-agent-sdk');
|
|
40
39
|
return { executable: selected, sdkVersion: '1.0.50', cliVersion: version,
|
|
41
40
|
create: ({ prompt, options }) => (queryFactory ?? sdk.query)({ prompt, options: {
|
|
42
41
|
cwd: options.cwd, sessionId: options.sessionId, plugins: options.plugins,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, realpath, lstat } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { clientDir } from '../config.js';
|
|
7
|
+
export const QODER_SDK_VERSION = '1.0.50';
|
|
8
|
+
export const QODER_SDK_PACKAGE = '@qoder-ai/qoder-agent-sdk';
|
|
9
|
+
export const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
export async function sdkRecipe(options = {}) {
|
|
11
|
+
const directory = options.recipeDir ?? fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? '../../../runtime-deps/qoder-sdk/' : '../../runtime-deps/qoder-sdk/', import.meta.url));
|
|
12
|
+
const [manifest, lock] = await Promise.all(['package.json', 'package-lock.json'].map(file => readFile(join(directory, file))));
|
|
13
|
+
const pkg = JSON.parse(manifest.toString());
|
|
14
|
+
const parsed = JSON.parse(lock.toString());
|
|
15
|
+
if (pkg.dependencies?.[QODER_SDK_PACKAGE] !== QODER_SDK_VERSION ||
|
|
16
|
+
parsed.packages?.['']?.dependencies?.[QODER_SDK_PACKAGE] !== QODER_SDK_VERSION ||
|
|
17
|
+
parsed.packages?.[`node_modules/${QODER_SDK_PACKAGE}`]?.version !== QODER_SDK_VERSION)
|
|
18
|
+
throw new Error('Invalid bundled Qoder SDK recipe');
|
|
19
|
+
const digest = sha256(Buffer.concat([Buffer.from('axom-qoder-sdk-v1\0'), manifest, Buffer.from('\0'), lock]));
|
|
20
|
+
return { manifest, lock, digest, packages: parsed.packages,
|
|
21
|
+
root: resolve(options.configDir ?? clientDir(), 'acp-adapters', 'qoder-sdk', digest, `${process.platform}-${process.arch}-node${process.versions.node.split('.')[0]}`) };
|
|
22
|
+
}
|
|
23
|
+
function inside(root, file) {
|
|
24
|
+
const rel = relative(root, file);
|
|
25
|
+
return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
26
|
+
}
|
|
27
|
+
/** Validate the selected instance, never a package found in an ancestor or global prefix. */
|
|
28
|
+
export async function validateSdkInstance(instance, recipe) {
|
|
29
|
+
const canonical = await realpath(instance);
|
|
30
|
+
if (!(await lstat(instance)).isDirectory())
|
|
31
|
+
throw new Error('SDK instance must not be a symlink');
|
|
32
|
+
for (const [file, bytes] of [['package.json', recipe.manifest], ['package-lock.json', recipe.lock]]) {
|
|
33
|
+
if (!(await readFile(join(instance, file))).equals(bytes))
|
|
34
|
+
throw new Error(`SDK ${file} does not match recipe`);
|
|
35
|
+
}
|
|
36
|
+
for (const [key, metadata] of Object.entries(recipe.packages)) {
|
|
37
|
+
if (key === '')
|
|
38
|
+
continue;
|
|
39
|
+
if (!key.startsWith('node_modules/') || key.split('/').includes('..') || metadata.link)
|
|
40
|
+
throw new Error('Invalid SDK lockfile package path');
|
|
41
|
+
const directory = join(instance, key);
|
|
42
|
+
let actual;
|
|
43
|
+
try {
|
|
44
|
+
if (!inside(canonical, await realpath(directory)))
|
|
45
|
+
throw new Error('SDK dependency escapes its instance');
|
|
46
|
+
actual = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (metadata.optional && error.code === 'ENOENT')
|
|
50
|
+
continue;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
const name = key.slice(key.lastIndexOf('node_modules/') + 'node_modules/'.length);
|
|
54
|
+
if (actual.name !== name || actual.version !== metadata.version)
|
|
55
|
+
throw new Error(`SDK dependency mismatch: ${name}`);
|
|
56
|
+
}
|
|
57
|
+
const require = createRequire(join(instance, 'package.json'));
|
|
58
|
+
const entry = await realpath(require.resolve(QODER_SDK_PACKAGE));
|
|
59
|
+
if (!inside(join(canonical, 'node_modules', QODER_SDK_PACKAGE), entry))
|
|
60
|
+
throw new Error('SDK entry escapes its package');
|
|
61
|
+
return { entry, entrySha256: sha256(await readFile(entry)) };
|
|
62
|
+
}
|
|
63
|
+
export function assertSdkApi(sdk) {
|
|
64
|
+
const api = sdk;
|
|
65
|
+
if (!api || ['query', 'qodercliAuth', 'listSubagents', 'getSubagentMessages'].some(key => typeof api[key] !== 'function') || !api.ProcessTransport?.default)
|
|
66
|
+
throw new Error('Prepared Qoder SDK is missing required APIs');
|
|
67
|
+
}
|
|
68
|
+
export async function readPreparedSdkRuntime(options = {}) {
|
|
69
|
+
const recipe = await sdkRecipe(options);
|
|
70
|
+
const pointer = JSON.parse(await readFile(join(recipe.root, 'current.json'), 'utf8'));
|
|
71
|
+
if (pointer.digest !== recipe.digest || !/^[a-f0-9-]{36}$/.test(pointer.installationId ?? ''))
|
|
72
|
+
throw new Error('Invalid SDK installation pointer');
|
|
73
|
+
const instance = join(recipe.root, 'instances', pointer.installationId);
|
|
74
|
+
const receipt = JSON.parse(await readFile(join(instance, 'receipt.json'), 'utf8'));
|
|
75
|
+
const validated = await validateSdkInstance(instance, recipe);
|
|
76
|
+
if (receipt.schema !== 1 || receipt.digest !== recipe.digest || receipt.version !== QODER_SDK_VERSION ||
|
|
77
|
+
receipt.installationId !== pointer.installationId || receipt.entrySha256 !== validated.entrySha256 ||
|
|
78
|
+
receipt.lockSha256 !== sha256(recipe.lock))
|
|
79
|
+
throw new Error('Prepared SDK receipt does not match installation');
|
|
80
|
+
return { instance, ...validated };
|
|
81
|
+
}
|
|
82
|
+
/** Read-only: ACP serving must never install dependencies or fall back to the developer SDK. */
|
|
83
|
+
export async function loadPreparedSdkRuntime(options = {}) {
|
|
84
|
+
try {
|
|
85
|
+
const prepared = await readPreparedSdkRuntime(options);
|
|
86
|
+
const sdk = await import(pathToFileURL(prepared.entry).href);
|
|
87
|
+
assertSdkApi(sdk);
|
|
88
|
+
return { ...prepared, sdk };
|
|
89
|
+
}
|
|
90
|
+
catch (cause) {
|
|
91
|
+
throw new Error(`Qoder SDK is not prepared or is damaged. Run axom qoder-acp --prepare-sdk as the same user with AXOM_WORKSTATION_CONFIG_DIR=${options.configDir ?? clientDir()}`, { cause });
|
|
92
|
+
}
|
|
93
|
+
}
|