@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/types/async/job-manager.d.ts +25 -0
  3. package/dist/types/commit/model-selection.d.ts +1 -1
  4. package/dist/types/config/model-registry.d.ts +3 -1
  5. package/dist/types/config/model-resolver.d.ts +1 -19
  6. package/dist/types/config/models-config-schema.d.ts +12 -0
  7. package/dist/types/config/settings-schema.d.ts +15 -1
  8. package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
  9. package/dist/types/harness-control-plane/types.d.ts +7 -2
  10. package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
  11. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  12. package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
  13. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
  14. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
  15. package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
  16. package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
  17. package/dist/types/session/agent-session.d.ts +12 -1
  18. package/dist/types/session/session-manager.d.ts +1 -1
  19. package/dist/types/tools/bash.d.ts +2 -0
  20. package/dist/types/tools/browser/actions.d.ts +54 -0
  21. package/dist/types/tools/browser.d.ts +80 -0
  22. package/dist/types/tools/image-gen.d.ts +1 -0
  23. package/dist/types/tools/index.d.ts +3 -1
  24. package/dist/types/tools/job.d.ts +1 -1
  25. package/package.json +7 -7
  26. package/src/async/job-manager.ts +120 -1
  27. package/src/commands/ultragoal.ts +7 -1
  28. package/src/commit/agentic/index.ts +2 -2
  29. package/src/commit/model-selection.ts +7 -22
  30. package/src/commit/pipeline.ts +2 -2
  31. package/src/config/model-registry.ts +17 -9
  32. package/src/config/model-resolver.ts +14 -84
  33. package/src/config/models-config-schema.ts +2 -0
  34. package/src/config/settings-schema.ts +14 -1
  35. package/src/gjc-runtime/goal-mode-request.ts +21 -1
  36. package/src/harness-control-plane/owner.ts +3 -3
  37. package/src/harness-control-plane/rpc-adapter.ts +7 -1
  38. package/src/harness-control-plane/types.ts +8 -11
  39. package/src/internal-urls/docs-index.generated.ts +3 -3
  40. package/src/memories/index.ts +1 -1
  41. package/src/modes/acp/acp-agent.ts +17 -9
  42. package/src/modes/acp/acp-event-mapper.ts +33 -1
  43. package/src/modes/components/custom-editor.ts +19 -3
  44. package/src/modes/controllers/input-controller.ts +27 -7
  45. package/src/modes/controllers/selector-controller.ts +7 -1
  46. package/src/modes/interactive-mode.ts +3 -1
  47. package/src/modes/rpc/rpc-client.ts +16 -3
  48. package/src/modes/rpc/rpc-mode.ts +5 -2
  49. package/src/modes/shared/agent-wire/command-contract.ts +18 -0
  50. package/src/modes/shared/agent-wire/event-contract.ts +147 -0
  51. package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
  52. package/src/modes/shared/agent-wire/event-observation.ts +397 -0
  53. package/src/modes/shared/agent-wire/protocol.ts +24 -81
  54. package/src/modes/utils/context-usage.ts +2 -2
  55. package/src/prompts/agents/explore.md +1 -1
  56. package/src/prompts/agents/plan.md +1 -1
  57. package/src/prompts/agents/reviewer.md +1 -1
  58. package/src/prompts/tools/browser.md +3 -2
  59. package/src/runtime-mcp/manager.ts +15 -2
  60. package/src/sdk.ts +3 -1
  61. package/src/session/agent-session.ts +60 -4
  62. package/src/session/session-manager.ts +1 -1
  63. package/src/task/agents.ts +1 -1
  64. package/src/tools/bash.ts +6 -1
  65. package/src/tools/browser/actions.ts +189 -0
  66. package/src/tools/browser.ts +91 -1
  67. package/src/tools/image-gen.ts +42 -15
  68. package/src/tools/index.ts +7 -1
  69. package/src/tools/inspect-image.ts +10 -8
  70. package/src/tools/job.ts +12 -2
  71. package/src/tools/monitor.ts +98 -17
  72. package/src/utils/commit-message-generator.ts +6 -13
  73. package/src/utils/title-generator.ts +1 -1
  74. package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
  75. package/src/harness-control-plane/frame-mapper.ts +0 -286
  76. package/src/priority.json +0 -37
@@ -388,7 +388,7 @@ async function runPhase2(options: {
388
388
  const phase2Model = await resolveMemoryModel({
389
389
  modelRegistry,
390
390
  session,
391
- fallbackRole: "smol",
391
+ fallbackRole: "default",
392
392
  });
393
393
  if (!phase2Model) {
394
394
  markPhase2FailureWithFallback(db, {
@@ -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
- mapAgentSessionEventToAcpSessionUpdates,
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 mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
1132
- getMessageId: message => this.#getLiveMessageId(record, message),
1133
- getMessageProgress: message => this.#getLiveMessageProgress(record, message),
1134
- getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
1135
- cwd: record.session.sessionManager.getCwd(),
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 = mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, {
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 [...mapAgentSessionEventToAcpSessionUpdates(startEvent, sessionId, { cwd }), ...notifications];
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
- default:
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") && this.onEscape) {
295
+ if (this.#matchesAction(data, "app.interrupt")) {
289
296
  if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
290
- this.onEscape();
291
- return;
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]). Enter steer (matches the
293
- // free-text Enter semantics applied a few lines below at the streaming
294
- // branch). Ctrl+Enter routes through `handleFollowUp` and dispatches the
295
- // same helper with `"followUp"`.
296
- if (await this.#invokeSkillCommand(text, "steer")) {
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 steer behavior
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: "steer", images }),
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 (value === "auto" || value === "openai" || value === "gemini" || value === "openrouter") {
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 ? await consumePendingGoalModeRequest(this.sessionManager.getCwd()) : null;
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
- if (!isAgentEvent(data)) return;
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(data);
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 JSON
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 bridge wire frames.
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
- type AgentSessionEventType,
12
- BRIDGE_PROTOCOL_VERSION,
13
- type BridgeEventFrame,
14
- type BridgeFrameEnvelope,
15
- type BridgeFrameType,
16
- } from "./protocol";
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): AgentSessionEventType {
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 BridgeFrameSequencer {
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 BridgeFrameType, TPayload>(
85
+ next<TType extends AgentWireFrameType, TPayload>(
82
86
  type: TType,
83
87
  payload: TPayload,
84
88
  correlationId?: string,
85
- ): BridgeFrameEnvelope<TType, TPayload> {
89
+ ): AgentWireFrameEnvelope<TType, TPayload> {
86
90
  this.#seq += 1;
87
- const frame: BridgeFrameEnvelope<TType, TPayload> = {
88
- protocol_version: BRIDGE_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
- /** Serialize a single `AgentSessionEvent` into an `event` wire frame. */
103
- export function toBridgeEventFrame(event: AgentSessionEvent, sequencer: BridgeFrameSequencer): BridgeEventFrame {
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: BridgeFrameSequencer,
137
+ sequencer: AgentWireFrameSequencer,
119
138
  ): import("./protocol").BridgeWorkflowGateFrame {
120
139
  return sequencer.next("workflow_gate", gate, gate.gate_id);
121
140
  }