@gajae-code/coding-agent 0.8.2 → 0.9.0

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 (179) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/fixture-report.d.ts +20 -0
  5. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  6. package/dist/types/cli/nofile-limit.d.ts +11 -0
  7. package/dist/types/cli/update-cli.d.ts +22 -0
  8. package/dist/types/config/settings-schema.d.ts +105 -1
  9. package/dist/types/discovery/index.d.ts +1 -0
  10. package/dist/types/export/html/template.generated.d.ts +1 -1
  11. package/dist/types/export/ttsr.d.ts +14 -3
  12. package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  15. package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
  16. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
  18. package/dist/types/goals/runtime.d.ts +1 -0
  19. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  20. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  21. package/dist/types/modes/components/status-line.d.ts +9 -0
  22. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  24. package/dist/types/modes/interactive-mode.d.ts +2 -0
  25. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  26. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  27. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  28. package/dist/types/modes/types.d.ts +1 -0
  29. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  30. package/dist/types/notifications/config.d.ts +6 -0
  31. package/dist/types/notifications/telegram-daemon.d.ts +8 -0
  32. package/dist/types/notifications/threaded-render.d.ts +6 -0
  33. package/dist/types/sdk.d.ts +3 -0
  34. package/dist/types/session/agent-session.d.ts +43 -1
  35. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  36. package/dist/types/session/messages.d.ts +4 -0
  37. package/dist/types/session/session-manager.d.ts +17 -1
  38. package/dist/types/skill-state/active-state.d.ts +18 -1
  39. package/dist/types/system-prompt.d.ts +18 -2
  40. package/dist/types/task/executor.d.ts +1 -0
  41. package/dist/types/task/fork-context-budget.d.ts +7 -0
  42. package/dist/types/task/token-log.d.ts +30 -0
  43. package/dist/types/task/token-log.test.d.ts +1 -0
  44. package/dist/types/tools/bisect.d.ts +81 -0
  45. package/dist/types/tools/index.d.ts +1 -0
  46. package/dist/types/tools/search.d.ts +2 -2
  47. package/dist/types/utils/file-mentions.d.ts +14 -0
  48. package/dist/types/utils/git.d.ts +32 -0
  49. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  50. package/package.json +7 -7
  51. package/src/append-only-mode.ts +58 -0
  52. package/src/autoresearch/prompt.md +1 -1
  53. package/src/autoresearch/tools/log-experiment.ts +1 -1
  54. package/src/capability/rule.ts +9 -0
  55. package/src/cli/config-cli.ts +27 -3
  56. package/src/cli/fixture-report.test.ts +230 -0
  57. package/src/cli/fixture-report.ts +171 -0
  58. package/src/cli/nofile-limit.ts +48 -0
  59. package/src/cli/update-cli.ts +85 -9
  60. package/src/cli.ts +26 -0
  61. package/src/commands/contribution-prep.ts +1 -1
  62. package/src/config/settings-schema.ts +68 -1
  63. package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
  64. package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
  65. package/src/defaults/gjc/skills/team/SKILL.md +16 -13
  66. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  67. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  68. package/src/defaults/gjc-defaults.ts +9 -0
  69. package/src/discovery/helpers.ts +10 -0
  70. package/src/discovery/index.ts +1 -0
  71. package/src/export/html/template.generated.ts +1 -1
  72. package/src/export/html/template.js +60 -2
  73. package/src/export/ttsr.ts +96 -18
  74. package/src/extensibility/extensions/runner.ts +181 -219
  75. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  76. package/src/gjc-runtime/deep-interview-state.ts +31 -2
  77. package/src/gjc-runtime/session-state-sidecar.ts +208 -13
  78. package/src/gjc-runtime/state-writer.ts +15 -0
  79. package/src/gjc-runtime/team-runtime.ts +164 -15
  80. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  82. package/src/gjc-runtime/ultragoal-runtime.ts +1593 -143
  83. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  84. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  85. package/src/goals/runtime.ts +10 -2
  86. package/src/hindsight/backend.ts +1 -3
  87. package/src/hooks/native-skill-hook.ts +95 -4
  88. package/src/internal-urls/docs-index.generated.ts +14 -5
  89. package/src/main.ts +43 -2
  90. package/src/memories/index.ts +12 -1
  91. package/src/modes/components/assistant-message.ts +241 -82
  92. package/src/modes/components/custom-editor.ts +1 -0
  93. package/src/modes/components/status-line.ts +167 -54
  94. package/src/modes/components/tool-execution.ts +44 -19
  95. package/src/modes/controllers/command-controller.ts +39 -2
  96. package/src/modes/controllers/event-controller.ts +66 -47
  97. package/src/modes/controllers/input-controller.ts +1 -1
  98. package/src/modes/interactive-mode.ts +9 -1
  99. package/src/modes/rpc/rpc-mode.ts +29 -3
  100. package/src/modes/rpc/rpc-types.ts +9 -0
  101. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  102. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  103. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  104. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  105. package/src/modes/types.ts +1 -0
  106. package/src/modes/utils/ui-helpers.ts +17 -4
  107. package/src/notifications/config.ts +16 -4
  108. package/src/notifications/index.ts +78 -5
  109. package/src/notifications/telegram-daemon-cli.ts +2 -2
  110. package/src/notifications/telegram-daemon-control.ts +2 -2
  111. package/src/notifications/telegram-daemon.ts +85 -8
  112. package/src/notifications/threaded-render.ts +17 -1
  113. package/src/prompts/agents/architect.md +19 -10
  114. package/src/prompts/agents/critic.md +16 -18
  115. package/src/prompts/agents/executor.md +14 -11
  116. package/src/prompts/agents/planner.md +18 -13
  117. package/src/prompts/ci-green-request.md +3 -8
  118. package/src/prompts/goals/goal-continuation.md +0 -5
  119. package/src/prompts/goals/goal-mode-active.md +0 -5
  120. package/src/prompts/system/agent-creation-architect.md +2 -2
  121. package/src/prompts/system/plan-mode-active.md +1 -1
  122. package/src/prompts/system/plan-mode-subagent.md +3 -3
  123. package/src/prompts/system/project-prompt.md +0 -13
  124. package/src/prompts/system/subagent-system-prompt.md +3 -2
  125. package/src/prompts/system/system-prompt.md +26 -19
  126. package/src/prompts/system/volatile-project-context.md +10 -0
  127. package/src/prompts/system/web-search.md +4 -7
  128. package/src/prompts/tools/apply-patch.md +6 -0
  129. package/src/prompts/tools/ast-edit.md +1 -0
  130. package/src/prompts/tools/bash.md +1 -1
  131. package/src/prompts/tools/bisect.md +22 -0
  132. package/src/prompts/tools/browser.md +1 -1
  133. package/src/prompts/tools/cron.md +0 -2
  134. package/src/prompts/tools/eval.md +2 -2
  135. package/src/prompts/tools/find.md +1 -1
  136. package/src/prompts/tools/github.md +6 -6
  137. package/src/prompts/tools/goal.md +1 -3
  138. package/src/prompts/tools/image-gen.md +12 -5
  139. package/src/prompts/tools/irc.md +5 -4
  140. package/src/prompts/tools/job.md +2 -2
  141. package/src/prompts/tools/lsp.md +1 -1
  142. package/src/prompts/tools/monitor.md +2 -2
  143. package/src/prompts/tools/patch.md +2 -2
  144. package/src/prompts/tools/replace.md +2 -21
  145. package/src/prompts/tools/rewind.md +1 -1
  146. package/src/prompts/tools/search-tool-bm25.md +1 -11
  147. package/src/prompts/tools/search.md +1 -1
  148. package/src/prompts/tools/skill.md +3 -2
  149. package/src/prompts/tools/ssh.md +2 -1
  150. package/src/prompts/tools/subagent.md +2 -2
  151. package/src/prompts/tools/task.md +4 -5
  152. package/src/prompts/tools/todo-write.md +1 -1
  153. package/src/prompts/tools/vim.md +1 -0
  154. package/src/prompts/tools/web-search.md +2 -0
  155. package/src/prompts/tools/write.md +8 -2
  156. package/src/sdk.ts +32 -22
  157. package/src/session/agent-session.ts +732 -107
  158. package/src/session/file-mention-pruning.ts +64 -0
  159. package/src/session/messages.ts +4 -0
  160. package/src/session/session-manager.ts +59 -2
  161. package/src/skill-state/active-state.ts +141 -5
  162. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  163. package/src/slash-commands/builtin-registry.ts +65 -0
  164. package/src/system-prompt.ts +57 -16
  165. package/src/task/executor.ts +125 -20
  166. package/src/task/fork-context-advisory.ts +2 -7
  167. package/src/task/fork-context-budget.ts +9 -0
  168. package/src/task/index.ts +4 -3
  169. package/src/task/output-manager.ts +4 -2
  170. package/src/task/token-log.test.ts +188 -0
  171. package/src/task/token-log.ts +179 -0
  172. package/src/tools/bisect.ts +367 -0
  173. package/src/tools/index.ts +3 -0
  174. package/src/tools/output-meta.ts +116 -4
  175. package/src/tools/search.ts +8 -2
  176. package/src/tools/skill.ts +15 -0
  177. package/src/utils/file-mentions.ts +39 -7
  178. package/src/utils/git.ts +73 -0
  179. package/src/web/scrapers/youtube.ts +24 -12
@@ -24,10 +24,20 @@ import type { ResolveToolDetails } from "../../tools/resolve";
24
24
  import { interruptHint } from "../shared";
25
25
  import { buildAbortDisplayMessage } from "../utils/abort-message";
26
26
  import { ringTerminalBell } from "../utils/terminal-bell";
27
+ import { argsWithPartialJson } from "../utils/ui-helpers";
27
28
 
28
29
  type AgentSessionEventKind = AgentSessionEvent["type"];
29
30
 
30
31
  const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
32
+
33
+ /** Test-only performance counters for advisory baseline tests. */
34
+ export const __eventControllerPerfCounters = {
35
+ messageUpdateContentVisits: 0,
36
+ reset(): void {
37
+ this.messageUpdateContentVisits = 0;
38
+ },
39
+ };
40
+
31
41
  const COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS = 10_000;
32
42
 
33
43
  interface CompletionNotifyPayload {
@@ -100,6 +110,8 @@ export class EventController {
100
110
  #lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
101
111
  #idleCompactionTimer?: NodeJS.Timeout;
102
112
  #ircExpiryTimers = new Map<string, NodeJS.Timeout>();
113
+ #toolIntentCache = new Map<string, { args: unknown; intent: string | undefined }>();
114
+ #thinkingContentIndices = new Set<number>();
103
115
  #handlers: AgentSessionEventHandlers;
104
116
 
105
117
  constructor(private ctx: InteractiveModeContext) {
@@ -307,12 +319,14 @@ export class EventController {
307
319
  } else if (event.message.role === "assistant") {
308
320
  this.#lastThinkingCount = 0;
309
321
  this.#resetReadGroup();
322
+ this.#toolIntentCache.clear();
323
+ this.#thinkingContentIndices.clear();
310
324
  this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock, () =>
311
325
  this.ctx.ui.requestRender(),
312
326
  );
313
327
  this.ctx.streamingMessage = event.message;
314
328
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
315
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
329
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
316
330
  this.ctx.ui.requestRender();
317
331
  }
318
332
  }
@@ -375,46 +389,53 @@ export class EventController {
375
389
  async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
376
390
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
377
391
  this.ctx.streamingMessage = event.message;
378
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
379
-
380
- const thinkingCount = this.ctx.streamingMessage.content.filter(
381
- content => content.type === "thinking" && content.thinking.trim(),
382
- ).length;
383
- if (thinkingCount > this.#lastThinkingCount) {
384
- this.#resetReadGroup();
385
- this.#lastThinkingCount = thinkingCount;
392
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
393
+ const contentIndex = event.assistantMessageEvent?.contentIndex;
394
+ const changedContent =
395
+ typeof contentIndex === "number" && contentIndex >= 0
396
+ ? this.ctx.streamingMessage.content[contentIndex]
397
+ : undefined;
398
+ const contentsToProcess = changedContent ? [changedContent] : this.ctx.streamingMessage.content;
399
+
400
+ if (changedContent?.type === "thinking" && changedContent.thinking.trim()) {
401
+ if (typeof contentIndex === "number") this.#thinkingContentIndices.add(contentIndex);
402
+ const thinkingCount = this.#thinkingContentIndices.size;
403
+ if (thinkingCount > this.#lastThinkingCount) {
404
+ this.#resetReadGroup();
405
+ this.#lastThinkingCount = thinkingCount;
406
+ }
407
+ } else if (!changedContent) {
408
+ const thinkingCount = this.ctx.streamingMessage.content.filter(
409
+ content => content.type === "thinking" && content.thinking.trim(),
410
+ ).length;
411
+ if (thinkingCount > this.#lastThinkingCount) {
412
+ this.#resetReadGroup();
413
+ this.#lastThinkingCount = thinkingCount;
414
+ }
386
415
  }
387
416
 
388
- for (const content of this.ctx.streamingMessage.content) {
417
+ for (const content of contentsToProcess) {
418
+ __eventControllerPerfCounters.messageUpdateContentVisits += 1;
389
419
  if (content.type !== "toolCall") continue;
390
420
  if (content.name === "read") {
391
- if (!readArgsHaveTarget(content.arguments)) {
392
- // Args still streaming — defer until path is parseable so we can route to the
393
- // read group (regular files) vs ToolExecutionComponent (internal URLs).
394
- // Creating either component now would lock the read into the wrong shape.
395
- continue;
396
- }
421
+ if (!readArgsHaveTarget(content.arguments)) continue;
397
422
  if (!readArgsTargetInternalUrl(content.arguments)) {
398
423
  this.#trackReadToolCall(content.id, content.arguments);
399
424
  const component = this.ctx.pendingTools.get(content.id);
400
- if (component) {
401
- component.updateArgs(content.arguments, content.id);
402
- } else {
425
+ if (component) component.updateArgs(content.arguments, content.id);
426
+ else {
403
427
  const group = this.#getReadGroup();
404
428
  group.updateArgs(content.arguments, content.id);
405
429
  this.ctx.pendingTools.set(content.id, group);
406
430
  }
407
431
  continue;
408
432
  }
409
- // Internal URL read falls through to ToolExecutionComponent below.
410
433
  }
411
434
 
412
- // Preserve the raw partial JSON for renderers that need to surface fields before the JSON object closes.
413
- // Bash uses this to show inline env assignments during streaming instead of popping them in at completion.
414
- const renderArgs =
415
- "partialJson" in content
416
- ? { ...content.arguments, __partialJson: content.partialJson }
417
- : content.arguments;
435
+ const renderArgs = argsWithPartialJson(
436
+ content.arguments,
437
+ "partialJson" in content ? content.partialJson : undefined,
438
+ );
418
439
  if (!this.ctx.pendingTools.has(content.id)) {
419
440
  this.#resetReadGroup();
420
441
  this.ctx.chatContainer.addChild(new Text("", 0, 0));
@@ -437,32 +458,30 @@ export class EventController {
437
458
  this.ctx.chatContainer.addChild(component);
438
459
  this.ctx.pendingTools.set(content.id, component);
439
460
  } else {
440
- const component = this.ctx.pendingTools.get(content.id);
441
- if (component) {
442
- component.updateArgs(renderArgs, content.id);
443
- }
461
+ this.ctx.pendingTools.get(content.id)?.updateArgs(renderArgs, content.id);
444
462
  }
445
- }
446
463
 
447
- // Update working message with intent from streamed tool arguments
448
- for (const content of this.ctx.streamingMessage.content) {
449
- if (content.type !== "toolCall") continue;
450
464
  const args = content.arguments;
451
465
  if (!args || typeof args !== "object") continue;
466
+ let intent: string | undefined;
452
467
  if (INTENT_FIELD in args) {
453
- this.#updateWorkingMessageFromIntent(args[INTENT_FIELD]);
454
- continue;
455
- }
456
- const tool = this.ctx.session.getToolByName(content.name);
457
- if (typeof tool?.intent !== "function") continue;
458
- try {
459
- const derived = tool.intent(args as never)?.trim();
460
- if (derived) {
461
- this.#updateWorkingMessageFromIntent(derived);
468
+ intent = String(args[INTENT_FIELD]);
469
+ } else {
470
+ const cached = this.#toolIntentCache.get(content.id);
471
+ if (cached?.args === args) intent = cached.intent;
472
+ else {
473
+ const tool = this.ctx.session.getToolByName(content.name);
474
+ if (typeof tool?.intent === "function") {
475
+ try {
476
+ intent = tool.intent(args as never)?.trim();
477
+ } catch {
478
+ // intent function must never break the UI
479
+ }
480
+ }
481
+ this.#toolIntentCache.set(content.id, { args, intent });
462
482
  }
463
- } catch {
464
- // intent function must never break the UI
465
483
  }
484
+ if (intent) this.#updateWorkingMessageFromIntent(intent);
466
485
  }
467
486
 
468
487
  this.ctx.ui.requestRender();
@@ -495,9 +514,9 @@ export class EventController {
495
514
  // display only — does NOT mutate the persisted message's stopReason
496
515
  // (the marker on errorMessage drives replay-side suppression).
497
516
  const msgWithoutAbort = { ...this.ctx.streamingMessage, stopReason: "stop" as const };
498
- this.ctx.streamingComponent.updateContent(msgWithoutAbort);
517
+ this.ctx.streamingComponent.updateContent(msgWithoutAbort, { streaming: false });
499
518
  } else {
500
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
519
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: false });
501
520
  }
502
521
 
503
522
  if (this.ctx.streamingMessage.stopReason !== "aborted" && this.ctx.streamingMessage.stopReason !== "error") {
@@ -1308,7 +1308,7 @@ export class InputController {
1308
1308
  // If streaming, re-add the streaming component with updated visibility and re-render
1309
1309
  if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
1310
1310
  this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.hideThinkingBlock);
1311
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
1311
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
1312
1312
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
1313
1313
  }
1314
1314
 
@@ -118,7 +118,7 @@ import { UiHelpers } from "./utils/ui-helpers";
118
118
 
119
119
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
120
120
  const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
121
- const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Shift+Tab: Reasoning`;
121
+ export const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Ctrl+R: Search history · Shift+Tab: Reasoning`;
122
122
  const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
123
123
  const FRIENDLY_KEY_PARTS: Record<string, string> = {
124
124
  alt: "Alt",
@@ -163,6 +163,7 @@ function configureDefaultComposerChrome(editor: CustomEditor): void {
163
163
  editor.setInputPrefix(getDefaultInputPrefix());
164
164
  editor.setPlaceholder(DEFAULT_COMPOSER_PLACEHOLDER);
165
165
  editor.setPaddingX(1);
166
+ editor.setRightGutterWidth(1);
166
167
  editor.setTopBorder(undefined);
167
168
  }
168
169
 
@@ -1715,6 +1716,7 @@ export class InteractiveMode implements InteractiveModeContext {
1715
1716
  planContent,
1716
1717
  finalPlanFilePath: options.finalPlanFilePath,
1717
1718
  contextPreserved: options.preserveContext === true,
1719
+ tools: this.session.getActiveToolNames(),
1718
1720
  });
1719
1721
  await this.session.prompt(planModePrompt, { synthetic: true });
1720
1722
  }
@@ -2206,6 +2208,7 @@ export class InteractiveMode implements InteractiveModeContext {
2206
2208
  message => renderWorkingMessage(message, this.#getWorkingMessageAccent()),
2207
2209
  this.#defaultWorkingMessage,
2208
2210
  getSymbolTheme().spinnerFrames,
2211
+ { timeDependentColor: true },
2209
2212
  );
2210
2213
  this.statusContainer.addChild(this.loadingAnimation);
2211
2214
  }
@@ -2363,6 +2366,11 @@ export class InteractiveMode implements InteractiveModeContext {
2363
2366
  return this.#commandController.handleClearCommand();
2364
2367
  }
2365
2368
 
2369
+ handleContextClearCommand(): Promise<void> {
2370
+ this.#prepareSessionSwitch();
2371
+ return this.#commandController.handleContextClearCommand();
2372
+ }
2373
+
2366
2374
  handleDropCommand(): Promise<void> {
2367
2375
  this.#prepareSessionSwitch();
2368
2376
  return this.#commandController.handleDropCommand();
@@ -22,8 +22,13 @@ import { type Theme, theme } from "../../modes/theme/theme";
22
22
  import type { AgentSession } from "../../session/agent-session";
23
23
  import { initializeExtensions } from "../runtime-init";
24
24
  import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
25
- import { AgentWireFrameSequencer, toAgentWireEventFrame } from "../shared/agent-wire/event-envelope";
26
- import { rpcError as error } from "../shared/agent-wire/responses";
25
+ import {
26
+ AgentWireCompactEventEncoder,
27
+ AgentWireFrameSequencer,
28
+ toAgentWireCompactEventFrame,
29
+ toAgentWireEventFrame,
30
+ } from "../shared/agent-wire/event-envelope";
31
+ import { rpcError as error, rpcSuccess } from "../shared/agent-wire/responses";
27
32
  import { registerRpcSession, unregisterRpcSession } from "../shared/agent-wire/session-registry";
28
33
  import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
29
34
  import { modelSupportsTokenCostMetrics, UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
@@ -297,6 +302,7 @@ export async function runRpcMode(
297
302
  output({ type: "ready" });
298
303
  }
299
304
  const emitRpcTitles = shouldEmitRpcTitles();
305
+ const rpcCapabilities = { compactMessageUpdate: false };
300
306
  const decodeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
301
307
 
302
308
  const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
@@ -636,8 +642,13 @@ export async function runRpcMode(
636
642
  // Output all agent events as canonical agent-wire `event` frames (docs/rpc.md):
637
643
  // { type:"event", protocol_version, session_id, seq, frame_id, payload:{ event_type, event } }.
638
644
  const eventSequencer = new AgentWireFrameSequencer(session.sessionId);
645
+ const compactEventEncoder = new AgentWireCompactEventEncoder(eventSequencer);
639
646
  session.subscribe(event => {
640
- output(toAgentWireEventFrame(event, eventSequencer));
647
+ output(
648
+ rpcCapabilities.compactMessageUpdate
649
+ ? toAgentWireCompactEventFrame(event, compactEventEncoder)
650
+ : toAgentWireEventFrame(event, eventSequencer),
651
+ );
641
652
  });
642
653
 
643
654
  // Handle a single command through the shared agent-wire dispatcher so RPC
@@ -710,6 +721,21 @@ export async function runRpcMode(
710
721
  hostUriBridge.handleResult(parsed);
711
722
  return;
712
723
  }
724
+ if ((parsed as RpcCommand).type === "set_capabilities") {
725
+ // RPC has no startup handshake; clients negotiate optional event-shape capabilities
726
+ // with this explicit command. Defaults remain legacy/full-frame until accepted here.
727
+ const command = parsed as Extract<RpcCommand, { type: "set_capabilities" }>;
728
+ const requested = Array.isArray(command.capabilities) ? command.capabilities : [];
729
+ const acceptedCapabilities = requested.filter(capability => capability === "compact_message_update");
730
+ rpcCapabilities.compactMessageUpdate = acceptedCapabilities.includes("compact_message_update");
731
+ output(
732
+ rpcSuccess(command.id, "set_capabilities", {
733
+ acceptedCapabilities,
734
+ unsupported: requested.filter(capability => capability !== "compact_message_update"),
735
+ }),
736
+ );
737
+ return;
738
+ }
713
739
  // Ordered commands run through a serial chain to preserve causal order; the
714
740
  // reader never blocks, so cancellation commands stay responsive even while a
715
741
  // long command is in flight (issue 13).
@@ -13,6 +13,7 @@ import type { SessionStats } from "../../session/agent-session";
13
13
  import type { TodoPhase } from "../../tools/todo-write";
14
14
 
15
15
  export type RpcGetStateInclude = "tools" | "dumpTools" | "systemPrompt";
16
+ export type RpcCapability = "compact_message_update";
16
17
 
17
18
  // ============================================================================
18
19
  // RPC Commands (stdin)
@@ -33,6 +34,7 @@ export type RpcCommand =
33
34
  | { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
34
35
  | { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
35
36
  | { id?: string; type: "get_pending_workflow_gates" }
37
+ | { id?: string; type: "set_capabilities"; capabilities: RpcCapability[] }
36
38
 
37
39
  // Model
38
40
  | { id?: string; type: "set_model"; provider: string; modelId: string }
@@ -140,6 +142,13 @@ export type RpcResponse =
140
142
  success: true;
141
143
  data: { gates: RpcWorkflowGate[] };
142
144
  }
145
+ | {
146
+ id?: string;
147
+ type: "response";
148
+ command: "set_capabilities";
149
+ success: true;
150
+ data: { acceptedCapabilities: RpcCapability[]; unsupported: string[] };
151
+ }
143
152
 
144
153
  // Model
145
154
  | {
@@ -130,6 +130,10 @@ export function isRpcCommand(value: unknown): value is RpcCommand {
130
130
  return Array.isArray(value.tools) && value.tools.every(hostToolDefinition);
131
131
  case "set_host_uri_schemes":
132
132
  return Array.isArray(value.schemes) && value.schemes.every(hostUriScheme);
133
+ case "set_capabilities":
134
+ return (
135
+ Array.isArray(value.capabilities) && value.capabilities.every(capability => typeof capability === "string")
136
+ );
133
137
  case "set_model":
134
138
  return stringField(value, "provider") && stringField(value, "modelId");
135
139
  case "set_thinking_level":
@@ -13,6 +13,8 @@
13
13
  * an `AgentSessionEvent` variant fails typecheck until it is registered, and so
14
14
  * conformance tests can assert fixture coverage equals the registry exactly.
15
15
  */
16
+
17
+ import type { AssistantMessageEvent } from "@gajae-code/ai";
16
18
  import type { AgentSessionEvent } from "../../../session/agent-session";
17
19
 
18
20
  /** Wire protocol version. Bump on breaking envelope/semantic changes. */
@@ -68,6 +70,25 @@ export interface AgentWireEventPayload {
68
70
  event_type: AgentWireEventType;
69
71
  event: AgentSessionEvent;
70
72
  }
73
+ export type AgentWireCompactAssistantMessageEvent = AssistantMessageEvent extends infer TEvent
74
+ ? TEvent extends { type: string }
75
+ ? Omit<TEvent, "partial" | "message" | "error">
76
+ : never
77
+ : never;
78
+
79
+ export interface AgentWireCompactMessageUpdatePayload {
80
+ event_type: "message_update";
81
+ /** Compact RPC opt-in payload: delta-only update plus minimal routing metadata. */
82
+ compact: true;
83
+ message_id: string | null;
84
+ content_index: number | null;
85
+ assistantMessageEvent: AgentWireCompactAssistantMessageEvent;
86
+ /** Full message checkpoint, snapshotted synchronously when this frame is enqueued. */
87
+ checkpoint_message?: Extract<AgentSessionEvent, { type: "message_update" }>["message"];
88
+ checkpoint_reason?: "periodic";
89
+ }
90
+
91
+ export type AgentWireEventFramePayload = AgentWireEventPayload | AgentWireCompactMessageUpdatePayload;
71
92
 
72
93
  /**
73
94
  * Bounded observed-signal vocabulary surfaced to owner control planes. Mirrors
@@ -146,3 +167,4 @@ export interface AgentWireFrameEnvelope<TType extends AgentWireFrameType = Agent
146
167
 
147
168
  /** An `AgentSessionEvent` serialized into a versioned wire frame. */
148
169
  export type AgentWireEventFrame = AgentWireFrameEnvelope<"event", AgentWireEventPayload>;
170
+ export type AgentWireCompactEventFrame = AgentWireFrameEnvelope<"event", AgentWireEventFramePayload>;
@@ -12,6 +12,9 @@ import { randomUUID } from "node:crypto";
12
12
  import type { AgentSessionEvent } from "../../../session/agent-session";
13
13
  import {
14
14
  AGENT_WIRE_PROTOCOL_VERSION,
15
+ type AgentWireCompactAssistantMessageEvent,
16
+ type AgentWireCompactEventFrame,
17
+ type AgentWireCompactMessageUpdatePayload,
15
18
  type AgentWireEventFrame,
16
19
  type AgentWireEventPayload,
17
20
  type AgentWireEventType,
@@ -19,6 +22,26 @@ import {
19
22
  type AgentWireFrameType,
20
23
  } from "./event-contract";
21
24
 
25
+ export const AGENT_WIRE_COMPACT_MESSAGE_UPDATE_CHECKPOINT_INTERVAL = 32;
26
+
27
+ function recordObject(value: unknown): Record<string, unknown> | undefined {
28
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
29
+ }
30
+
31
+ function messageIdOf(message: unknown): string | null {
32
+ const id = recordObject(message)?.id;
33
+ return typeof id === "string" ? id : null;
34
+ }
35
+
36
+ function contentIndexOf(assistantMessageEvent: unknown): number | null {
37
+ const contentIndex = recordObject(assistantMessageEvent)?.contentIndex;
38
+ return typeof contentIndex === "number" && Number.isInteger(contentIndex) ? contentIndex : null;
39
+ }
40
+
41
+ function snapshotMessage<T>(message: T): T {
42
+ return structuredClone(message);
43
+ }
44
+
22
45
  function assertNever(value: never): never {
23
46
  throw new Error(`Unhandled AgentSessionEvent variant: ${JSON.stringify(value)}`);
24
47
  }
@@ -123,6 +146,65 @@ export function toAgentWireEventFrame(
123
146
  export function toAgentWireEventPayload(event: AgentSessionEvent): AgentWireEventPayload {
124
147
  return { event_type: agentSessionEventType(event), event };
125
148
  }
149
+ export function sanitizeAssistantMessageEventForCompact(
150
+ assistantMessageEvent: Extract<AgentSessionEvent, { type: "message_update" }>["assistantMessageEvent"],
151
+ ): AgentWireCompactAssistantMessageEvent {
152
+ const {
153
+ partial: _partial,
154
+ message: _message,
155
+ error: _error,
156
+ ...deltaOnly
157
+ } = assistantMessageEvent as Record<string, unknown>;
158
+ return deltaOnly as AgentWireCompactAssistantMessageEvent;
159
+ }
160
+
161
+ export interface AgentWireCompactMessageUpdateOptions {
162
+ checkpointInterval?: number;
163
+ }
164
+
165
+ /**
166
+ * Stateful compact event serializer for opt-in RPC clients.
167
+ *
168
+ * Only `message_update` changes shape: it carries the immutable provider delta
169
+ * plus message/content identifiers, with periodic full-message checkpoints that
170
+ * are cloned synchronously before the caller returns to the event loop. All other
171
+ * event variants remain canonical full event frames so legacy bridge/RPC behavior
172
+ * and terminal `message_end` reconstruction are unchanged.
173
+ */
174
+ export class AgentWireCompactEventEncoder {
175
+ readonly #sequencer: AgentWireFrameSequencer;
176
+ readonly #checkpointInterval: number;
177
+ #messageUpdateCount = 0;
178
+
179
+ constructor(sequencer: AgentWireFrameSequencer, options: AgentWireCompactMessageUpdateOptions = {}) {
180
+ this.#sequencer = sequencer;
181
+ this.#checkpointInterval = options.checkpointInterval ?? AGENT_WIRE_COMPACT_MESSAGE_UPDATE_CHECKPOINT_INTERVAL;
182
+ }
183
+
184
+ frame(event: AgentSessionEvent): AgentWireCompactEventFrame {
185
+ if (event.type !== "message_update") return toAgentWireEventFrame(event, this.#sequencer);
186
+ this.#messageUpdateCount += 1;
187
+ const payload: AgentWireCompactMessageUpdatePayload = {
188
+ event_type: "message_update",
189
+ compact: true,
190
+ message_id: messageIdOf(event.message),
191
+ content_index: contentIndexOf(event.assistantMessageEvent),
192
+ assistantMessageEvent: sanitizeAssistantMessageEventForCompact(event.assistantMessageEvent),
193
+ };
194
+ if (this.#checkpointInterval > 0 && this.#messageUpdateCount % this.#checkpointInterval === 0) {
195
+ payload.checkpoint_message = snapshotMessage(event.message);
196
+ payload.checkpoint_reason = "periodic";
197
+ }
198
+ return this.#sequencer.next("event", payload);
199
+ }
200
+ }
201
+
202
+ export function toAgentWireCompactEventFrame(
203
+ event: AgentSessionEvent,
204
+ encoder: AgentWireCompactEventEncoder,
205
+ ): AgentWireCompactEventFrame {
206
+ return encoder.frame(event);
207
+ }
126
208
 
127
209
  /** Back-compat alias for {@link toAgentWireEventFrame}. */
128
210
  export const toBridgeEventFrame = toAgentWireEventFrame;
@@ -47,6 +47,7 @@ const RPC_COMMAND_SCOPE_REGISTRY: Record<RpcCommandType, BridgeCommandScope> = {
47
47
  set_host_tools: "host_tools",
48
48
  set_host_uri_schemes: "host_uri",
49
49
  get_pending_workflow_gates: "message:read",
50
+ set_capabilities: "control",
50
51
  set_model: "model",
51
52
  cycle_model: "model",
52
53
  get_available_models: "model",
@@ -216,6 +216,7 @@ export interface InteractiveModeContext {
216
216
  handleDumpCommand(): void;
217
217
  handleDebugTranscriptCommand(): Promise<void>;
218
218
  handleClearCommand(): Promise<void>;
219
+ handleContextClearCommand(): Promise<void>;
219
220
  handleDropCommand(): Promise<void>;
220
221
  handleForkCommand(): Promise<void>;
221
222
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
@@ -36,6 +36,19 @@ interface RenderInitialMessagesOptions {
36
36
  preserveExistingChat?: boolean;
37
37
  }
38
38
 
39
+ export function argsWithPartialJson(args: unknown, partialJson: unknown): unknown {
40
+ if (typeof partialJson !== "string" || !args || typeof args !== "object" || Array.isArray(args)) return args;
41
+ // Non-enumerable so the transient streaming buffer reaches renderers via direct
42
+ // property read but never serializes into persisted/wire message content.
43
+ Object.defineProperty(args, "__partialJson", {
44
+ value: partialJson,
45
+ enumerable: false,
46
+ configurable: true,
47
+ writable: true,
48
+ });
49
+ return args;
50
+ }
51
+
39
52
  type QueuedMessages = {
40
53
  steering: string[];
41
54
  followUp: string[];
@@ -392,10 +405,10 @@ export class UiHelpers {
392
405
 
393
406
  readGroup = null;
394
407
  const tool = this.ctx.session.getToolByName(content.name);
395
- const renderArgs =
396
- "partialJson" in content
397
- ? { ...content.arguments, __partialJson: content.partialJson }
398
- : content.arguments;
408
+ const renderArgs = argsWithPartialJson(
409
+ content.arguments,
410
+ "partialJson" in content ? content.partialJson : undefined,
411
+ );
399
412
  const component = new ToolExecutionComponent(
400
413
  content.name,
401
414
  renderArgs,
@@ -38,13 +38,24 @@ export function getNotificationConfig(settings: Settings): NotificationConfig {
38
38
  };
39
39
  }
40
40
 
41
+ export function hasNonBlankValue(value: string | undefined): boolean {
42
+ return typeof value === "string" && value.trim().length > 0;
43
+ }
44
+
45
+ /** Is Telegram configured with usable non-blank boundary credentials? */
46
+ export function isTelegramConfigured(
47
+ cfg: NotificationConfig,
48
+ ): cfg is NotificationConfig & { botToken: string; chatId: string } {
49
+ return cfg.enabled && hasNonBlankValue(cfg.botToken) && hasNonBlankValue(cfg.chatId);
50
+ }
51
+
41
52
  /** Is global config sufficient for auto-on (enabled + at least one configured adapter)? */
42
53
  export function isGloballyConfigured(cfg: NotificationConfig): boolean {
43
54
  return (
44
55
  cfg.enabled &&
45
- ((Boolean(cfg.botToken) && Boolean(cfg.chatId)) ||
46
- (Boolean(cfg.discord.botToken) && Boolean(cfg.discord.channelId)) ||
47
- (Boolean(cfg.slack.botToken) && Boolean(cfg.slack.channelId)))
56
+ (isTelegramConfigured(cfg) ||
57
+ (hasNonBlankValue(cfg.discord.botToken) && hasNonBlankValue(cfg.discord.channelId)) ||
58
+ (hasNonBlankValue(cfg.slack.botToken) && hasNonBlankValue(cfg.slack.channelId)))
48
59
  );
49
60
  }
50
61
 
@@ -88,6 +99,7 @@ export function isSessionNotificationsEnabled(input: {
88
99
  /** Mask a bot token for display: first 4 chars + "…" + "(len N)"; "(unset)" when undefined/empty. Never reveal full token. */
89
100
  export function maskToken(token: string | undefined): string {
90
101
  if (!token) return "(unset)";
102
+ if (token.length <= 4) return `…(len ${token.length})`;
91
103
  return `${token.slice(0, 4)}…(len ${token.length})`;
92
104
  }
93
105
 
@@ -131,6 +143,6 @@ export function buildRedactedAction(
131
143
  // Asks stay fully readable/answerable even under redaction.
132
144
  if (action.kind === "ask") return action;
133
145
 
134
- const { summary: _summary, question: _question, ...base } = action;
146
+ const { summary: _summary, question: _question, options: _options, ...base } = action;
135
147
  return base;
136
148
  }