@gajae-code/coding-agent 0.4.2 → 0.4.3
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 +11 -0
- package/dist/types/async/job-manager.d.ts +25 -0
- package/dist/types/commit/model-selection.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -19
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +15 -1
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
- package/dist/types/harness-control-plane/types.d.ts +7 -2
- package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
- package/dist/types/session/agent-session.d.ts +12 -1
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/actions.d.ts +54 -0
- package/dist/types/tools/browser.d.ts +80 -0
- package/dist/types/tools/image-gen.d.ts +1 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +120 -1
- package/src/commands/ultragoal.ts +7 -1
- package/src/commit/agentic/index.ts +2 -2
- package/src/commit/model-selection.ts +7 -22
- package/src/commit/pipeline.ts +2 -2
- package/src/config/model-registry.ts +17 -9
- package/src/config/model-resolver.ts +14 -84
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +14 -1
- package/src/gjc-runtime/goal-mode-request.ts +21 -1
- package/src/harness-control-plane/owner.ts +3 -3
- package/src/harness-control-plane/rpc-adapter.ts +7 -1
- package/src/harness-control-plane/types.ts +8 -11
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-agent.ts +17 -9
- package/src/modes/acp/acp-event-mapper.ts +33 -1
- package/src/modes/components/custom-editor.ts +19 -3
- package/src/modes/controllers/input-controller.ts +27 -7
- package/src/modes/controllers/selector-controller.ts +7 -1
- package/src/modes/interactive-mode.ts +3 -1
- package/src/modes/rpc/rpc-client.ts +16 -3
- package/src/modes/rpc/rpc-mode.ts +5 -2
- package/src/modes/shared/agent-wire/command-contract.ts +18 -0
- package/src/modes/shared/agent-wire/event-contract.ts +147 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
- package/src/modes/shared/agent-wire/event-observation.ts +397 -0
- package/src/modes/shared/agent-wire/protocol.ts +24 -81
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/plan.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/tools/browser.md +3 -2
- package/src/runtime-mcp/manager.ts +15 -2
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +60 -4
- package/src/session/session-manager.ts +1 -1
- package/src/task/agents.ts +1 -1
- package/src/tools/bash.ts +6 -1
- package/src/tools/browser/actions.ts +189 -0
- package/src/tools/browser.ts +91 -1
- package/src/tools/image-gen.ts +42 -15
- package/src/tools/index.ts +7 -1
- package/src/tools/inspect-image.ts +10 -8
- package/src/tools/job.ts +12 -2
- package/src/tools/monitor.ts +98 -17
- package/src/utils/commit-message-generator.ts +6 -13
- package/src/utils/title-generator.ts +1 -1
- package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
- package/src/harness-control-plane/frame-mapper.ts +0 -286
- package/src/priority.json +0 -37
package/src/memories/index.ts
CHANGED
|
@@ -72,10 +72,11 @@ import {
|
|
|
72
72
|
} from "../../session/session-manager";
|
|
73
73
|
import { ACP_BUILTIN_SLASH_COMMANDS, executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
|
74
74
|
import { parseThinkingLevel } from "../../thinking";
|
|
75
|
+
import { toAgentWireEventPayload } from "../shared/agent-wire/event-envelope";
|
|
75
76
|
import { createAcpClientBridge } from "./acp-client-bridge";
|
|
76
77
|
import {
|
|
77
78
|
buildToolCallStartUpdate,
|
|
78
|
-
|
|
79
|
+
mapAgentWireEventPayloadToAcpSessionUpdates,
|
|
79
80
|
normalizeReplayToolArguments,
|
|
80
81
|
} from "./acp-event-mapper";
|
|
81
82
|
import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
|
|
@@ -1128,12 +1129,16 @@ export class AcpAgent implements Agent {
|
|
|
1128
1129
|
}
|
|
1129
1130
|
|
|
1130
1131
|
this.#prepareLiveAssistantMessage(record, event);
|
|
1131
|
-
for (const notification of
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1132
|
+
for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(
|
|
1133
|
+
toAgentWireEventPayload(event),
|
|
1134
|
+
record.session.sessionId,
|
|
1135
|
+
{
|
|
1136
|
+
getMessageId: message => this.#getLiveMessageId(record, message),
|
|
1137
|
+
getMessageProgress: message => this.#getLiveMessageProgress(record, message),
|
|
1138
|
+
getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
|
|
1139
|
+
cwd: record.session.sessionManager.getCwd(),
|
|
1140
|
+
},
|
|
1141
|
+
)) {
|
|
1137
1142
|
await this.#connection.sessionUpdate(notification);
|
|
1138
1143
|
}
|
|
1139
1144
|
if (event.type === "tool_execution_end") {
|
|
@@ -1887,14 +1892,17 @@ export class AcpAgent implements Agent {
|
|
|
1887
1892
|
errorMessage: message.errorMessage,
|
|
1888
1893
|
},
|
|
1889
1894
|
};
|
|
1890
|
-
const notifications =
|
|
1895
|
+
const notifications = mapAgentWireEventPayloadToAcpSessionUpdates(toAgentWireEventPayload(endEvent), sessionId, {
|
|
1891
1896
|
cwd,
|
|
1892
1897
|
getToolArgs: toolCallId => (toolCallId === message.toolCallId ? options.toolArgs : undefined),
|
|
1893
1898
|
});
|
|
1894
1899
|
if (options.includeStart === false) {
|
|
1895
1900
|
return notifications;
|
|
1896
1901
|
}
|
|
1897
|
-
return [
|
|
1902
|
+
return [
|
|
1903
|
+
...mapAgentWireEventPayloadToAcpSessionUpdates(toAgentWireEventPayload(startEvent), sessionId, { cwd }),
|
|
1904
|
+
...notifications,
|
|
1905
|
+
];
|
|
1898
1906
|
}
|
|
1899
1907
|
|
|
1900
1908
|
#buildReplayToolArgs(details: unknown): { path?: string } {
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
10
10
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
11
11
|
import type { TodoStatus } from "../../tools/todo-write";
|
|
12
|
+
import type { AgentWireEventPayload } from "../shared/agent-wire/event-contract";
|
|
12
13
|
|
|
13
14
|
interface MessageProgress {
|
|
14
15
|
textEmitted: boolean;
|
|
@@ -145,6 +146,14 @@ export function mapToolKind(toolName: string): ToolKind {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
export function mapAgentWireEventPayloadToAcpSessionUpdates(
|
|
150
|
+
payload: AgentWireEventPayload,
|
|
151
|
+
sessionId: string,
|
|
152
|
+
options: AcpEventMapperOptions = {},
|
|
153
|
+
): SessionNotification[] {
|
|
154
|
+
return mapAgentSessionEventToAcpSessionUpdates(payload.event, sessionId, options);
|
|
155
|
+
}
|
|
156
|
+
|
|
148
157
|
export function mapAgentSessionEventToAcpSessionUpdates(
|
|
149
158
|
event: AgentSessionEvent,
|
|
150
159
|
sessionId: string,
|
|
@@ -221,11 +230,34 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
221
230
|
}
|
|
222
231
|
case "todo_auto_clear":
|
|
223
232
|
return [toSessionNotification(sessionId, { sessionUpdate: "plan", entries: [] })];
|
|
224
|
-
|
|
233
|
+
// These event types are intentionally not represented as ACP session updates.
|
|
234
|
+
case "agent_start":
|
|
235
|
+
case "agent_end":
|
|
236
|
+
case "turn_start":
|
|
237
|
+
case "turn_end":
|
|
238
|
+
case "message_start":
|
|
239
|
+
case "auto_compaction_start":
|
|
240
|
+
case "auto_compaction_end":
|
|
241
|
+
case "auto_retry_start":
|
|
242
|
+
case "auto_retry_end":
|
|
243
|
+
case "retry_fallback_applied":
|
|
244
|
+
case "retry_fallback_succeeded":
|
|
245
|
+
case "ttsr_triggered":
|
|
246
|
+
case "irc_message":
|
|
247
|
+
case "notice":
|
|
248
|
+
case "thinking_level_changed":
|
|
249
|
+
case "goal_updated":
|
|
225
250
|
return [];
|
|
251
|
+
default:
|
|
252
|
+
return assertNeverAcp(event);
|
|
226
253
|
}
|
|
227
254
|
}
|
|
228
255
|
|
|
256
|
+
function assertNeverAcp(event: never): SessionNotification[] {
|
|
257
|
+
void (event as AgentSessionEvent);
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
|
|
229
261
|
function mapAssistantMessageUpdate(
|
|
230
262
|
event: Extract<AgentSessionEvent, { type: "message_update" }>,
|
|
231
263
|
sessionId: string,
|
|
@@ -51,6 +51,13 @@ type PastePendingClearReason = "timeout" | "queue-limit";
|
|
|
51
51
|
*/
|
|
52
52
|
export class CustomEditor extends Editor {
|
|
53
53
|
onEscape?: () => void;
|
|
54
|
+
/**
|
|
55
|
+
* Optional high-priority interrupt consumer. Invoked when the interrupt key
|
|
56
|
+
* is pressed, before `onEscape`. Returning `true` consumes the keystroke.
|
|
57
|
+
* Used so a transient UI (e.g. the btw panel) stays dismissable even while
|
|
58
|
+
* another controller has temporarily installed its own `onEscape` handler.
|
|
59
|
+
*/
|
|
60
|
+
onInterruptPriority?: () => boolean;
|
|
54
61
|
shouldBypassAutocompleteOnEscape?: () => boolean;
|
|
55
62
|
onClear?: () => void;
|
|
56
63
|
onExit?: () => void;
|
|
@@ -285,10 +292,19 @@ export class CustomEditor extends Editor {
|
|
|
285
292
|
|
|
286
293
|
// Intercept configured interrupt shortcut.
|
|
287
294
|
// Default behavior keeps autocomplete dismissal, but parent can prioritize global interrupt handling.
|
|
288
|
-
if (this.#matchesAction(data, "app.interrupt")
|
|
295
|
+
if (this.#matchesAction(data, "app.interrupt")) {
|
|
289
296
|
if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
|
|
290
|
-
|
|
291
|
-
|
|
297
|
+
// A priority interrupt consumer (e.g. an open btw panel) must win over any
|
|
298
|
+
// transient onEscape handler other controllers install (auto-compaction,
|
|
299
|
+
// auto-retry, manual compaction, etc.) so dismissal stays wired regardless
|
|
300
|
+
// of which handler currently owns onEscape.
|
|
301
|
+
if (this.onInterruptPriority?.()) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (this.onEscape) {
|
|
305
|
+
this.onEscape();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
292
308
|
}
|
|
293
309
|
}
|
|
294
310
|
|
|
@@ -64,6 +64,11 @@ export class InputController {
|
|
|
64
64
|
this.ctx.autoCompactionEscapeHandler ||
|
|
65
65
|
this.ctx.retryEscapeHandler,
|
|
66
66
|
);
|
|
67
|
+
// An open btw panel must stay dismissable with Esc even while another
|
|
68
|
+
// controller (auto-compaction, auto-retry, manual compaction, etc.) has
|
|
69
|
+
// temporarily replaced editor.onEscape. This priority hook is never
|
|
70
|
+
// swapped out, so it always wins for the interrupt key.
|
|
71
|
+
this.ctx.editor.onInterruptPriority = () => (this.ctx.hasActiveBtw() ? this.ctx.handleBtwEscape() : false);
|
|
67
72
|
this.ctx.editor.onEscape = () => {
|
|
68
73
|
if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
|
|
69
74
|
return;
|
|
@@ -289,11 +294,12 @@ export class InputController {
|
|
|
289
294
|
text = slashResult;
|
|
290
295
|
}
|
|
291
296
|
|
|
292
|
-
// Handle skill commands (/skill:name [args]).
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
+
// Handle skill commands (/skill:name [args]). While streaming, Enter
|
|
298
|
+
// honors `busyPromptMode`: "steer" interrupts the active turn, "queue"
|
|
299
|
+
// runs after it completes (matches the free-text Enter semantics applied
|
|
300
|
+
// a few lines below at the streaming branch). Ctrl+Enter always routes
|
|
301
|
+
// through `handleFollowUp` and dispatches the same helper with `"followUp"`.
|
|
302
|
+
if (await this.#invokeSkillCommand(text, this.#busyStreamingBehavior())) {
|
|
297
303
|
return;
|
|
298
304
|
}
|
|
299
305
|
|
|
@@ -344,7 +350,9 @@ export class InputController {
|
|
|
344
350
|
return;
|
|
345
351
|
}
|
|
346
352
|
|
|
347
|
-
// If streaming, use prompt() with
|
|
353
|
+
// If streaming, use prompt() with the busy-prompt behavior the user
|
|
354
|
+
// selected: "steer" interrupts the active turn, "queue" defers the
|
|
355
|
+
// prompt to run after the active turn completes (in submission order).
|
|
348
356
|
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
|
349
357
|
if (this.ctx.session.isStreaming) {
|
|
350
358
|
this.ctx.editor.addToHistory(text);
|
|
@@ -355,9 +363,10 @@ export class InputController {
|
|
|
355
363
|
// (a user-role `message_start` event) leaves any draft the user has
|
|
356
364
|
// typed since queuing intact. Same protection as #783, applied to
|
|
357
365
|
// the streaming/queue path.
|
|
366
|
+
const streamingBehavior = this.#busyStreamingBehavior();
|
|
358
367
|
await this.ctx.withLocalSubmission(
|
|
359
368
|
text,
|
|
360
|
-
() => this.ctx.session.prompt(text, { streamingBehavior
|
|
369
|
+
() => this.ctx.session.prompt(text, { streamingBehavior, images }),
|
|
361
370
|
{ imageCount: images?.length ?? 0 },
|
|
362
371
|
);
|
|
363
372
|
this.ctx.updatePendingMessagesDisplay();
|
|
@@ -450,6 +459,17 @@ export class InputController {
|
|
|
450
459
|
}
|
|
451
460
|
}
|
|
452
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Resolve how a prompt submitted while the agent is busy should be delivered.
|
|
464
|
+
* Driven by the `busyPromptMode` setting and kept distinct from the
|
|
465
|
+
* follow-up keybinding: "steer" interrupts the active turn, "queue" defers
|
|
466
|
+
* the prompt to the follow-up queue so it runs after the active turn
|
|
467
|
+
* completes (in submission order). Only consulted while streaming.
|
|
468
|
+
*/
|
|
469
|
+
#busyStreamingBehavior(): "steer" | "followUp" {
|
|
470
|
+
return this.ctx.settings.get("busyPromptMode") === "queue" ? "followUp" : "steer";
|
|
471
|
+
}
|
|
472
|
+
|
|
453
473
|
/**
|
|
454
474
|
* Dispatch skill slash invocation(s) (`/skill:<name>`) through custom messages
|
|
455
475
|
* using the supplied `streamingBehavior`. Returns true if the text was a
|
|
@@ -506,7 +506,13 @@ export class SelectorController {
|
|
|
506
506
|
}
|
|
507
507
|
break;
|
|
508
508
|
case "providers.image":
|
|
509
|
-
if (
|
|
509
|
+
if (
|
|
510
|
+
value === "auto" ||
|
|
511
|
+
value === "openai" ||
|
|
512
|
+
value === "gemini" ||
|
|
513
|
+
value === "openrouter" ||
|
|
514
|
+
value === "antigravity"
|
|
515
|
+
) {
|
|
510
516
|
setPreferredImageProvider(value);
|
|
511
517
|
}
|
|
512
518
|
break;
|
|
@@ -1156,7 +1156,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1156
1156
|
this.#updateGoalModeStatus();
|
|
1157
1157
|
return;
|
|
1158
1158
|
}
|
|
1159
|
-
const pendingGoal = goalEnabled
|
|
1159
|
+
const pendingGoal = goalEnabled
|
|
1160
|
+
? await consumePendingGoalModeRequest(this.sessionManager.getCwd(), this.sessionManager.getSessionId())
|
|
1161
|
+
: null;
|
|
1160
1162
|
if (pendingGoal) {
|
|
1161
1163
|
await this.#enterGoalMode({ objective: pendingGoal.objective, silent: true });
|
|
1162
1164
|
this.#scheduleGoalContinuation();
|
|
@@ -150,6 +150,18 @@ function isRpcWorkflowGate(value: unknown): value is RpcWorkflowGate {
|
|
|
150
150
|
);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Unwrap a canonical agent-wire `event` frame `{ type:"event", payload:{ event_type, event } }`
|
|
155
|
+
* to its inner `AgentSessionEvent`. Returns null for any frame that is not a
|
|
156
|
+
* canonical event frame (session events are only delivered wrapped).
|
|
157
|
+
*/
|
|
158
|
+
function unwrapAgentWireEventFrame(value: unknown): unknown {
|
|
159
|
+
if (isRecord(value) && value.type === "event" && isRecord(value.payload)) {
|
|
160
|
+
return value.payload.event;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
153
165
|
function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): AgentToolResult<TDetails> {
|
|
154
166
|
if (typeof result === "string") {
|
|
155
167
|
return {
|
|
@@ -763,11 +775,12 @@ export class RpcClient {
|
|
|
763
775
|
return;
|
|
764
776
|
}
|
|
765
777
|
|
|
766
|
-
|
|
778
|
+
// Canonical agent-wire event frame: { type:"event", payload:{ event_type, event } }.
|
|
779
|
+
const event = unwrapAgentWireEventFrame(data);
|
|
780
|
+
if (!isAgentEvent(event)) return;
|
|
767
781
|
|
|
768
|
-
// Otherwise it's an event
|
|
769
782
|
for (const listener of this.#eventListeners) {
|
|
770
|
-
listener(
|
|
783
|
+
listener(event);
|
|
771
784
|
}
|
|
772
785
|
}
|
|
773
786
|
|
|
@@ -21,6 +21,7 @@ import { type Theme, theme } from "../../modes/theme/theme";
|
|
|
21
21
|
import type { AgentSession } from "../../session/agent-session";
|
|
22
22
|
import { initializeExtensions } from "../runtime-init";
|
|
23
23
|
import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
|
|
24
|
+
import { AgentWireFrameSequencer, toAgentWireEventFrame } from "../shared/agent-wire/event-envelope";
|
|
24
25
|
import { rpcError as error } from "../shared/agent-wire/responses";
|
|
25
26
|
import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
|
|
26
27
|
import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
|
|
@@ -471,9 +472,11 @@ export async function runRpcMode(
|
|
|
471
472
|
uiContext: rpcUiContext,
|
|
472
473
|
});
|
|
473
474
|
|
|
474
|
-
// Output all agent events as
|
|
475
|
+
// Output all agent events as canonical agent-wire `event` frames (docs/rpc.md):
|
|
476
|
+
// { type:"event", protocol_version, session_id, seq, frame_id, payload:{ event_type, event } }.
|
|
477
|
+
const eventSequencer = new AgentWireFrameSequencer(session.sessionId);
|
|
475
478
|
session.subscribe(event => {
|
|
476
|
-
output(event);
|
|
479
|
+
output(toAgentWireEventFrame(event, eventSequencer));
|
|
477
480
|
});
|
|
478
481
|
|
|
479
482
|
// Handle a single command through the shared agent-wire dispatcher so RPC
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical command-surface boundary for the agent-wire adapters.
|
|
3
|
+
*
|
|
4
|
+
* RPC and Bridge SHARE the JSONL `RpcCommand` grammar and dispatch it through
|
|
5
|
+
* the single `dispatchRpcCommand` entry in `command-dispatch.ts`. This module
|
|
6
|
+
* re-exports that command surface so the shared contract has one documented home.
|
|
7
|
+
*
|
|
8
|
+
* ACP does NOT use `RpcCommand`. It keeps its richer `@agentclientprotocol/sdk`
|
|
9
|
+
* command surface (fork/resume/elicitation/session-mode/session-model) and only
|
|
10
|
+
* shares the lower session/event layer (`AgentWireEventPayload`). ACP must never
|
|
11
|
+
* import `dispatchRpcCommand`.
|
|
12
|
+
*
|
|
13
|
+
* Event semantics are intentionally elsewhere: `event-contract.ts` owns the event
|
|
14
|
+
* types + registry and `event-observation.ts` owns the single semantic mapping.
|
|
15
|
+
*/
|
|
16
|
+
export type { RpcCommand, RpcResponse } from "../../rpc/rpc-types";
|
|
17
|
+
export { dispatchRpcCommand, type RpcCommandDispatchContext } from "./command-dispatch";
|
|
18
|
+
export { isRpcCommandType, RPC_COMMAND_TYPES, type RpcCommandType } from "./scopes";
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical agent-wire contract: the single transport-neutral source of truth
|
|
3
|
+
* for AgentSession events and bounded owner observations.
|
|
4
|
+
*
|
|
5
|
+
* Two distinct consumer-facing shapes, deliberately NOT collapsed into one:
|
|
6
|
+
* - `AgentWireEventPayload`: rich, full `AgentSessionEvent` for renderers
|
|
7
|
+
* (ACP SDK notifications, RPC/Bridge event frames).
|
|
8
|
+
* - `AgentWireOwnerObservation`: bounded/redacted owner evidence for control
|
|
9
|
+
* planes (Harness). Never carries assistant text, message deltas, raw tool
|
|
10
|
+
* args, raw command output, raw tool results, answers, or oversize strings.
|
|
11
|
+
*
|
|
12
|
+
* The exhaustive `AGENT_SESSION_EVENT_TYPE_REGISTRY` lives here so that adding
|
|
13
|
+
* an `AgentSessionEvent` variant fails typecheck until it is registered, and so
|
|
14
|
+
* conformance tests can assert fixture coverage equals the registry exactly.
|
|
15
|
+
*/
|
|
16
|
+
import type { AgentSessionEvent } from "../../../session/agent-session";
|
|
17
|
+
|
|
18
|
+
/** Wire protocol version. Bump on breaking envelope/semantic changes. */
|
|
19
|
+
export const AGENT_WIRE_PROTOCOL_VERSION = 2 as const;
|
|
20
|
+
export type AgentWireProtocolVersion = typeof AGENT_WIRE_PROTOCOL_VERSION;
|
|
21
|
+
|
|
22
|
+
/** The discriminant of every `AgentSessionEvent` the agent can emit. */
|
|
23
|
+
export type AgentWireEventType = AgentSessionEvent["type"];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compile-time exhaustive registry of every `AgentSessionEvent` variant. The
|
|
27
|
+
* `Record<AgentWireEventType, true>` shape forces every member to be present:
|
|
28
|
+
* a new union variant is a type error until added here, and a removed variant
|
|
29
|
+
* is a type error until deleted.
|
|
30
|
+
*/
|
|
31
|
+
const AGENT_SESSION_EVENT_TYPE_REGISTRY: Record<AgentWireEventType, true> = {
|
|
32
|
+
agent_start: true,
|
|
33
|
+
agent_end: true,
|
|
34
|
+
turn_start: true,
|
|
35
|
+
turn_end: true,
|
|
36
|
+
message_start: true,
|
|
37
|
+
message_update: true,
|
|
38
|
+
message_end: true,
|
|
39
|
+
tool_execution_start: true,
|
|
40
|
+
tool_execution_update: true,
|
|
41
|
+
tool_execution_end: true,
|
|
42
|
+
auto_compaction_start: true,
|
|
43
|
+
auto_compaction_end: true,
|
|
44
|
+
auto_retry_start: true,
|
|
45
|
+
auto_retry_end: true,
|
|
46
|
+
retry_fallback_applied: true,
|
|
47
|
+
retry_fallback_succeeded: true,
|
|
48
|
+
ttsr_triggered: true,
|
|
49
|
+
todo_reminder: true,
|
|
50
|
+
todo_auto_clear: true,
|
|
51
|
+
irc_message: true,
|
|
52
|
+
notice: true,
|
|
53
|
+
thinking_level_changed: true,
|
|
54
|
+
goal_updated: true,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Every agent-session event type, derived from the exhaustive registry. */
|
|
58
|
+
export const AGENT_WIRE_EVENT_TYPES: readonly AgentWireEventType[] = Object.keys(
|
|
59
|
+
AGENT_SESSION_EVENT_TYPE_REGISTRY,
|
|
60
|
+
) as AgentWireEventType[];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Rich event payload. Carries the full `AgentSessionEvent` so renderers (ACP,
|
|
64
|
+
* RPC, Bridge) can present message content, tool args/results, todo state, etc.
|
|
65
|
+
*/
|
|
66
|
+
export interface AgentWireEventPayload {
|
|
67
|
+
event_type: AgentWireEventType;
|
|
68
|
+
event: AgentSessionEvent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Bounded observed-signal vocabulary surfaced to owner control planes. Mirrors
|
|
73
|
+
* the Harness `ObservedSignal` set; the Harness type aliases this in a later
|
|
74
|
+
* step so there is a single source of truth.
|
|
75
|
+
*/
|
|
76
|
+
export type AgentWireObservedSignal =
|
|
77
|
+
| "SessionStart"
|
|
78
|
+
| "prompt-accepted"
|
|
79
|
+
| "tool-call"
|
|
80
|
+
| "test-running"
|
|
81
|
+
| "commit-created"
|
|
82
|
+
| "completed"
|
|
83
|
+
| "error"
|
|
84
|
+
| "streaming"
|
|
85
|
+
| "idle";
|
|
86
|
+
|
|
87
|
+
export type AgentWireSeverity = "info" | "warn" | "critical";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Bounded, redacted owner observation. Evidence may include ids, names,
|
|
91
|
+
* categories, statuses, cursors, timestamps, short codes, and bounded short
|
|
92
|
+
* messages ONLY. It must never carry assistant text, message deltas, raw tool
|
|
93
|
+
* args, raw command output, raw tool result content, answers, or oversize
|
|
94
|
+
* strings.
|
|
95
|
+
*/
|
|
96
|
+
export interface AgentWireOwnerObservation {
|
|
97
|
+
/** Set when this observation derives from an `AgentSessionEvent`. */
|
|
98
|
+
eventType?: AgentWireEventType;
|
|
99
|
+
/** Set when this observation derives from a non-event wire frame. */
|
|
100
|
+
frameType?: string;
|
|
101
|
+
/** Owner event kind (e.g. `rpc_tool_started`). */
|
|
102
|
+
kind: string;
|
|
103
|
+
/** Bounded observed signal, or null when the frame carries no signal. */
|
|
104
|
+
signal: AgentWireObservedSignal | null;
|
|
105
|
+
/** Bounded evidence — ids/names/statuses/cursors/timestamps/short codes only. */
|
|
106
|
+
evidence: Record<string, unknown>;
|
|
107
|
+
/** Severity for the emitted event. */
|
|
108
|
+
severity: AgentWireSeverity;
|
|
109
|
+
/** Never-drop observations: must be enqueued in order, never coalesced away. */
|
|
110
|
+
semantic: boolean;
|
|
111
|
+
/** Coalescing key for high-frequency non-semantic frames; null otherwise. */
|
|
112
|
+
coalesceKey: string | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Top-level frame categories carried over any agent-wire transport. */
|
|
116
|
+
export type AgentWireFrameType =
|
|
117
|
+
| "ready"
|
|
118
|
+
| "event"
|
|
119
|
+
| "response"
|
|
120
|
+
| "ui_request"
|
|
121
|
+
| "permission_request"
|
|
122
|
+
| "host_tool_call"
|
|
123
|
+
| "host_uri_request"
|
|
124
|
+
| "reset"
|
|
125
|
+
| "workflow_gate"
|
|
126
|
+
| "error";
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Universal frame envelope. Every frame on every transport carries these
|
|
130
|
+
* fields so clients can order (`seq`), resume (`seq` cursor), and correlate
|
|
131
|
+
* request/response pairs (`correlation_id`).
|
|
132
|
+
*/
|
|
133
|
+
export interface AgentWireFrameEnvelope<TType extends AgentWireFrameType = AgentWireFrameType, TPayload = unknown> {
|
|
134
|
+
protocol_version: AgentWireProtocolVersion;
|
|
135
|
+
session_id: string;
|
|
136
|
+
/** Monotonic per-session sequence number, starting at 1. */
|
|
137
|
+
seq: number;
|
|
138
|
+
/** Unique id for this frame. */
|
|
139
|
+
frame_id: string;
|
|
140
|
+
/** Ties a request frame to its response frame, when applicable. */
|
|
141
|
+
correlation_id?: string;
|
|
142
|
+
type: TType;
|
|
143
|
+
payload: TPayload;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** An `AgentSessionEvent` serialized into a versioned wire frame. */
|
|
147
|
+
export type AgentWireEventFrame = AgentWireFrameEnvelope<"event", AgentWireEventPayload>;
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Serialize `AgentSessionEvent`s into versioned
|
|
2
|
+
* Serialize `AgentSessionEvent`s into versioned agent-wire frames.
|
|
3
3
|
*
|
|
4
4
|
* The mapping is intentionally exhaustive: `agentSessionEventType` switches over
|
|
5
5
|
* every variant of the event union and calls `assertNever` in the default arm,
|
|
6
6
|
* so a newly added event variant fails to compile until it is handled here.
|
|
7
|
+
*
|
|
8
|
+
* The canonical sequencer + frame builders live here; the historical `Bridge*`
|
|
9
|
+
* names are retained as thin aliases for callers that have not yet migrated.
|
|
7
10
|
*/
|
|
8
11
|
import { randomUUID } from "node:crypto";
|
|
9
12
|
import type { AgentSessionEvent } from "../../../session/agent-session";
|
|
10
13
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
type
|
|
14
|
-
type
|
|
15
|
-
type
|
|
16
|
-
|
|
14
|
+
AGENT_WIRE_PROTOCOL_VERSION,
|
|
15
|
+
type AgentWireEventFrame,
|
|
16
|
+
type AgentWireEventPayload,
|
|
17
|
+
type AgentWireEventType,
|
|
18
|
+
type AgentWireFrameEnvelope,
|
|
19
|
+
type AgentWireFrameType,
|
|
20
|
+
} from "./event-contract";
|
|
17
21
|
|
|
18
22
|
function assertNever(value: never): never {
|
|
19
23
|
throw new Error(`Unhandled AgentSessionEvent variant: ${JSON.stringify(value)}`);
|
|
@@ -24,7 +28,7 @@ function assertNever(value: never): never {
|
|
|
24
28
|
*
|
|
25
29
|
* Exhaustive over the union; adding a variant without a case is a type error.
|
|
26
30
|
*/
|
|
27
|
-
export function agentSessionEventType(event: AgentSessionEvent):
|
|
31
|
+
export function agentSessionEventType(event: AgentSessionEvent): AgentWireEventType {
|
|
28
32
|
switch (event.type) {
|
|
29
33
|
case "agent_start":
|
|
30
34
|
case "agent_end":
|
|
@@ -59,7 +63,7 @@ export function agentSessionEventType(event: AgentSessionEvent): AgentSessionEve
|
|
|
59
63
|
* Per-session monotonic frame builder. One instance per active session; `seq`
|
|
60
64
|
* starts at 1 and increments per frame so clients can order and resume.
|
|
61
65
|
*/
|
|
62
|
-
export class
|
|
66
|
+
export class AgentWireFrameSequencer {
|
|
63
67
|
readonly #sessionId: string;
|
|
64
68
|
#seq = 0;
|
|
65
69
|
|
|
@@ -78,14 +82,14 @@ export class BridgeFrameSequencer {
|
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
/** Build the next envelope of the given type with a fresh seq + frame id. */
|
|
81
|
-
next<TType extends
|
|
85
|
+
next<TType extends AgentWireFrameType, TPayload>(
|
|
82
86
|
type: TType,
|
|
83
87
|
payload: TPayload,
|
|
84
88
|
correlationId?: string,
|
|
85
|
-
):
|
|
89
|
+
): AgentWireFrameEnvelope<TType, TPayload> {
|
|
86
90
|
this.#seq += 1;
|
|
87
|
-
const frame:
|
|
88
|
-
protocol_version:
|
|
91
|
+
const frame: AgentWireFrameEnvelope<TType, TPayload> = {
|
|
92
|
+
protocol_version: AGENT_WIRE_PROTOCOL_VERSION,
|
|
89
93
|
session_id: this.#sessionId,
|
|
90
94
|
seq: this.#seq,
|
|
91
95
|
frame_id: randomUUID(),
|
|
@@ -99,14 +103,29 @@ export class BridgeFrameSequencer {
|
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
/**
|
|
103
|
-
export
|
|
106
|
+
/** Back-compat alias for {@link AgentWireFrameSequencer}. */
|
|
107
|
+
export const BridgeFrameSequencer = AgentWireFrameSequencer;
|
|
108
|
+
export type BridgeFrameSequencer = AgentWireFrameSequencer;
|
|
109
|
+
|
|
110
|
+
/** Serialize a single `AgentSessionEvent` into a canonical `event` wire frame. */
|
|
111
|
+
export function toAgentWireEventFrame(
|
|
112
|
+
event: AgentSessionEvent,
|
|
113
|
+
sequencer: AgentWireFrameSequencer,
|
|
114
|
+
): AgentWireEventFrame {
|
|
104
115
|
return sequencer.next("event", {
|
|
105
116
|
event_type: agentSessionEventType(event),
|
|
106
117
|
event,
|
|
107
118
|
});
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
/** Build the rich event payload (renderer-facing) for an `AgentSessionEvent`. */
|
|
122
|
+
export function toAgentWireEventPayload(event: AgentSessionEvent): AgentWireEventPayload {
|
|
123
|
+
return { event_type: agentSessionEventType(event), event };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Back-compat alias for {@link toAgentWireEventFrame}. */
|
|
127
|
+
export const toBridgeEventFrame = toAgentWireEventFrame;
|
|
128
|
+
|
|
110
129
|
/**
|
|
111
130
|
* Serialize a `workflow_gate` event into a sequenced wire frame (#321). The
|
|
112
131
|
* gate_id is stamped as the correlation id so the answer (posted to the
|
|
@@ -115,7 +134,7 @@ export function toBridgeEventFrame(event: AgentSessionEvent, sequencer: BridgeFr
|
|
|
115
134
|
*/
|
|
116
135
|
export function toBridgeWorkflowGateFrame(
|
|
117
136
|
gate: import("../../rpc/rpc-types").RpcWorkflowGate,
|
|
118
|
-
sequencer:
|
|
137
|
+
sequencer: AgentWireFrameSequencer,
|
|
119
138
|
): import("./protocol").BridgeWorkflowGateFrame {
|
|
120
139
|
return sequencer.next("workflow_gate", gate, gate.gate_id);
|
|
121
140
|
}
|