@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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge wire protocol: frame types, one parser, and the outbound builders.
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* PURE. No I/O, no timers, no streams: every function here takes text or plain
|
|
7
|
+
* objects and returns text or plain objects. That is what makes the protocol
|
|
8
|
+
* testable at the level where the bugs actually live (a frame with a missing
|
|
9
|
+
* field, a `respondsTo` that points at nothing) without a subprocess in sight.
|
|
10
|
+
*
|
|
11
|
+
* TWO RULES DECIDE EVERYTHING BELOW, and they pull in opposite directions:
|
|
12
|
+
*
|
|
13
|
+
* AN UNKNOWN `type` IS SILENTLY IGNORED (forward compatibility). A caller
|
|
14
|
+
* written against a newer CLI must not be punished for a frame this build never
|
|
15
|
+
* heard of; the same promise `stdin-stream.ts` already makes for `stream-json`.
|
|
16
|
+
*
|
|
17
|
+
* AN UNKNOWN `respondsTo` IS AN ERROR (caller bug). There is no forward
|
|
18
|
+
* compatibility to preserve - the caller answered a request that does not
|
|
19
|
+
* exist - and silence would leave it waiting for an outcome forever.
|
|
20
|
+
*/
|
|
21
|
+
import type { NormalizedPlan, NormalizedQuestion, HumanRequest } from '../../tools/human-input.js';
|
|
22
|
+
export declare const BRIDGE_PROTOCOL_VERSION = 2;
|
|
23
|
+
/**
|
|
24
|
+
* What this build announces in `system/init.capabilities` when bridge is on.
|
|
25
|
+
*
|
|
26
|
+
* APPEND ONLY, NEVER REORDER, same discipline as `EXEC_CAPABILITIES`: the caller
|
|
27
|
+
* asks `includes('caller_tools')` and ignores what it does not recognise.
|
|
28
|
+
*/
|
|
29
|
+
export declare const BRIDGE_CAPABILITIES: readonly string[];
|
|
30
|
+
/** The four things a caller can be asked to decide. */
|
|
31
|
+
export type DecisionKind = 'tool' | 'questions' | 'plan' | 'skill_install';
|
|
32
|
+
/**
|
|
33
|
+
* The two directions a pending request can travel.
|
|
34
|
+
*
|
|
35
|
+
* `decision` is the CLI asking (a tool approval, a question, a plan, an install);
|
|
36
|
+
* `caller_tool` is the CLI asking the caller to DO something. A `respondsTo`
|
|
37
|
+
* pointing at the wrong one is `bridge_kind_mismatch`, which is why the table has
|
|
38
|
+
* to know which kind each id belongs to.
|
|
39
|
+
*/
|
|
40
|
+
export type PendingKindValue = 'decision' | 'caller_tool';
|
|
41
|
+
/**
|
|
42
|
+
* Where a tool call came from. `depth` 0 is the lead; subagents are 1 or more.
|
|
43
|
+
*
|
|
44
|
+
* SENT ON EVERY REQUEST, even the lead's, so a caller never has to infer it from
|
|
45
|
+
* the absence of a field - the same reason `sessionFile ?? null` is stamped
|
|
46
|
+
* unconditionally on `system/init`.
|
|
47
|
+
*/
|
|
48
|
+
export interface AgentScope {
|
|
49
|
+
id: string;
|
|
50
|
+
label: string;
|
|
51
|
+
depth: number;
|
|
52
|
+
}
|
|
53
|
+
export declare const LEAD_SCOPE: AgentScope;
|
|
54
|
+
/** One caller-served tool, as `tool_register` declares it. */
|
|
55
|
+
export interface CallerToolSpec {
|
|
56
|
+
name: string;
|
|
57
|
+
label?: string;
|
|
58
|
+
description: string;
|
|
59
|
+
parameters: Record<string, unknown>;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface BridgePromptFrame {
|
|
63
|
+
type: 'prompt';
|
|
64
|
+
id?: string;
|
|
65
|
+
text: string;
|
|
66
|
+
/**
|
|
67
|
+
* Thread mode only (headless-threads-and-lifecycle-hooks section 2.3.2). ABSENT
|
|
68
|
+
* MEANS THE DEFAULT THREAD `main`, which is what keeps a v1 caller working without
|
|
69
|
+
* a single change (AC-11).
|
|
70
|
+
*/
|
|
71
|
+
threadId?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface BridgeSteerFrame {
|
|
74
|
+
type: 'steer';
|
|
75
|
+
id?: string;
|
|
76
|
+
text: string;
|
|
77
|
+
threadId?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface BridgeInterruptFrame {
|
|
80
|
+
type: 'interrupt';
|
|
81
|
+
id?: string;
|
|
82
|
+
/** Omitted means "the turn in flight". A queued turn is named explicitly. */
|
|
83
|
+
requestId?: string;
|
|
84
|
+
/**
|
|
85
|
+
* A THREAD ID HERE IS A REFUSAL, NOT A ROUTE (section 2.3.2 / AC-34).
|
|
86
|
+
*
|
|
87
|
+
* It is parsed so the router can answer
|
|
88
|
+
* `ack{accepted:false, code:'interrupt_is_process_level'}` instead of treating the
|
|
89
|
+
* frame as unknown. Degrading it to `cancel_request` would erase the difference
|
|
90
|
+
* between "the user pressed Ctrl-C" and "the backend withdrew one request".
|
|
91
|
+
*/
|
|
92
|
+
threadId?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Stop ONE request; keep the process serving (section 2.4.7).
|
|
96
|
+
*
|
|
97
|
+
* A SECOND FRAME RATHER THAN A FLAG ON `interrupt`, because the two answer different
|
|
98
|
+
* questions: `interrupt` means "stop this RUN" (process-level, `128 + signo`), and
|
|
99
|
+
* re-purposing it would make "the user pressed Ctrl-C" indistinguishable from "the
|
|
100
|
+
* backend withdrew one request" - after the fact there would be no way to tell.
|
|
101
|
+
*
|
|
102
|
+
* It is a CONTROL frame, so it continues to be routed after `end` (a caller that has
|
|
103
|
+
* sent its last prompt is still entitled to withdraw one), and it is ignored after
|
|
104
|
+
* `result` like every other frame.
|
|
105
|
+
*/
|
|
106
|
+
export interface BridgeCancelRequestFrame {
|
|
107
|
+
type: 'cancel_request';
|
|
108
|
+
/** This frame's own id, echoed on the `ack`. */
|
|
109
|
+
id?: string;
|
|
110
|
+
/** Omitted means "the request in flight"; named, it must match one exactly. */
|
|
111
|
+
requestId?: string;
|
|
112
|
+
threadId?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The four decision shapes share one frame, discriminated by what it carries.
|
|
116
|
+
*
|
|
117
|
+
* A SINGLE FRAME RATHER THAN FOUR is the whole point of `kind` on the outbound
|
|
118
|
+
* side: the caller answers with the same envelope whatever it was asked, and the
|
|
119
|
+
* gate that asked is the one that knows how to read the answer.
|
|
120
|
+
*/
|
|
121
|
+
export interface BridgeDecisionFrame {
|
|
122
|
+
type: 'decision';
|
|
123
|
+
id?: string;
|
|
124
|
+
/**
|
|
125
|
+
* OPTIONAL ON PURPOSE (section 2.3.2). `respondsTo` is already globally unique, so a
|
|
126
|
+
* caller that lost its thread bookkeeping is still served correctly by the default
|
|
127
|
+
* thread's server - which is why the field is a convenience rather than a key.
|
|
128
|
+
*/
|
|
129
|
+
threadId?: string;
|
|
130
|
+
respondsTo: string;
|
|
131
|
+
decision?: 'allow' | 'deny';
|
|
132
|
+
scope?: 'once' | 'session';
|
|
133
|
+
reason?: string;
|
|
134
|
+
/** Untrusted. `validateHumanResponse` is the only reader. */
|
|
135
|
+
answers?: unknown;
|
|
136
|
+
cancelled?: boolean;
|
|
137
|
+
planDecision?: unknown;
|
|
138
|
+
feedback?: string;
|
|
139
|
+
}
|
|
140
|
+
export interface BridgeToolResultFrame {
|
|
141
|
+
type: 'tool_result';
|
|
142
|
+
id?: string;
|
|
143
|
+
threadId?: string;
|
|
144
|
+
respondsTo: string;
|
|
145
|
+
/** A string is one text part; an array is a content-block list. */
|
|
146
|
+
content: unknown;
|
|
147
|
+
isError?: boolean;
|
|
148
|
+
}
|
|
149
|
+
export interface BridgeToolRegisterFrame {
|
|
150
|
+
type: 'tool_register';
|
|
151
|
+
id?: string;
|
|
152
|
+
threadId?: string;
|
|
153
|
+
/** Untrusted; `validateCallerTools` narrows it. */
|
|
154
|
+
tools: unknown[];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Open one more conversation (section 2.3.2).
|
|
158
|
+
*
|
|
159
|
+
* ACCEPTED ONLY IN THREAD MODE. Without `--threads` the answer is
|
|
160
|
+
* `ack{accepted:false, code:'threads_disabled'}` (AC-2), never silence: a wrapper that
|
|
161
|
+
* believes it opened a thread and gets nothing would send prompts into `main` and
|
|
162
|
+
* receive answers to a conversation it never asked for.
|
|
163
|
+
*/
|
|
164
|
+
export interface BridgeThreadOpenFrame {
|
|
165
|
+
type: 'thread_open';
|
|
166
|
+
id?: string;
|
|
167
|
+
/** `[a-z0-9][a-z0-9_-]{0,31}`; `resolveThreadIdentity` is the only validator. */
|
|
168
|
+
threadId: string;
|
|
169
|
+
/** Explicit session id; absent means `${base}-${threadId}`. */
|
|
170
|
+
sessionId?: string;
|
|
171
|
+
}
|
|
172
|
+
export interface BridgeThreadCloseFrame {
|
|
173
|
+
type: 'thread_close';
|
|
174
|
+
id?: string;
|
|
175
|
+
threadId: string;
|
|
176
|
+
/** `settle` (default) waits for the request in flight; `abort` cancels it. */
|
|
177
|
+
mode?: 'settle' | 'abort';
|
|
178
|
+
}
|
|
179
|
+
export interface BridgeThreadListFrame {
|
|
180
|
+
type: 'thread_list';
|
|
181
|
+
id?: string;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Register lifecycle hooks (section 2.4.2).
|
|
185
|
+
*
|
|
186
|
+
* `hooks` IS `unknown[]` ON PURPOSE: the frame comes off a pipe, and `validateHookSpecs`
|
|
187
|
+
* is the only reader. Typing it here would be a lie TypeScript happily propagates into
|
|
188
|
+
* a timeout table.
|
|
189
|
+
*/
|
|
190
|
+
export interface BridgeHookRegisterFrame {
|
|
191
|
+
type: 'hook_register';
|
|
192
|
+
id?: string;
|
|
193
|
+
threadId?: string;
|
|
194
|
+
hooks: unknown[];
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The caller's answer to one `hook_request`.
|
|
198
|
+
*
|
|
199
|
+
* ONE FRAME FOR FOUR POINTS, discriminated by `action` - the same shape `decision`
|
|
200
|
+
* already uses for its four kinds. The point is not repeated because the CLI already
|
|
201
|
+
* knows which hook id it asked with.
|
|
202
|
+
*/
|
|
203
|
+
export interface BridgeHookAnswerFrame {
|
|
204
|
+
type: 'hook_answer';
|
|
205
|
+
id?: string;
|
|
206
|
+
threadId?: string;
|
|
207
|
+
respondsTo: string;
|
|
208
|
+
/** `allow` | `deny` | `rewrite` | `inject` | `keep` | `replace`; validated by point. */
|
|
209
|
+
action: string;
|
|
210
|
+
/** `prompt_pre` rewrite and `turn_start` inject. */
|
|
211
|
+
text?: string;
|
|
212
|
+
/** `tool_pre` rewrite. */
|
|
213
|
+
input?: unknown;
|
|
214
|
+
/** `tool_post` replace. */
|
|
215
|
+
content?: unknown;
|
|
216
|
+
isError?: boolean;
|
|
217
|
+
reason?: string;
|
|
218
|
+
message?: string;
|
|
219
|
+
}
|
|
220
|
+
export interface BridgePingFrame {
|
|
221
|
+
type: 'ping';
|
|
222
|
+
id?: string;
|
|
223
|
+
}
|
|
224
|
+
export interface BridgeEndFrame {
|
|
225
|
+
type: 'end';
|
|
226
|
+
}
|
|
227
|
+
/** A `type` this build does not know. Parsed successfully, then ignored. */
|
|
228
|
+
export interface BridgeIgnoredFrame {
|
|
229
|
+
type: 'ignored';
|
|
230
|
+
rawType: string;
|
|
231
|
+
}
|
|
232
|
+
export type BridgeInputFrame = BridgePromptFrame | BridgeSteerFrame | BridgeInterruptFrame | BridgeCancelRequestFrame | BridgeDecisionFrame | BridgeToolResultFrame | BridgeToolRegisterFrame | BridgeThreadOpenFrame | BridgeThreadCloseFrame | BridgeThreadListFrame | BridgeHookRegisterFrame | BridgeHookAnswerFrame | BridgePingFrame | BridgeEndFrame | BridgeIgnoredFrame;
|
|
233
|
+
export interface BridgeParseOk {
|
|
234
|
+
ok: true;
|
|
235
|
+
frame: BridgeInputFrame;
|
|
236
|
+
}
|
|
237
|
+
export interface BridgeParseFailure {
|
|
238
|
+
ok: false;
|
|
239
|
+
/** A stable `error.code` value; see the event contract in `events.ts`. */
|
|
240
|
+
code: string;
|
|
241
|
+
message: string;
|
|
242
|
+
}
|
|
243
|
+
export type BridgeParseResult = BridgeParseOk | BridgeParseFailure;
|
|
244
|
+
export declare const BRIDGE_FRAME_INVALID = "bridge_frame_invalid";
|
|
245
|
+
export declare const BRIDGE_FRAME_TOO_LARGE = "bridge_frame_too_large";
|
|
246
|
+
/** The `tool` payload of a `decision_request{kind:'tool'}`. */
|
|
247
|
+
export interface DecisionToolPayload {
|
|
248
|
+
name: string;
|
|
249
|
+
callId: string;
|
|
250
|
+
input: Record<string, unknown>;
|
|
251
|
+
/** One readable line, e.g. `Run: npm test`. Reuses `summarize()`. */
|
|
252
|
+
summary: string;
|
|
253
|
+
turn: number;
|
|
254
|
+
}
|
|
255
|
+
export interface DecisionInstallPayload {
|
|
256
|
+
tool: string;
|
|
257
|
+
summary: string;
|
|
258
|
+
}
|
|
259
|
+
/** What one `requestDecision` call is asking about. */
|
|
260
|
+
export type DecisionAsk = {
|
|
261
|
+
kind: 'tool';
|
|
262
|
+
tool: DecisionToolPayload;
|
|
263
|
+
agent?: AgentScope;
|
|
264
|
+
} | {
|
|
265
|
+
kind: 'questions';
|
|
266
|
+
questions: NormalizedQuestion[];
|
|
267
|
+
agent?: AgentScope;
|
|
268
|
+
} | {
|
|
269
|
+
kind: 'plan';
|
|
270
|
+
plan: NormalizedPlan;
|
|
271
|
+
agent?: AgentScope;
|
|
272
|
+
} | {
|
|
273
|
+
kind: 'skill_install';
|
|
274
|
+
install: DecisionInstallPayload;
|
|
275
|
+
agent?: AgentScope;
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* The caller's answer, unvalidated. `kind` on the request decides its shape.
|
|
279
|
+
*
|
|
280
|
+
* DELIBERATELY LOOSE. These fields come off a pipe, so they are `unknown` until a
|
|
281
|
+
* validator says otherwise; typing them as `Answer[]` here would be a lie that
|
|
282
|
+
* TypeScript happily propagates into model-visible text.
|
|
283
|
+
*/
|
|
284
|
+
export interface DecisionAnswer {
|
|
285
|
+
decision?: 'allow' | 'deny';
|
|
286
|
+
scope?: 'once' | 'session';
|
|
287
|
+
reason?: string;
|
|
288
|
+
answers?: unknown;
|
|
289
|
+
cancelled?: boolean;
|
|
290
|
+
planDecision?: unknown;
|
|
291
|
+
feedback?: string;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* What `requestDecision` resolves with. IT NEVER REJECTS, AND EVERY NON-ANSWER IS
|
|
295
|
+
* ITS OWN MEMBER (review P1-6).
|
|
296
|
+
*
|
|
297
|
+
* A boolean would have collapsed four different situations - answered, timed out,
|
|
298
|
+
* the turn was aborted, the channel closed - into "not allowed", and the gate
|
|
299
|
+
* that has to write model-visible text cannot tell them apart afterwards. The
|
|
300
|
+
* three non-`answered` members also all map to `null` on the `HumanInputGate`
|
|
301
|
+
* path, because its contract is "resolves null on cancel" rather than "invent an
|
|
302
|
+
* empty answer".
|
|
303
|
+
*/
|
|
304
|
+
export type DecisionOutcome = {
|
|
305
|
+
outcome: 'answered';
|
|
306
|
+
answer: DecisionAnswer;
|
|
307
|
+
} | {
|
|
308
|
+
outcome: 'timeout';
|
|
309
|
+
deadlineAt: number;
|
|
310
|
+
} | {
|
|
311
|
+
outcome: 'aborted';
|
|
312
|
+
} | {
|
|
313
|
+
outcome: 'closed';
|
|
314
|
+
reason: string;
|
|
315
|
+
};
|
|
316
|
+
/** A model-driven call into a caller tool. */
|
|
317
|
+
export interface CallerToolCall {
|
|
318
|
+
callId: string;
|
|
319
|
+
turn: number;
|
|
320
|
+
tool: string;
|
|
321
|
+
input: Record<string, unknown>;
|
|
322
|
+
/** The EFFECTIVE ceiling, after `min(spec, --caller-tool-timeout)`. */
|
|
323
|
+
timeoutMs: number;
|
|
324
|
+
agent: AgentScope;
|
|
325
|
+
}
|
|
326
|
+
/** A caller tool's answer, before truncation and content checks. */
|
|
327
|
+
export interface CallerToolAnswer {
|
|
328
|
+
content: unknown;
|
|
329
|
+
isError?: boolean;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Turn one stdin line into a frame, or into a stable failure code.
|
|
333
|
+
*
|
|
334
|
+
* THE OVER-LONG CHECK IS HERE AS WELL AS IN THE SPLITTER, on purpose: the
|
|
335
|
+
* splitter protects memory, this protects the contract, and a caller that raises
|
|
336
|
+
* `maxFrameBytes` for a test still gets the same refusal from the parser.
|
|
337
|
+
*/
|
|
338
|
+
export declare function parseBridgeInput(line: string): BridgeParseResult;
|
|
339
|
+
/**
|
|
340
|
+
* Map a `HumanInputGate` request onto the frame that asks the caller.
|
|
341
|
+
*
|
|
342
|
+
* THE CLI IS THE AUTHORITY ON THE QUESTIONS, and this function is where that is
|
|
343
|
+
* visible: it copies the normalized question set the model produced, so the
|
|
344
|
+
* caller renders what was actually asked rather than inventing its own text.
|
|
345
|
+
*/
|
|
346
|
+
export declare function humanDecisionFrame(req: HumanRequest): DecisionAsk;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { BRIDGE_LIMITS } from './limits.js';
|
|
2
|
+
export const BRIDGE_PROTOCOL_VERSION = 2;
|
|
3
|
+
/**
|
|
4
|
+
* What this build announces in `system/init.capabilities` when bridge is on.
|
|
5
|
+
*
|
|
6
|
+
* APPEND ONLY, NEVER REORDER, same discipline as `EXEC_CAPABILITIES`: the caller
|
|
7
|
+
* asks `includes('caller_tools')` and ignores what it does not recognise.
|
|
8
|
+
*/
|
|
9
|
+
export const BRIDGE_CAPABILITIES = [
|
|
10
|
+
'bridge',
|
|
11
|
+
'decision',
|
|
12
|
+
'caller_tools',
|
|
13
|
+
'steer',
|
|
14
|
+
'ping',
|
|
15
|
+
// Per-request budgets, accounting, cancellation and `request_end`
|
|
16
|
+
// (headless-request-scope-and-typed-result section 4.2). APPENDED, and the protocol
|
|
17
|
+
// version stays 2: `protocolVersion` only ever moves for an INCOMPATIBLE shape
|
|
18
|
+
// change, and every frame this capability promises is additive.
|
|
19
|
+
'request_scope',
|
|
20
|
+
];
|
|
21
|
+
export const LEAD_SCOPE = { id: 'lead', label: 'lead', depth: 0 };
|
|
22
|
+
export const BRIDGE_FRAME_INVALID = 'bridge_frame_invalid';
|
|
23
|
+
export const BRIDGE_FRAME_TOO_LARGE = 'bridge_frame_too_large';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Parsing
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function invalid(message) {
|
|
28
|
+
return { ok: false, code: BRIDGE_FRAME_INVALID, message };
|
|
29
|
+
}
|
|
30
|
+
function optionalString(value) {
|
|
31
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Turn one stdin line into a frame, or into a stable failure code.
|
|
35
|
+
*
|
|
36
|
+
* THE OVER-LONG CHECK IS HERE AS WELL AS IN THE SPLITTER, on purpose: the
|
|
37
|
+
* splitter protects memory, this protects the contract, and a caller that raises
|
|
38
|
+
* `maxFrameBytes` for a test still gets the same refusal from the parser.
|
|
39
|
+
*/
|
|
40
|
+
export function parseBridgeInput(line) {
|
|
41
|
+
const bytes = Buffer.byteLength(line, 'utf-8');
|
|
42
|
+
if (bytes > BRIDGE_LIMITS.maxFrameBytes) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
code: BRIDGE_FRAME_TOO_LARGE,
|
|
46
|
+
message: `Skipped a bridge frame of ${bytes} bytes; the limit is ${BRIDGE_LIMITS.maxFrameBytes}.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(line);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return invalid(`Skipped a bridge line that is not JSON: ${clip(line)}`);
|
|
55
|
+
}
|
|
56
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
57
|
+
return invalid(`Skipped a bridge line that is not a JSON object: ${clip(line)}`);
|
|
58
|
+
}
|
|
59
|
+
const raw = parsed;
|
|
60
|
+
const type = typeof raw.type === 'string' ? raw.type : '';
|
|
61
|
+
if (type.length === 0)
|
|
62
|
+
return invalid('A bridge frame needs a string "type".');
|
|
63
|
+
// `user` is the v1 spelling of a prompt with no id. Kept because a caller
|
|
64
|
+
// migrated from `--input-format stream-json` should not have to rewrite.
|
|
65
|
+
if (type === 'user')
|
|
66
|
+
return parsePrompt({ ...raw, type: 'prompt' }, raw);
|
|
67
|
+
if (type === 'prompt')
|
|
68
|
+
return parsePrompt(raw, raw);
|
|
69
|
+
if (type === 'steer') {
|
|
70
|
+
const text = typeof raw.text === 'string' ? raw.text : '';
|
|
71
|
+
if (text.trim().length === 0)
|
|
72
|
+
return invalid('A "steer" frame needs a non-empty "text".');
|
|
73
|
+
return okFrame({ type: 'steer', text, ...idOf(raw), ...threadOf(raw) });
|
|
74
|
+
}
|
|
75
|
+
if (type === 'interrupt') {
|
|
76
|
+
const requestId = optionalString(raw.requestId);
|
|
77
|
+
return okFrame({
|
|
78
|
+
type: 'interrupt',
|
|
79
|
+
...idOf(raw),
|
|
80
|
+
...threadOf(raw),
|
|
81
|
+
...(requestId ? { requestId } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (type === 'cancel_request') {
|
|
85
|
+
// EVERY FIELD IS OPTIONAL, so there is nothing to reject: no `requestId` means "the
|
|
86
|
+
// request in flight", which is exactly what a caller that lost its own bookkeeping
|
|
87
|
+
// would send. Whether the target exists is a ROUTING question, answered by the
|
|
88
|
+
// runner with an `ack` - not a parse question.
|
|
89
|
+
const requestId = optionalString(raw.requestId);
|
|
90
|
+
return okFrame({
|
|
91
|
+
type: 'cancel_request',
|
|
92
|
+
...idOf(raw),
|
|
93
|
+
...threadOf(raw),
|
|
94
|
+
...(requestId ? { requestId } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (type === 'decision')
|
|
98
|
+
return parseDecision(raw);
|
|
99
|
+
if (type === 'tool_result')
|
|
100
|
+
return parseToolResult(raw);
|
|
101
|
+
if (type === 'tool_register') {
|
|
102
|
+
if (!Array.isArray(raw.tools))
|
|
103
|
+
return invalid('A "tool_register" frame needs a "tools" array.');
|
|
104
|
+
return okFrame({ type: 'tool_register', tools: raw.tools, ...idOf(raw), ...threadOf(raw) });
|
|
105
|
+
}
|
|
106
|
+
if (type === 'thread_open')
|
|
107
|
+
return parseThreadOpen(raw);
|
|
108
|
+
if (type === 'thread_close')
|
|
109
|
+
return parseThreadClose(raw);
|
|
110
|
+
if (type === 'thread_list')
|
|
111
|
+
return okFrame({ type: 'thread_list', ...idOf(raw) });
|
|
112
|
+
if (type === 'hook_register') {
|
|
113
|
+
if (!Array.isArray(raw.hooks))
|
|
114
|
+
return invalid('A "hook_register" frame needs a "hooks" array.');
|
|
115
|
+
return okFrame({ type: 'hook_register', hooks: raw.hooks, ...idOf(raw), ...threadOf(raw) });
|
|
116
|
+
}
|
|
117
|
+
if (type === 'hook_answer')
|
|
118
|
+
return parseHookAnswer(raw);
|
|
119
|
+
if (type === 'ping')
|
|
120
|
+
return okFrame({ type: 'ping', ...idOf(raw) });
|
|
121
|
+
if (type === 'end')
|
|
122
|
+
return okFrame({ type: 'end' });
|
|
123
|
+
// FORWARD COMPATIBILITY, the one case that is silent by design.
|
|
124
|
+
return okFrame({ type: 'ignored', rawType: type });
|
|
125
|
+
}
|
|
126
|
+
function okFrame(frame) {
|
|
127
|
+
return { ok: true, frame };
|
|
128
|
+
}
|
|
129
|
+
function idOf(raw) {
|
|
130
|
+
const id = optionalString(raw.id);
|
|
131
|
+
return id ? { id } : {};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* `threadId` is only ever the ROUTE, never validated here.
|
|
135
|
+
*
|
|
136
|
+
* Whether an id names an open thread is a registry question with an `ack` for an
|
|
137
|
+
* answer; whether it is well FORMED is `resolveThreadIdentity`'s question, and it
|
|
138
|
+
* produces the `thread_id_invalid` refusal. A parser that also judged the shape would
|
|
139
|
+
* give the same input two different refusals depending on which layer saw it first.
|
|
140
|
+
*/
|
|
141
|
+
function threadOf(raw) {
|
|
142
|
+
const threadId = optionalString(raw.threadId);
|
|
143
|
+
return threadId ? { threadId } : {};
|
|
144
|
+
}
|
|
145
|
+
function parseThreadOpen(raw) {
|
|
146
|
+
const threadId = optionalString(raw.threadId);
|
|
147
|
+
if (!threadId)
|
|
148
|
+
return invalid('A "thread_open" frame needs a "threadId".');
|
|
149
|
+
const sessionId = optionalString(raw.sessionId);
|
|
150
|
+
return okFrame({
|
|
151
|
+
type: 'thread_open',
|
|
152
|
+
threadId,
|
|
153
|
+
...idOf(raw),
|
|
154
|
+
...(sessionId ? { sessionId } : {}),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function parseThreadClose(raw) {
|
|
158
|
+
const threadId = optionalString(raw.threadId);
|
|
159
|
+
if (!threadId)
|
|
160
|
+
return invalid('A "thread_close" frame needs a "threadId".');
|
|
161
|
+
// AN UNKNOWN MODE IS A PARSE ERROR, NOT A DEFAULT (AC-31 / AC-32). Coercing `"ABORT"` or
|
|
162
|
+
// a typo to `settle` would answer "wait for the request in flight" to a caller that asked
|
|
163
|
+
// to cancel it - the two behaviours differ in whether the work happens at all.
|
|
164
|
+
if (raw.mode !== undefined && raw.mode !== 'abort' && raw.mode !== 'settle') {
|
|
165
|
+
return invalid('A "thread_close" mode must be "settle" or "abort".');
|
|
166
|
+
}
|
|
167
|
+
return okFrame({
|
|
168
|
+
type: 'thread_close',
|
|
169
|
+
threadId,
|
|
170
|
+
...idOf(raw),
|
|
171
|
+
...(raw.mode === 'abort' || raw.mode === 'settle' ? { mode: raw.mode } : {}),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function parseHookAnswer(raw) {
|
|
175
|
+
const respondsTo = optionalString(raw.respondsTo);
|
|
176
|
+
if (!respondsTo)
|
|
177
|
+
return invalid('A "hook_answer" frame needs "respondsTo".');
|
|
178
|
+
const action = optionalString(raw.action);
|
|
179
|
+
if (!action)
|
|
180
|
+
return invalid('A "hook_answer" frame needs an "action".');
|
|
181
|
+
return okFrame({
|
|
182
|
+
type: 'hook_answer',
|
|
183
|
+
respondsTo,
|
|
184
|
+
action,
|
|
185
|
+
...idOf(raw),
|
|
186
|
+
...threadOf(raw),
|
|
187
|
+
...(typeof raw.text === 'string' ? { text: raw.text } : {}),
|
|
188
|
+
...(raw.input !== undefined ? { input: raw.input } : {}),
|
|
189
|
+
...(raw.content !== undefined ? { content: raw.content } : {}),
|
|
190
|
+
...(raw.isError === true ? { isError: true } : {}),
|
|
191
|
+
...(typeof raw.reason === 'string' ? { reason: raw.reason } : {}),
|
|
192
|
+
...(typeof raw.message === 'string' ? { message: raw.message } : {}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function parsePrompt(raw, idSource) {
|
|
196
|
+
const text = typeof raw.text === 'string' ? raw.text : raw.content;
|
|
197
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
198
|
+
return invalid('A "prompt" frame needs a non-empty "text".');
|
|
199
|
+
}
|
|
200
|
+
return okFrame({ type: 'prompt', text, ...idOf(idSource), ...threadOf(raw) });
|
|
201
|
+
}
|
|
202
|
+
function parseDecision(raw) {
|
|
203
|
+
const respondsTo = optionalString(raw.respondsTo);
|
|
204
|
+
if (!respondsTo)
|
|
205
|
+
return invalid('A "decision" frame needs "respondsTo".');
|
|
206
|
+
const frame = { type: 'decision', respondsTo, ...idOf(raw), ...threadOf(raw) };
|
|
207
|
+
if (raw.decision === 'allow' || raw.decision === 'deny')
|
|
208
|
+
frame.decision = raw.decision;
|
|
209
|
+
if (raw.scope === 'once' || raw.scope === 'session')
|
|
210
|
+
frame.scope = raw.scope;
|
|
211
|
+
if (typeof raw.reason === 'string')
|
|
212
|
+
frame.reason = raw.reason;
|
|
213
|
+
if (typeof raw.feedback === 'string')
|
|
214
|
+
frame.feedback = raw.feedback;
|
|
215
|
+
if (raw.cancelled === true)
|
|
216
|
+
frame.cancelled = true;
|
|
217
|
+
if (raw.answers !== undefined)
|
|
218
|
+
frame.answers = raw.answers;
|
|
219
|
+
if (raw.planDecision !== undefined)
|
|
220
|
+
frame.planDecision = raw.planDecision;
|
|
221
|
+
return okFrame(frame);
|
|
222
|
+
}
|
|
223
|
+
function parseToolResult(raw) {
|
|
224
|
+
const respondsTo = optionalString(raw.respondsTo);
|
|
225
|
+
if (!respondsTo)
|
|
226
|
+
return invalid('A "tool_result" frame needs "respondsTo".');
|
|
227
|
+
if (raw.content === undefined)
|
|
228
|
+
return invalid('A "tool_result" frame needs "content".');
|
|
229
|
+
return okFrame({
|
|
230
|
+
type: 'tool_result',
|
|
231
|
+
respondsTo,
|
|
232
|
+
content: raw.content,
|
|
233
|
+
...(raw.isError === true ? { isError: true } : {}),
|
|
234
|
+
...idOf(raw),
|
|
235
|
+
...threadOf(raw),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Keep an error message readable without dumping a megabyte into the event. */
|
|
239
|
+
function clip(text) {
|
|
240
|
+
return text.length <= 120 ? text : `${text.slice(0, 117)}...`;
|
|
241
|
+
}
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Outbound builders
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
/**
|
|
246
|
+
* Map a `HumanInputGate` request onto the frame that asks the caller.
|
|
247
|
+
*
|
|
248
|
+
* THE CLI IS THE AUTHORITY ON THE QUESTIONS, and this function is where that is
|
|
249
|
+
* visible: it copies the normalized question set the model produced, so the
|
|
250
|
+
* caller renders what was actually asked rather than inventing its own text.
|
|
251
|
+
*/
|
|
252
|
+
export function humanDecisionFrame(req) {
|
|
253
|
+
return req.kind === 'questions'
|
|
254
|
+
? { kind: 'questions', questions: req.questions }
|
|
255
|
+
: { kind: 'plan', plan: req.plan };
|
|
256
|
+
}
|