@aragon-agent/cli 0.6.38 → 0.6.39
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 +2523 -2326
- package/README.md +3614 -3173
- package/dist/agent/controller.d.ts +142 -1
- package/dist/agent/controller.js +171 -3
- package/dist/agent/reducer.d.ts +41 -1
- package/dist/agent/reducer.js +25 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/cli.js +85 -27
- package/dist/commands/builtins.js +13 -1
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/config/cli-commands.d.ts +23 -0
- package/dist/config/cli-commands.js +36 -0
- package/dist/config/env.js +17 -4
- package/dist/config/load.js +6 -1
- package/dist/config/schema.d.ts +43 -4
- package/dist/config/schema.js +31 -5
- package/dist/config/store.js +17 -2
- package/dist/diagnostics/doctor.d.ts +9 -0
- package/dist/diagnostics/doctor.js +34 -0
- package/dist/diagnostics/info.d.ts +71 -0
- package/dist/diagnostics/info.js +91 -5
- package/dist/exec/bridge/caller-tools.d.ts +96 -0
- package/dist/exec/bridge/caller-tools.js +213 -0
- package/dist/exec/bridge/decisions.d.ts +78 -0
- package/dist/exec/bridge/decisions.js +116 -0
- package/dist/exec/bridge/human-answers.d.ts +38 -0
- package/dist/exec/bridge/human-answers.js +164 -0
- package/dist/exec/bridge/limits.d.ts +68 -0
- package/dist/exec/bridge/limits.js +68 -0
- package/dist/exec/bridge/ndjson.d.ts +17 -0
- package/dist/exec/bridge/ndjson.js +83 -0
- package/dist/exec/bridge/pending.d.ts +70 -0
- package/dist/exec/bridge/pending.js +132 -0
- package/dist/exec/bridge/protocol.d.ts +346 -0
- package/dist/exec/bridge/protocol.js +256 -0
- package/dist/exec/bridge/server.d.ts +142 -0
- package/dist/exec/bridge/server.js +502 -0
- package/dist/exec/bridge/source.d.ts +58 -0
- package/dist/exec/bridge/source.js +145 -0
- package/dist/exec/bridge/writer.d.ts +26 -0
- package/dist/exec/bridge/writer.js +156 -0
- package/dist/exec/cli-commands.js +30 -2
- package/dist/exec/emitter.d.ts +16 -2
- package/dist/exec/emitter.js +26 -6
- package/dist/exec/events.d.ts +465 -10
- package/dist/exec/events.js +47 -1
- package/dist/exec/hooks/dispatch.d.ts +76 -0
- package/dist/exec/hooks/dispatch.js +169 -0
- package/dist/exec/hooks/index.d.ts +83 -0
- package/dist/exec/hooks/index.js +205 -0
- package/dist/exec/hooks/limits.d.ts +56 -0
- package/dist/exec/hooks/limits.js +49 -0
- package/dist/exec/hooks/registry.d.ts +49 -0
- package/dist/exec/hooks/registry.js +91 -0
- package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
- package/dist/exec/hooks/tool-wrapper.js +154 -0
- package/dist/exec/hooks/turn.d.ts +61 -0
- package/dist/exec/hooks/turn.js +59 -0
- package/dist/exec/hooks/validate.d.ts +85 -0
- package/dist/exec/hooks/validate.js +310 -0
- package/dist/exec/index.d.ts +130 -4
- package/dist/exec/index.js +501 -10
- package/dist/exec/options.d.ts +124 -2
- package/dist/exec/options.js +413 -6
- package/dist/exec/permission.d.ts +9 -1
- package/dist/exec/permission.js +7 -1
- package/dist/exec/request/limits.d.ts +51 -0
- package/dist/exec/request/limits.js +49 -0
- package/dist/exec/request/scope.d.ts +106 -0
- package/dist/exec/request/scope.js +149 -0
- package/dist/exec/result/limits.d.ts +74 -0
- package/dist/exec/result/limits.js +72 -0
- package/dist/exec/result/prompt.d.ts +28 -0
- package/dist/exec/result/prompt.js +74 -0
- package/dist/exec/result/schema.d.ts +99 -0
- package/dist/exec/result/schema.js +464 -0
- package/dist/exec/result/sink.d.ts +56 -0
- package/dist/exec/result/sink.js +50 -0
- package/dist/exec/result/tool.d.ts +37 -0
- package/dist/exec/result/tool.js +118 -0
- package/dist/exec/runner.d.ts +395 -1
- package/dist/exec/runner.js +860 -8
- package/dist/exec/threads/aggregate.d.ts +34 -0
- package/dist/exec/threads/aggregate.js +30 -0
- package/dist/exec/threads/capacity.d.ts +23 -0
- package/dist/exec/threads/capacity.js +38 -0
- package/dist/exec/threads/dispatch-source.d.ts +60 -0
- package/dist/exec/threads/dispatch-source.js +106 -0
- package/dist/exec/threads/execute-threads.d.ts +53 -0
- package/dist/exec/threads/execute-threads.js +234 -0
- package/dist/exec/threads/gate.d.ts +42 -0
- package/dist/exec/threads/gate.js +81 -0
- package/dist/exec/threads/identity.d.ts +36 -0
- package/dist/exec/threads/identity.js +67 -0
- package/dist/exec/threads/interrupt.d.ts +29 -0
- package/dist/exec/threads/interrupt.js +79 -0
- package/dist/exec/threads/limits.d.ts +58 -0
- package/dist/exec/threads/limits.js +35 -0
- package/dist/exec/threads/outcome.d.ts +47 -0
- package/dist/exec/threads/outcome.js +26 -0
- package/dist/exec/threads/registry.d.ts +81 -0
- package/dist/exec/threads/registry.js +427 -0
- package/dist/exec/threads/runtime.d.ts +107 -0
- package/dist/exec/threads/runtime.js +523 -0
- package/dist/exec/threads/thread-source.d.ts +20 -0
- package/dist/exec/threads/thread-source.js +134 -0
- package/dist/fast/limits.d.ts +54 -0
- package/dist/fast/limits.js +54 -0
- package/dist/fast/summary-call.d.ts +72 -0
- package/dist/fast/summary-call.js +133 -0
- package/dist/fast/summary-prompt.d.ts +106 -0
- package/dist/fast/summary-prompt.js +320 -0
- package/dist/fast/task-summary-types.d.ts +66 -0
- package/dist/fast/task-summary-types.js +17 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/limits.d.ts +7 -0
- package/dist/proc/limits.js +7 -0
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/team/runtime.d.ts +9 -0
- package/dist/team/runtime.js +8 -0
- package/dist/team/subagent.d.ts +15 -0
- package/dist/team/subagent.js +45 -1
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +224 -11
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/interaction-hints.js +6 -0
- package/dist/ui/layout/OverlayFrame.d.ts +18 -0
- package/dist/ui/layout/OverlayFrame.js +19 -4
- package/dist/ui/layout/header-hints.d.ts +55 -62
- package/dist/ui/layout/header-hints.js +59 -166
- package/dist/ui/layout/header-left.d.ts +115 -0
- package/dist/ui/layout/header-left.js +206 -0
- package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
- package/dist/ui/layout/shortcut-panel-layout.js +321 -0
- package/dist/ui/layout/status-detail-layout.js +5 -1
- package/dist/ui/layout/task-summary-cell.d.ts +61 -0
- package/dist/ui/layout/task-summary-cell.js +65 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.js +29 -2
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every bound the bridge protocol has, in one object (headless-host-bridge I-6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* ONE AUTHORITY, NO LITERALS ELSEWHERE. "How long may a decision wait", "how many
|
|
7
|
+
* caller tools may exist", "how many bytes may one frame be" are all answers a
|
|
8
|
+
* caller can be told (`aragon info --json`) and a validator has to enforce. Two
|
|
9
|
+
* copies of a number are two answers the moment one of them is edited, and the
|
|
10
|
+
* pair that disagrees is always discovered in the field.
|
|
11
|
+
*
|
|
12
|
+
* STRUCTURAL, NOT POLICY. These are what a protocol frame can physically carry,
|
|
13
|
+
* which is the same distinction `config/schema.ts` draws for user-tunable keys
|
|
14
|
+
* (`.claude-index/index.md`, "limits.ts is structural"). Nothing here is
|
|
15
|
+
* persisted and nothing here is settable from `config.json`: the two timeouts
|
|
16
|
+
* have flags because they are per-invocation budgets, and every other number is
|
|
17
|
+
* a property of the wire format.
|
|
18
|
+
*/
|
|
19
|
+
export const BRIDGE_LIMITS = {
|
|
20
|
+
/** One NDJSON frame. A larger line is skipped, never fatal. */
|
|
21
|
+
maxFrameBytes: 1_048_576,
|
|
22
|
+
/** Declared caller tools in one `tool_register` frame. */
|
|
23
|
+
maxCallerTools: 32,
|
|
24
|
+
maxCallerToolNameChars: 40,
|
|
25
|
+
maxCallerToolSchemaBytes: 16_384,
|
|
26
|
+
maxCallerToolDescriptionChars: 1_024,
|
|
27
|
+
/** Mirrors Core's `ToolExecutor` default `maxOutputSize` (executor.ts:42). */
|
|
28
|
+
maxCallerToolResultBytes: 100_000,
|
|
29
|
+
/** Concurrent in-flight caller tools. Over the cap, the call is refused. */
|
|
30
|
+
maxOutstandingCallerTools: 8,
|
|
31
|
+
/** Concurrent unanswered decisions, lead plus every subagent. */
|
|
32
|
+
maxOutstandingDecisions: 8,
|
|
33
|
+
/**
|
|
34
|
+
* Recently settled request ids remembered so a REPEATED answer can be answered
|
|
35
|
+
* as `bridge_duplicate_response` instead of `bridge_unknown_request`.
|
|
36
|
+
*
|
|
37
|
+
* The design dropped the 512-entry decision ledger (review P2-7) on the grounds
|
|
38
|
+
* that the pending table's settle-once already covered duplicate detection. That
|
|
39
|
+
* is true only while the entry is still IN the table: this table removes an
|
|
40
|
+
* entry the moment it settles, so without a small ring the very first repeat
|
|
41
|
+
* would be reported as an unknown request - a different code, and a misleading
|
|
42
|
+
* one, for a caller that answered twice. The ring has exactly one consumer (the
|
|
43
|
+
* duplicate branch in `pending.ts`), which is the condition P2-7 set for keeping
|
|
44
|
+
* any such state at all.
|
|
45
|
+
*/
|
|
46
|
+
maxSettledIds: 256,
|
|
47
|
+
/** Default and hard ceiling for one decision's deadline. */
|
|
48
|
+
defaultDecisionTimeoutMs: 300_000,
|
|
49
|
+
maxDecisionTimeoutMs: 1_800_000,
|
|
50
|
+
/** Caller tool timeouts: the flag's default, and its hard ceiling. */
|
|
51
|
+
defaultCallerToolTimeoutMs: 60_000,
|
|
52
|
+
maxCallerToolTimeoutMs: 600_000,
|
|
53
|
+
/**
|
|
54
|
+
* Strings the caller supplies that end up in front of the model.
|
|
55
|
+
*
|
|
56
|
+
* BOUNDED BECAUSE THEY ARE MODEL-VISIBLE, not because they are expensive: a
|
|
57
|
+
* denial reason and plan feedback both reach the transcript, so an unbounded
|
|
58
|
+
* one is an unbounded line in every subsequent provider request.
|
|
59
|
+
*/
|
|
60
|
+
maxDenyReasonChars: 500,
|
|
61
|
+
maxPlanFeedbackChars: 2_000,
|
|
62
|
+
maxCustomAnswerChars: 2_000,
|
|
63
|
+
/** `tool_invoke.input` is echoed back so the caller can act on it. */
|
|
64
|
+
maxInvokeInputChars: 16_384,
|
|
65
|
+
/** stdout backpressure watermarks. See `writer.ts`. */
|
|
66
|
+
softPendingBytes: 4_194_304,
|
|
67
|
+
hardPendingBytes: 16_777_216,
|
|
68
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface NdjsonSplitterOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Structural ceiling for one frame. `BRIDGE_LIMITS.maxFrameBytes` unless a
|
|
4
|
+
* caller is deliberately testing the boundary with a smaller one.
|
|
5
|
+
*/
|
|
6
|
+
maxFrameBytes?: number;
|
|
7
|
+
/** One complete line, terminator removed. Never called for an empty line. */
|
|
8
|
+
onLine: (line: string) => void;
|
|
9
|
+
/** A line was dropped for exceeding the ceiling. */
|
|
10
|
+
onTooLarge?: (bytes: number) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface NdjsonSplitter {
|
|
13
|
+
push(chunk: string | Buffer): void;
|
|
14
|
+
/** Deliver a final line that arrived without a trailing newline. */
|
|
15
|
+
end(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function createNdjsonSplitter(options: NdjsonSplitterOptions): NdjsonSplitter;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON line splitting, shared by `--input-format stream-json` and `bridge`.
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* PURE, AND EXTRACTED RATHER THAN COPIED. Both input formats read newline-delimited
|
|
7
|
+
* JSON off stdin and both must treat a bad line as non-fatal; a second copy of the
|
|
8
|
+
* splitting loop would be a second place for the `\r` rule below to be forgotten.
|
|
9
|
+
*
|
|
10
|
+
* THE `\r` RULE IS NOT A HYPOTHETICAL. This project ships Windows-first, and a
|
|
11
|
+
* caller on Windows that writes `JSON + "\r\n"` puts a carriage return at the end
|
|
12
|
+
* of every line. `JSON.parse` tolerates it, but the string we hand to a
|
|
13
|
+
* `content` field would then carry a stray `\r`, which reaches the model as part
|
|
14
|
+
* of a tool result. So the terminator is trimmed BEFORE the byte count and BEFORE
|
|
15
|
+
* parsing, in that order: counting the raw line first would reject a frame that is
|
|
16
|
+
* exactly at the cap only because of a line ending nobody meant to send.
|
|
17
|
+
*/
|
|
18
|
+
import { BRIDGE_LIMITS } from './limits.js';
|
|
19
|
+
export function createNdjsonSplitter(options) {
|
|
20
|
+
const maxBytes = options.maxFrameBytes ?? BRIDGE_LIMITS.maxFrameBytes;
|
|
21
|
+
let buffer = '';
|
|
22
|
+
/**
|
|
23
|
+
* `true` while discarding the tail of an over-long line.
|
|
24
|
+
*
|
|
25
|
+
* WITHOUT THIS the reader would buffer an unbounded amount of memory waiting for
|
|
26
|
+
* a newline that may never come - a caller that never sends one (a corrupted
|
|
27
|
+
* pipe, a writer stuck mid-frame) would exhaust the process instead of costing
|
|
28
|
+
* one skipped frame.
|
|
29
|
+
*/
|
|
30
|
+
let discarding = false;
|
|
31
|
+
const deliver = (raw) => {
|
|
32
|
+
if (discarding) {
|
|
33
|
+
discarding = false;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
37
|
+
if (line.trim().length === 0)
|
|
38
|
+
return;
|
|
39
|
+
const bytes = Buffer.byteLength(line, 'utf-8');
|
|
40
|
+
if (bytes > maxBytes) {
|
|
41
|
+
options.onTooLarge?.(bytes);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
options.onLine(line);
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
push(chunk) {
|
|
48
|
+
buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
49
|
+
for (;;) {
|
|
50
|
+
const index = buffer.indexOf('\n');
|
|
51
|
+
if (index < 0) {
|
|
52
|
+
// No terminator yet. An over-long partial line is dropped NOW rather
|
|
53
|
+
// than kept until one arrives; see `discarding`.
|
|
54
|
+
//
|
|
55
|
+
// ALREADY DISCARDING MEANS RETAIN NOTHING, not merely "do not report
|
|
56
|
+
// again": the flag only exists so the caller is told once, and keeping
|
|
57
|
+
// the tail would reintroduce the unbounded growth this branch was
|
|
58
|
+
// written to prevent - a caller that streams 100 MiB with no newline
|
|
59
|
+
// would still hold all of it.
|
|
60
|
+
if (discarding) {
|
|
61
|
+
buffer = '';
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const bytes = Buffer.byteLength(buffer, 'utf-8');
|
|
65
|
+
if (bytes > maxBytes) {
|
|
66
|
+
options.onTooLarge?.(bytes);
|
|
67
|
+
buffer = '';
|
|
68
|
+
discarding = true;
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const line = buffer.slice(0, index);
|
|
73
|
+
buffer = buffer.slice(index + 1);
|
|
74
|
+
deliver(line);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
end() {
|
|
78
|
+
if (buffer.length > 0)
|
|
79
|
+
deliver(buffer);
|
|
80
|
+
buffer = '';
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AgentScope, PendingKindValue } from './protocol.js';
|
|
2
|
+
export type PendingKind = PendingKindValue;
|
|
3
|
+
export interface PendingMeta {
|
|
4
|
+
tool?: string;
|
|
5
|
+
callId?: string;
|
|
6
|
+
agent?: AgentScope;
|
|
7
|
+
}
|
|
8
|
+
/** What an entry resolves with. Never rejects; see `DecisionOutcome`. */
|
|
9
|
+
export type PendingOutcome<T> = {
|
|
10
|
+
outcome: 'settled';
|
|
11
|
+
value: T;
|
|
12
|
+
} | {
|
|
13
|
+
outcome: 'timeout';
|
|
14
|
+
deadlineAt: number;
|
|
15
|
+
} | {
|
|
16
|
+
outcome: 'aborted';
|
|
17
|
+
} | {
|
|
18
|
+
outcome: 'closed';
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
export interface PendingCreateOptions {
|
|
22
|
+
kind: PendingKind;
|
|
23
|
+
timeoutMs: number;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
meta?: PendingMeta;
|
|
26
|
+
}
|
|
27
|
+
export interface PendingHandle<T> {
|
|
28
|
+
requestId: string;
|
|
29
|
+
createdAt: number;
|
|
30
|
+
deadlineAt: number;
|
|
31
|
+
settled: Promise<PendingOutcome<T>>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `kind_mismatch` is NOT a duplicate and NOT unknown: the two frames really do
|
|
35
|
+
* refer to this request id, they just answer the wrong one of the two questions
|
|
36
|
+
* this table multiplexes. The entry must survive, because the party on the other
|
|
37
|
+
* end is still waiting for its own answer (AC-55).
|
|
38
|
+
*/
|
|
39
|
+
export type PendingAnswerResult = 'ok' | 'duplicate' | 'unknown' | 'kind_mismatch';
|
|
40
|
+
export interface PendingTable {
|
|
41
|
+
/** `null` at capacity: refuse immediately rather than queueing. */
|
|
42
|
+
create<T>(options: PendingCreateOptions): PendingHandle<T> | null;
|
|
43
|
+
answer(requestId: string, kind: PendingKind, value: unknown): PendingAnswerResult;
|
|
44
|
+
/** Turn abort / interrupt / EOF: every entry resolves `aborted`, synchronously. */
|
|
45
|
+
abortAll(): void;
|
|
46
|
+
/** Server shutdown: every entry resolves `closed`. */
|
|
47
|
+
closeAll(reason: string): void;
|
|
48
|
+
size(): number;
|
|
49
|
+
kindOf(requestId: string): PendingKind | undefined;
|
|
50
|
+
metaOf(requestId: string): PendingMeta | undefined;
|
|
51
|
+
/** Ids still awaiting an answer, for tests and for `close()` logging. */
|
|
52
|
+
pendingIds(): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Was this id settled BY AN ANSWER recently enough to still be remembered?
|
|
55
|
+
*
|
|
56
|
+
* The entry itself is gone once settled, so without this probe a second answer
|
|
57
|
+
* is indistinguishable from an answer to an id that never existed - and those
|
|
58
|
+
* two are different codes on purpose (`bridge_duplicate_response` vs
|
|
59
|
+
* `bridge_unknown_request`). Timeouts, aborts and closures are deliberately NOT
|
|
60
|
+
* remembered: answering one of those is "too late", not "twice".
|
|
61
|
+
*/
|
|
62
|
+
wasAnswered(requestId: string): boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface PendingTableOptions {
|
|
65
|
+
now?: () => number;
|
|
66
|
+
maxDecisions?: number;
|
|
67
|
+
maxCallerTools?: number;
|
|
68
|
+
maxSettledIds?: number;
|
|
69
|
+
}
|
|
70
|
+
export declare function createPendingTable(options?: PendingTableOptions): PendingTable;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pending-request table: one entry per unanswered request, in either direction.
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* ONE TABLE FOR TWO TRAFFIC DIRECTIONS, DELIBERATELY (headless-host-bridge section 1.3).
|
|
7
|
+
* A `decision_request` is the CLI asking the caller, a `tool_invoke` is the caller
|
|
8
|
+
* serving the CLI; both need an id, a deadline, settle-once, cancellation and a
|
|
9
|
+
* capacity ceiling. Two implementations would be two sets of rules about what a
|
|
10
|
+
* duplicate answer means, and the second one would be written by someone reading
|
|
11
|
+
* the first one. `kind` is what keeps them distinguishable, and it is also what
|
|
12
|
+
* makes `bridge_kind_mismatch` detectable at all.
|
|
13
|
+
*
|
|
14
|
+
* SETTLE-ONCE IS THE WHOLE STATE MACHINE. `done` is the only authority: a second
|
|
15
|
+
* `answer()` for the same id returns `duplicate` and changes nothing, and an
|
|
16
|
+
* `answer()` for an id that was never allocated returns `unknown`. Neither case
|
|
17
|
+
* changes the outcome the waiting tool already received.
|
|
18
|
+
*/
|
|
19
|
+
import { BRIDGE_LIMITS } from './limits.js';
|
|
20
|
+
export function createPendingTable(options = {}) {
|
|
21
|
+
const now = options.now ?? Date.now;
|
|
22
|
+
const maxDecisions = options.maxDecisions ?? BRIDGE_LIMITS.maxOutstandingDecisions;
|
|
23
|
+
const maxCallerTools = options.maxCallerTools ?? BRIDGE_LIMITS.maxOutstandingCallerTools;
|
|
24
|
+
const maxSettled = options.maxSettledIds ?? BRIDGE_LIMITS.maxSettledIds;
|
|
25
|
+
const entries = new Map();
|
|
26
|
+
/** Bounded FIFO of ids settled by an ANSWER recently; see `maxSettledIds`. */
|
|
27
|
+
const settledIds = [];
|
|
28
|
+
let counter = 0;
|
|
29
|
+
const countOf = (kind) => {
|
|
30
|
+
let total = 0;
|
|
31
|
+
for (const entry of entries.values())
|
|
32
|
+
if (entry.kind === kind)
|
|
33
|
+
total += 1;
|
|
34
|
+
return total;
|
|
35
|
+
};
|
|
36
|
+
const remember = (requestId) => {
|
|
37
|
+
settledIds.push(requestId);
|
|
38
|
+
if (settledIds.length > maxSettled)
|
|
39
|
+
settledIds.shift();
|
|
40
|
+
};
|
|
41
|
+
const settleEntry = (entry, outcome, answered = false) => {
|
|
42
|
+
if (entry.done)
|
|
43
|
+
return false;
|
|
44
|
+
entry.done = true;
|
|
45
|
+
if (entry.timer)
|
|
46
|
+
clearTimeout(entry.timer);
|
|
47
|
+
entry.detachAbort?.();
|
|
48
|
+
entries.delete(entry.requestId);
|
|
49
|
+
if (answered)
|
|
50
|
+
remember(entry.requestId);
|
|
51
|
+
// RESOLVING IS SYNCHRONOUS HERE, which is what AC-49 depends on: when the
|
|
52
|
+
// turn aborts, every waiting tool is settled inside the abort listener's own
|
|
53
|
+
// tick, so it can finish inside the executor's 5 s grace window instead of
|
|
54
|
+
// being abandoned with the executor's own wording.
|
|
55
|
+
entry.settle(outcome);
|
|
56
|
+
return true;
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
create(create) {
|
|
60
|
+
const cap = create.kind === 'decision' ? maxDecisions : maxCallerTools;
|
|
61
|
+
if (countOf(create.kind) >= cap)
|
|
62
|
+
return null;
|
|
63
|
+
counter += 1;
|
|
64
|
+
const createdAt = now();
|
|
65
|
+
const entry = {
|
|
66
|
+
requestId: `req_${counter}`,
|
|
67
|
+
kind: create.kind,
|
|
68
|
+
createdAt,
|
|
69
|
+
deadlineAt: createdAt + create.timeoutMs,
|
|
70
|
+
meta: create.meta ?? {},
|
|
71
|
+
done: false,
|
|
72
|
+
settle: () => { },
|
|
73
|
+
};
|
|
74
|
+
let resolveOutcome = () => { };
|
|
75
|
+
const settled = new Promise((resolve) => {
|
|
76
|
+
resolveOutcome = resolve;
|
|
77
|
+
});
|
|
78
|
+
entry.settle = resolveOutcome;
|
|
79
|
+
entries.set(entry.requestId, entry);
|
|
80
|
+
entry.timer = setTimeout(() => {
|
|
81
|
+
settleEntry(entry, { outcome: 'timeout', deadlineAt: entry.deadlineAt });
|
|
82
|
+
}, Math.max(0, create.timeoutMs));
|
|
83
|
+
// `unref` so a pending deadline can never be the reason the process stays
|
|
84
|
+
// alive: the run has its own reason to be running, and this timer is not it.
|
|
85
|
+
entry.timer.unref?.();
|
|
86
|
+
const signal = create.signal;
|
|
87
|
+
if (signal?.aborted) {
|
|
88
|
+
// ALREADY ABORTED, SO SETTLE NOW RATHER THAN ARMING A LISTENER. The
|
|
89
|
+
// executor abandons a tool 5 s after abort; a wait that only learned about
|
|
90
|
+
// the abort later would surface as "did not stop within 5000ms of abort"
|
|
91
|
+
// instead of "the turn was interrupted" (AC-49).
|
|
92
|
+
settleEntry(entry, { outcome: 'aborted' });
|
|
93
|
+
}
|
|
94
|
+
else if (signal) {
|
|
95
|
+
const onAbort = () => {
|
|
96
|
+
settleEntry(entry, { outcome: 'aborted' });
|
|
97
|
+
};
|
|
98
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
99
|
+
entry.detachAbort = () => signal.removeEventListener('abort', onAbort);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
requestId: entry.requestId,
|
|
103
|
+
createdAt: entry.createdAt,
|
|
104
|
+
deadlineAt: entry.deadlineAt,
|
|
105
|
+
settled: settled,
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
answer(requestId, kind, value) {
|
|
109
|
+
const entry = entries.get(requestId);
|
|
110
|
+
if (!entry)
|
|
111
|
+
return settledIds.includes(requestId) ? 'duplicate' : 'unknown';
|
|
112
|
+
if (entry.kind !== kind)
|
|
113
|
+
return 'kind_mismatch';
|
|
114
|
+
return settleEntry(entry, { outcome: 'settled', value }, true) ? 'ok' : 'duplicate';
|
|
115
|
+
},
|
|
116
|
+
abortAll() {
|
|
117
|
+
for (const entry of [...entries.values()]) {
|
|
118
|
+
settleEntry(entry, { outcome: 'aborted' });
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
closeAll(reason) {
|
|
122
|
+
for (const entry of [...entries.values()]) {
|
|
123
|
+
settleEntry(entry, { outcome: 'closed', reason });
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
size: () => entries.size,
|
|
127
|
+
kindOf: (requestId) => entries.get(requestId)?.kind,
|
|
128
|
+
metaOf: (requestId) => entries.get(requestId)?.meta,
|
|
129
|
+
pendingIds: () => [...entries.keys()],
|
|
130
|
+
wasAnswered: (requestId) => settledIds.includes(requestId),
|
|
131
|
+
};
|
|
132
|
+
}
|