@gajae-code/coding-agent 0.3.0 → 0.3.1

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 (175) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/async/job-manager.d.ts +7 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/config/keybindings.d.ts +5 -0
  6. package/dist/types/config/settings-schema.d.ts +4 -4
  7. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  8. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  9. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  10. package/dist/types/eval/py/executor.d.ts +2 -0
  11. package/dist/types/eval/py/kernel.d.ts +2 -0
  12. package/dist/types/exec/bash-executor.d.ts +10 -0
  13. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  14. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  16. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  17. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  20. package/dist/types/hooks/skill-state.d.ts +21 -0
  21. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  22. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  23. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  24. package/dist/types/internal-urls/types.d.ts +4 -0
  25. package/dist/types/lsp/index.d.ts +10 -10
  26. package/dist/types/modes/bridge/auth.d.ts +12 -0
  27. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  29. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  30. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  32. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  33. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  34. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  35. package/dist/types/modes/components/status-line.d.ts +2 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  37. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  38. package/dist/types/modes/index.d.ts +1 -0
  39. package/dist/types/modes/interactive-mode.d.ts +1 -0
  40. package/dist/types/modes/jobs-observer.d.ts +57 -0
  41. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  42. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  43. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  44. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  46. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  47. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  48. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  49. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  50. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  51. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  52. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  53. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  54. package/dist/types/modes/types.d.ts +1 -0
  55. package/dist/types/sdk.d.ts +2 -0
  56. package/dist/types/session/agent-session.d.ts +11 -1
  57. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  58. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  59. package/dist/types/task/id.d.ts +7 -0
  60. package/dist/types/task/index.d.ts +5 -0
  61. package/dist/types/task/receipt.d.ts +85 -0
  62. package/dist/types/task/spawn-gate.d.ts +38 -0
  63. package/dist/types/task/types.d.ts +143 -11
  64. package/dist/types/tools/cron.d.ts +6 -0
  65. package/dist/types/tools/index.d.ts +2 -0
  66. package/dist/types/tools/path-utils.d.ts +1 -0
  67. package/dist/types/tools/subagent.d.ts +15 -0
  68. package/package.json +7 -7
  69. package/scripts/build-binary.ts +7 -0
  70. package/src/async/job-manager.ts +36 -0
  71. package/src/cli/args.ts +9 -2
  72. package/src/commands/deep-interview.ts +1 -0
  73. package/src/commands/harness.ts +289 -19
  74. package/src/commands/launch.ts +2 -2
  75. package/src/commands/state.ts +2 -1
  76. package/src/commands/team.ts +22 -4
  77. package/src/config/keybindings.ts +6 -0
  78. package/src/config/settings-schema.ts +6 -3
  79. package/src/dap/client.ts +17 -3
  80. package/src/debug/crash-diagnostics.ts +223 -0
  81. package/src/debug/runtime-gauges.ts +20 -0
  82. package/src/deep-interview/render-middleware.ts +6 -0
  83. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  84. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  85. package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
  86. package/src/eval/py/executor.ts +21 -1
  87. package/src/eval/py/kernel.ts +15 -0
  88. package/src/exec/bash-executor.ts +41 -0
  89. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  90. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  91. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  92. package/src/gjc-runtime/state-migrations.ts +54 -7
  93. package/src/gjc-runtime/state-runtime.ts +461 -64
  94. package/src/gjc-runtime/state-schema.ts +192 -0
  95. package/src/gjc-runtime/state-writer.ts +32 -1
  96. package/src/gjc-runtime/team-runtime.ts +177 -105
  97. package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
  98. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  99. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  100. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  101. package/src/harness-control-plane/control-endpoint.ts +19 -8
  102. package/src/harness-control-plane/owner.ts +57 -10
  103. package/src/harness-control-plane/state-machine.ts +2 -1
  104. package/src/hooks/skill-state.ts +176 -26
  105. package/src/internal-urls/agent-protocol.ts +68 -21
  106. package/src/internal-urls/artifact-protocol.ts +12 -17
  107. package/src/internal-urls/docs-index.generated.ts +3 -2
  108. package/src/internal-urls/registry-helpers.ts +19 -16
  109. package/src/internal-urls/types.ts +4 -0
  110. package/src/lsp/client.ts +18 -2
  111. package/src/main.ts +21 -5
  112. package/src/modes/bridge/auth.ts +41 -0
  113. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  114. package/src/modes/bridge/bridge-mode.ts +520 -0
  115. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  116. package/src/modes/bridge/event-stream.ts +70 -0
  117. package/src/modes/components/custom-editor.ts +101 -0
  118. package/src/modes/components/hook-selector.ts +61 -18
  119. package/src/modes/components/jobs-overlay-model.ts +109 -0
  120. package/src/modes/components/jobs-overlay.ts +172 -0
  121. package/src/modes/components/status-line/presets.ts +7 -5
  122. package/src/modes/components/status-line/segments.ts +25 -0
  123. package/src/modes/components/status-line/types.ts +2 -0
  124. package/src/modes/components/status-line.ts +9 -1
  125. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  126. package/src/modes/controllers/input-controller.ts +97 -9
  127. package/src/modes/controllers/selector-controller.ts +29 -0
  128. package/src/modes/index.ts +1 -0
  129. package/src/modes/interactive-mode.ts +27 -0
  130. package/src/modes/jobs-observer.ts +204 -0
  131. package/src/modes/rpc/host-tools.ts +1 -186
  132. package/src/modes/rpc/host-uris.ts +1 -235
  133. package/src/modes/rpc/rpc-client.ts +25 -10
  134. package/src/modes/rpc/rpc-mode.ts +12 -381
  135. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  136. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  137. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  138. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  139. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  140. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  141. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  142. package/src/modes/shared/agent-wire/responses.ts +17 -0
  143. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  144. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  145. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  146. package/src/modes/types.ts +1 -0
  147. package/src/prompts/tools/subagent.md +12 -7
  148. package/src/prompts/tools/task-summary.md +3 -9
  149. package/src/prompts/tools/task.md +5 -1
  150. package/src/sdk.ts +4 -0
  151. package/src/session/agent-session.ts +214 -38
  152. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  153. package/src/skill-state/workflow-state-contract.ts +7 -4
  154. package/src/skill-state/workflow-state-version.ts +3 -0
  155. package/src/slash-commands/builtin-registry.ts +8 -0
  156. package/src/task/executor.ts +29 -5
  157. package/src/task/id.ts +33 -0
  158. package/src/task/index.ts +257 -67
  159. package/src/task/output-manager.ts +5 -4
  160. package/src/task/receipt.ts +297 -0
  161. package/src/task/render.ts +48 -131
  162. package/src/task/spawn-gate.ts +132 -0
  163. package/src/task/types.ts +48 -7
  164. package/src/tools/ask.ts +73 -33
  165. package/src/tools/ast-edit.ts +1 -0
  166. package/src/tools/ast-grep.ts +1 -0
  167. package/src/tools/bash.ts +1 -1
  168. package/src/tools/cron.ts +48 -0
  169. package/src/tools/find.ts +4 -1
  170. package/src/tools/index.ts +2 -0
  171. package/src/tools/path-utils.ts +3 -2
  172. package/src/tools/read.ts +1 -0
  173. package/src/tools/search.ts +1 -0
  174. package/src/tools/skill.ts +6 -1
  175. package/src/tools/subagent.ts +237 -84
@@ -28,6 +28,7 @@ import {
28
28
  } from "@gajae-code/tui";
29
29
  import { APP_NAME, adjustHsv, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@gajae-code/utils";
30
30
  import chalk from "chalk";
31
+ import { AsyncJobManager } from "../async";
31
32
  import { KeybindingsManager } from "../config/keybindings";
32
33
  import { isSettingsInitialized, type Settings, settings } from "../config/settings";
33
34
  import { DEFAULT_GJC_DEFINITION_NAMES } from "../defaults/gjc-defaults";
@@ -88,6 +89,7 @@ import { InputController } from "./controllers/input-controller";
88
89
  import { SelectorController } from "./controllers/selector-controller";
89
90
  import { SSHCommandController } from "./controllers/ssh-command-controller";
90
91
  import { TodoCommandController } from "./controllers/todo-command-controller";
92
+ import { JobsObserver } from "./jobs-observer";
91
93
  import { OAuthManualInputManager } from "./oauth-manual-input";
92
94
  import { SessionObserverRegistry } from "./session-observer-registry";
93
95
  import { interruptHint } from "./shared";
@@ -330,6 +332,7 @@ export class InteractiveMode implements InteractiveModeContext {
330
332
  #voicePreviousUseTerminalCursor: boolean | null = null;
331
333
  #resizeHandler?: () => void;
332
334
  #observerRegistry: SessionObserverRegistry;
335
+ #jobsObserver?: JobsObserver;
333
336
  #eventBus?: EventBus;
334
337
  #eventBusUnsubscribers: Array<() => void> = [];
335
338
  #welcomeComponent?: WelcomeComponent;
@@ -525,6 +528,19 @@ export class InteractiveMode implements InteractiveModeContext {
525
528
  this.ui.requestRender();
526
529
  });
527
530
 
531
+ // Event-driven monitor/cron jobs widget. Scoped to this session's owner so
532
+ // overlay actions cannot mutate another agent's background work.
533
+ const jobManager = AsyncJobManager.instance();
534
+ if (jobManager) {
535
+ const jobsObserver = new JobsObserver(jobManager, this.session.getAgentId());
536
+ this.#jobsObserver = jobsObserver;
537
+ this.statusLine.setJobs(jobsObserver.getSnapshot());
538
+ jobsObserver.onChange(() => {
539
+ this.statusLine.setJobs(jobsObserver.getSnapshot());
540
+ this.ui.requestRender();
541
+ });
542
+ }
543
+
528
544
  // Load initial todos
529
545
  await this.#loadTodoList();
530
546
 
@@ -1843,6 +1859,8 @@ export class InteractiveMode implements InteractiveModeContext {
1843
1859
  this.#observerRegistry.dispose();
1844
1860
  this.#eventController.dispose();
1845
1861
  this.statusLine.dispose();
1862
+ this.#jobsObserver?.dispose();
1863
+ this.editor.dispose();
1846
1864
  if (this.#resizeHandler) {
1847
1865
  process.stdout.removeListener("resize", this.#resizeHandler);
1848
1866
  this.#resizeHandler = undefined;
@@ -1944,6 +1962,7 @@ export class InteractiveMode implements InteractiveModeContext {
1944
1962
  nextEditor.setHistoryStorage(this.historyStorage);
1945
1963
  }
1946
1964
  nextEditor.setText(previousText);
1965
+ previousEditor.dispose();
1947
1966
 
1948
1967
  this.editorContainer.clear();
1949
1968
  this.editor = nextEditor;
@@ -2317,6 +2336,14 @@ export class InteractiveMode implements InteractiveModeContext {
2317
2336
  this.#selectorController.showSessionObserver(this.#observerRegistry);
2318
2337
  }
2319
2338
 
2339
+ showJobsOverlay(): void {
2340
+ if (!this.#jobsObserver) {
2341
+ this.showStatus("Background jobs are unavailable in this session");
2342
+ return;
2343
+ }
2344
+ this.#selectorController.showJobsOverlay(this.#jobsObserver);
2345
+ }
2346
+
2320
2347
  resetObserverRegistry(): void {
2321
2348
  this.#observerRegistry.resetSessions();
2322
2349
  this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
@@ -0,0 +1,204 @@
1
+ /**
2
+ * JobsObserver
3
+ *
4
+ * Single, event-driven aggregator over the two background-work sources surfaced
5
+ * by the status-line jobs widget and the jobs overlay:
6
+ * - monitor jobs (bash jobs started by the `monitor` tool, tracked in `AsyncJobManager`)
7
+ * - cron jobs (tracked in the cron module's owner-scoped schedule store)
8
+ *
9
+ * It subscribes to change hooks on both sources (no polling), debounces bursts
10
+ * to a microtask, and exposes a precomputed snapshot so the status-line render
11
+ * loop never scans the underlying stores. A failure latch keeps the widget red
12
+ * until `acknowledgeFailures()` is called (when the overlay opens), so a failed
13
+ * job that evicts before the user looks is not silently lost.
14
+ */
15
+ import type { AsyncJob, AsyncJobManager } from "../async";
16
+ import { deleteCronJobById, listCronSnapshots, onCronChange } from "../tools/cron";
17
+
18
+ export type JobsWorstState = "none" | "running" | "failed";
19
+
20
+ export interface MonitorJobView {
21
+ id: string;
22
+ label: string;
23
+ status: AsyncJob["status"];
24
+ startTime: number;
25
+ }
26
+
27
+ export interface CronJobView {
28
+ id: string;
29
+ humanSchedule: string;
30
+ cronExpression: string;
31
+ prompt: string;
32
+ recurring: boolean;
33
+ nextFireAt?: number;
34
+ createdAt: number;
35
+ }
36
+
37
+ export interface JobsSnapshot {
38
+ monitors: MonitorJobView[];
39
+ crons: CronJobView[];
40
+ activeMonitorCount: number;
41
+ activeCronCount: number;
42
+ worstState: JobsWorstState;
43
+ failedUnacknowledged: boolean;
44
+ }
45
+
46
+ export const EMPTY_JOBS_SNAPSHOT: JobsSnapshot = {
47
+ monitors: [],
48
+ crons: [],
49
+ activeMonitorCount: 0,
50
+ activeCronCount: 0,
51
+ worstState: "none",
52
+ failedUnacknowledged: false,
53
+ };
54
+
55
+ export class JobsObserver {
56
+ readonly #manager: AsyncJobManager;
57
+ readonly #ownerId: string | undefined;
58
+ readonly #unsubscribers: Array<() => void> = [];
59
+ readonly #listeners = new Set<() => void>();
60
+ #failedUnacknowledged = false;
61
+ #notifyScheduled = false;
62
+ #disposed = false;
63
+ #snapshot: JobsSnapshot = EMPTY_JOBS_SNAPSHOT;
64
+ readonly #acknowledgedFailedIds = new Set<string>();
65
+
66
+ constructor(manager: AsyncJobManager, ownerId: string | undefined) {
67
+ this.#manager = manager;
68
+ this.#ownerId = ownerId;
69
+ this.#unsubscribers.push(manager.onChange(() => this.#onUpstreamChange()));
70
+ this.#unsubscribers.push(onCronChange(() => this.#onUpstreamChange()));
71
+ this.#recompute();
72
+ }
73
+
74
+ /** Subscribe to debounced change events. Returns an unsubscribe function. */
75
+ onChange(cb: () => void): () => void {
76
+ this.#listeners.add(cb);
77
+ return () => {
78
+ this.#listeners.delete(cb);
79
+ };
80
+ }
81
+
82
+ #onUpstreamChange(): void {
83
+ if (this.#disposed) return;
84
+ this.#recompute();
85
+ if (this.#notifyScheduled) return;
86
+ this.#notifyScheduled = true;
87
+ queueMicrotask(() => {
88
+ this.#notifyScheduled = false;
89
+ if (this.#disposed) return;
90
+ this.#emit();
91
+ });
92
+ }
93
+
94
+ #emit(): void {
95
+ for (const cb of this.#listeners) {
96
+ try {
97
+ cb();
98
+ } catch {
99
+ // Listener errors are isolated; a bad subscriber must not break others.
100
+ }
101
+ }
102
+ }
103
+
104
+ #listMonitorJobs(): AsyncJob[] {
105
+ const filter = this.#ownerId ? { ownerId: this.#ownerId } : undefined;
106
+ return this.#manager.getAllJobs(filter).filter(job => job.type === "bash" && job.metadata?.monitor === true);
107
+ }
108
+
109
+ /**
110
+ * Recompute and store the snapshot. Called on construction and on every
111
+ * upstream change; the status-line render path only reads the stored
112
+ * snapshot (never scans the manager/cron stores).
113
+ */
114
+ #recompute(): void {
115
+ const monitorJobs = this.#listMonitorJobs();
116
+ const presentIds = new Set(monitorJobs.map(job => job.id));
117
+ // Prune acknowledged ids whose jobs have been evicted.
118
+ for (const id of this.#acknowledgedFailedIds) {
119
+ if (!presentIds.has(id)) this.#acknowledgedFailedIds.delete(id);
120
+ }
121
+ // Sticky failure latch: set when an unacknowledged failed monitor is seen
122
+ // (including at construction); stays set even after the failed job evicts,
123
+ // until acknowledgeFailures() clears it.
124
+ const hasUnacknowledgedFailure = monitorJobs.some(
125
+ job => job.status === "failed" && !this.#acknowledgedFailedIds.has(job.id),
126
+ );
127
+ if (hasUnacknowledgedFailure) this.#failedUnacknowledged = true;
128
+
129
+ const activeMonitors = monitorJobs.filter(job => job.status === "running");
130
+ const cronSnapshots = listCronSnapshots(this.#ownerId);
131
+ const monitors: MonitorJobView[] = monitorJobs
132
+ .map(job => ({ id: job.id, label: job.label, status: job.status, startTime: job.startTime }))
133
+ .sort((a, b) => b.startTime - a.startTime);
134
+ const crons: CronJobView[] = cronSnapshots
135
+ .map(snapshot => ({
136
+ id: snapshot.id,
137
+ humanSchedule: snapshot.humanSchedule,
138
+ cronExpression: snapshot.cron_expression,
139
+ prompt: snapshot.prompt,
140
+ recurring: snapshot.recurring,
141
+ nextFireAt: snapshot.nextFireAt,
142
+ createdAt: snapshot.createdAt,
143
+ }))
144
+ .sort((a, b) => b.createdAt - a.createdAt);
145
+ const worstState: JobsWorstState = this.#failedUnacknowledged
146
+ ? "failed"
147
+ : activeMonitors.length > 0 || crons.length > 0
148
+ ? "running"
149
+ : "none";
150
+ this.#snapshot = {
151
+ monitors,
152
+ crons,
153
+ activeMonitorCount: activeMonitors.length,
154
+ activeCronCount: crons.length,
155
+ worstState,
156
+ failedUnacknowledged: this.#failedUnacknowledged,
157
+ };
158
+ }
159
+
160
+ /** Return the precomputed snapshot (recomputed on each upstream change). */
161
+ getSnapshot(): JobsSnapshot {
162
+ return this.#snapshot;
163
+ }
164
+
165
+ /** Clear the failure latch (called when the user opens the jobs overlay). */
166
+ acknowledgeFailures(): void {
167
+ for (const job of this.#listMonitorJobs()) {
168
+ if (job.status === "failed") this.#acknowledgedFailedIds.add(job.id);
169
+ }
170
+ if (!this.#failedUnacknowledged) return;
171
+ this.#failedUnacknowledged = false;
172
+ this.#recompute();
173
+ this.#emit();
174
+ }
175
+
176
+ /** Cancel a running monitor job. Returns true when the job was cancelled. */
177
+ cancelMonitor(id: string): boolean {
178
+ return this.#manager.cancel(id);
179
+ }
180
+
181
+ /** Delete a visible scheduled cron job. Returns true when removed. */
182
+ deleteCron(id: string): boolean {
183
+ return deleteCronJobById(this.#ownerId, id);
184
+ }
185
+
186
+ /** Bounded tail of a monitor job's captured output (for the detail view). */
187
+ getMonitorOutput(id: string): string {
188
+ const slice = this.#manager.readOutputSince(id, 0, this.#ownerId ? { ownerId: this.#ownerId } : undefined);
189
+ return slice?.text ?? "";
190
+ }
191
+
192
+ dispose(): void {
193
+ this.#disposed = true;
194
+ for (const unsubscribe of this.#unsubscribers) {
195
+ try {
196
+ unsubscribe();
197
+ } catch {
198
+ // best-effort teardown
199
+ }
200
+ }
201
+ this.#unsubscribers.length = 0;
202
+ this.#listeners.clear();
203
+ }
204
+ }
@@ -1,186 +1 @@
1
- import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
- import type { Static, TSchema } from "@gajae-code/ai";
3
- import { Snowflake } from "@gajae-code/utils";
4
- import { applyToolProxy } from "../../extensibility/tool-proxy";
5
- import type { Theme } from "../../modes/theme/theme";
6
- import type {
7
- RpcHostToolCallRequest,
8
- RpcHostToolCancelRequest,
9
- RpcHostToolDefinition,
10
- RpcHostToolResult,
11
- RpcHostToolUpdate,
12
- } from "./rpc-types";
13
-
14
- type RpcHostToolOutput = (frame: RpcHostToolCallRequest | RpcHostToolCancelRequest) => void;
15
-
16
- type PendingHostToolCall = {
17
- resolve: (result: AgentToolResult<unknown>) => void;
18
- reject: (error: Error) => void;
19
- onUpdate?: AgentToolUpdateCallback<unknown>;
20
- };
21
-
22
- function isAgentToolResult(value: unknown): value is AgentToolResult<unknown> {
23
- if (!value || typeof value !== "object") return false;
24
- const content = (value as { content?: unknown }).content;
25
- return Array.isArray(content);
26
- }
27
-
28
- export function isRpcHostToolResult(value: unknown): value is RpcHostToolResult {
29
- if (!value || typeof value !== "object") return false;
30
- const frame = value as { type?: unknown; id?: unknown; result?: unknown };
31
- return frame.type === "host_tool_result" && typeof frame.id === "string" && isAgentToolResult(frame.result);
32
- }
33
-
34
- export function isRpcHostToolUpdate(value: unknown): value is RpcHostToolUpdate {
35
- if (!value || typeof value !== "object") return false;
36
- const frame = value as { type?: unknown; id?: unknown; partialResult?: unknown };
37
- return frame.type === "host_tool_update" && typeof frame.id === "string" && isAgentToolResult(frame.partialResult);
38
- }
39
-
40
- class RpcHostToolAdapter<TParams extends TSchema = TSchema, TTheme extends Theme = Theme>
41
- implements AgentTool<TParams, unknown, TTheme>
42
- {
43
- declare name: string;
44
- declare label: string;
45
- declare description: string;
46
- declare parameters: TParams;
47
- readonly strict = true;
48
- concurrency: "shared" | "exclusive" = "shared";
49
- #bridge: RpcHostToolBridge;
50
- #definition: RpcHostToolDefinition;
51
-
52
- constructor(definition: RpcHostToolDefinition, bridge: RpcHostToolBridge) {
53
- this.#definition = definition;
54
- this.#bridge = bridge;
55
- applyToolProxy(definition, this);
56
- }
57
-
58
- execute(
59
- toolCallId: string,
60
- params: Static<TParams>,
61
- signal?: AbortSignal,
62
- onUpdate?: AgentToolUpdateCallback<unknown>,
63
- ): Promise<AgentToolResult<unknown>> {
64
- return this.#bridge.requestExecution(
65
- this.#definition,
66
- toolCallId,
67
- params as Record<string, unknown>,
68
- signal,
69
- onUpdate,
70
- );
71
- }
72
- }
73
-
74
- export class RpcHostToolBridge {
75
- #output: RpcHostToolOutput;
76
- #definitions = new Map<string, RpcHostToolDefinition>();
77
- #pendingCalls = new Map<string, PendingHostToolCall>();
78
-
79
- constructor(output: RpcHostToolOutput) {
80
- this.#output = output;
81
- }
82
-
83
- getToolNames(): string[] {
84
- return Array.from(this.#definitions.keys());
85
- }
86
-
87
- setTools(tools: RpcHostToolDefinition[]): AgentTool[] {
88
- this.#definitions = new Map(tools.map(tool => [tool.name, tool]));
89
- return tools.map(tool => new RpcHostToolAdapter(tool, this));
90
- }
91
-
92
- handleResult(frame: RpcHostToolResult): boolean {
93
- const pending = this.#pendingCalls.get(frame.id);
94
- if (!pending) return false;
95
- this.#pendingCalls.delete(frame.id);
96
- if (frame.isError) {
97
- const text = frame.result.content
98
- .filter(
99
- (item): item is { type: "text"; text: string } => item.type === "text" && typeof item.text === "string",
100
- )
101
- .map(item => item.text)
102
- .join("\n")
103
- .trim();
104
- pending.reject(new Error(text || "Host tool execution failed"));
105
- return true;
106
- }
107
- pending.resolve(frame.result);
108
- return true;
109
- }
110
-
111
- handleUpdate(frame: RpcHostToolUpdate): boolean {
112
- const pending = this.#pendingCalls.get(frame.id);
113
- if (!pending) return false;
114
- pending.onUpdate?.(frame.partialResult);
115
- return true;
116
- }
117
-
118
- requestExecution(
119
- definition: RpcHostToolDefinition,
120
- toolCallId: string,
121
- args: Record<string, unknown>,
122
- signal?: AbortSignal,
123
- onUpdate?: AgentToolUpdateCallback<unknown>,
124
- ): Promise<AgentToolResult<unknown>> {
125
- if (signal?.aborted) {
126
- return Promise.reject(new Error(`Host tool "${definition.name}" was aborted`));
127
- }
128
-
129
- const id = Snowflake.next() as string;
130
- const { promise, resolve, reject } = Promise.withResolvers<AgentToolResult<unknown>>();
131
- let settled = false;
132
-
133
- const cleanup = () => {
134
- signal?.removeEventListener("abort", onAbort);
135
- this.#pendingCalls.delete(id);
136
- };
137
-
138
- const onAbort = () => {
139
- if (settled) return;
140
- settled = true;
141
- cleanup();
142
- this.#output({
143
- type: "host_tool_cancel",
144
- id: Snowflake.next() as string,
145
- targetId: id,
146
- });
147
- reject(new Error(`Host tool "${definition.name}" was aborted`));
148
- };
149
-
150
- signal?.addEventListener("abort", onAbort, { once: true });
151
- this.#pendingCalls.set(id, {
152
- resolve: result => {
153
- if (settled) return;
154
- settled = true;
155
- cleanup();
156
- resolve(result);
157
- },
158
- reject: error => {
159
- if (settled) return;
160
- settled = true;
161
- cleanup();
162
- reject(error);
163
- },
164
- onUpdate,
165
- });
166
-
167
- this.#output({
168
- type: "host_tool_call",
169
- id,
170
- toolCallId,
171
- toolName: definition.name,
172
- arguments: args,
173
- });
174
-
175
- return promise;
176
- }
177
-
178
- rejectAllPending(message: string): void {
179
- const error = new Error(message);
180
- const pendingCalls = Array.from(this.#pendingCalls.values());
181
- this.#pendingCalls.clear();
182
- for (const pending of pendingCalls) {
183
- pending.reject(error);
184
- }
185
- }
186
- }
1
+ export * from "../shared/agent-wire/host-tool-bridge";
@@ -1,235 +1 @@
1
- import { Snowflake } from "@gajae-code/utils";
2
- import { InternalUrlRouter } from "../../internal-urls";
3
- import type {
4
- InternalResource,
5
- InternalUrl,
6
- ProtocolHandler,
7
- ResolveContext,
8
- WriteContext,
9
- } from "../../internal-urls/types";
10
- import type {
11
- RpcHostUriCancelRequest,
12
- RpcHostUriRequest,
13
- RpcHostUriResult,
14
- RpcHostUriSchemeDefinition,
15
- } from "./rpc-types";
16
-
17
- type RpcHostUriOutput = (frame: RpcHostUriRequest | RpcHostUriCancelRequest) => void;
18
-
19
- type PendingUriRequest = {
20
- operation: "read" | "write";
21
- url: string;
22
- resolve: (frame: RpcHostUriResult) => void;
23
- reject: (error: Error) => void;
24
- };
25
-
26
- /** Type guard for inbound `host_uri_result` frames coming from the host. */
27
- export function isRpcHostUriResult(value: unknown): value is RpcHostUriResult {
28
- if (!value || typeof value !== "object") return false;
29
- const frame = value as { type?: unknown; id?: unknown };
30
- return frame.type === "host_uri_result" && typeof frame.id === "string";
31
- }
32
-
33
- /**
34
- * One handler instance per host-registered scheme. Delegates reads and (when
35
- * the scheme was registered as writable) writes to the bridge, which serializes
36
- * them over the RPC transport.
37
- */
38
- class RpcHostUriProtocolHandler implements ProtocolHandler {
39
- readonly scheme: string;
40
- readonly immutable: boolean;
41
- readonly write?: (url: InternalUrl, content: string, context?: WriteContext) => Promise<void>;
42
- readonly #bridge: RpcHostUriBridge;
43
-
44
- constructor(definition: RpcHostUriSchemeDefinition, bridge: RpcHostUriBridge) {
45
- this.scheme = definition.scheme;
46
- this.immutable = definition.immutable === true;
47
- this.#bridge = bridge;
48
- if (definition.writable === true) {
49
- this.write = (url, content, context) => this.#bridge.requestWrite(this.scheme, url, content, context);
50
- }
51
- }
52
-
53
- resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
54
- return this.#bridge.requestRead(this.scheme, url, context);
55
- }
56
- }
57
-
58
- /**
59
- * Bidirectional bridge that lets the RPC host own a set of URI schemes.
60
- *
61
- * The host registers schemes via `set_host_uri_schemes`; the bridge installs
62
- * a `RpcHostUriProtocolHandler` per scheme into the process-global
63
- * {@link InternalUrlRouter}. Reads land on the read tool through the existing
64
- * router; writes are intercepted by the write tool and dispatched through
65
- * `requestWrite`.
66
- */
67
- export class RpcHostUriBridge {
68
- #output: RpcHostUriOutput;
69
- #router: InternalUrlRouter;
70
- #definitions = new Map<string, RpcHostUriSchemeDefinition>();
71
- #pending = new Map<string, PendingUriRequest>();
72
-
73
- constructor(output: RpcHostUriOutput, router: InternalUrlRouter = InternalUrlRouter.instance()) {
74
- this.#output = output;
75
- this.#router = router;
76
- }
77
-
78
- getSchemes(): string[] {
79
- return Array.from(this.#definitions.keys());
80
- }
81
-
82
- /**
83
- * Replace the registered set of host URI schemes. Previously registered
84
- * schemes that no longer appear in the new set are unregistered from the
85
- * router; surviving and new schemes get fresh handler instances.
86
- */
87
- setSchemes(schemes: RpcHostUriSchemeDefinition[]): string[] {
88
- const normalized = new Map<string, RpcHostUriSchemeDefinition>();
89
- for (const raw of schemes) {
90
- const scheme = typeof raw?.scheme === "string" ? raw.scheme.trim().toLowerCase() : "";
91
- if (!scheme) {
92
- throw new Error("Host URI scheme must be a non-empty string");
93
- }
94
- if (!/^[a-z][a-z0-9+.-]*$/.test(scheme)) {
95
- throw new Error(`Host URI scheme contains invalid characters: ${raw.scheme}`);
96
- }
97
- normalized.set(scheme, {
98
- scheme,
99
- description: typeof raw.description === "string" ? raw.description : undefined,
100
- writable: raw.writable === true,
101
- immutable: raw.immutable === true,
102
- });
103
- }
104
-
105
- for (const previous of this.#definitions.keys()) {
106
- if (!normalized.has(previous)) {
107
- this.#router.unregister(previous);
108
- }
109
- }
110
- for (const definition of normalized.values()) {
111
- this.#router.register(new RpcHostUriProtocolHandler(definition, this));
112
- }
113
- this.#definitions = normalized;
114
- return Array.from(normalized.keys());
115
- }
116
-
117
- /**
118
- * Unregister every host scheme from the router and reject any in-flight
119
- * requests. Called on RPC shutdown to keep the global router clean for
120
- * subsequent sessions in the same process (used by tests).
121
- */
122
- clear(message: string = "Host URI bridge shut down"): void {
123
- for (const scheme of this.#definitions.keys()) {
124
- this.#router.unregister(scheme);
125
- }
126
- this.#definitions.clear();
127
- this.rejectAllPending(message);
128
- }
129
-
130
- /** Resolve a pending request by id; called by `rpc-mode` on inbound results. */
131
- handleResult(frame: RpcHostUriResult): boolean {
132
- const pending = this.#pending.get(frame.id);
133
- if (!pending) return false;
134
- this.#pending.delete(frame.id);
135
- pending.resolve(frame);
136
- return true;
137
- }
138
-
139
- rejectAllPending(message: string): void {
140
- const error = new Error(message);
141
- const pending = Array.from(this.#pending.values());
142
- this.#pending.clear();
143
- for (const entry of pending) {
144
- entry.reject(error);
145
- }
146
- }
147
-
148
- async requestRead(scheme: string, url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
149
- const result = await this.#dispatch("read", url.href, undefined, context?.signal);
150
- if (result.isError) {
151
- throw new Error(result.error || result.content || `Host URI read failed for ${url.href}`);
152
- }
153
- const content = result.content ?? "";
154
- const contentType = result.contentType ?? "text/plain";
155
- const definition = this.#definitions.get(scheme);
156
- return {
157
- url: url.href,
158
- content,
159
- contentType,
160
- size: Buffer.byteLength(content, "utf-8"),
161
- notes: result.notes && result.notes.length > 0 ? [...result.notes] : undefined,
162
- immutable: result.immutable ?? definition?.immutable === true,
163
- };
164
- }
165
-
166
- async requestWrite(_scheme: string, url: InternalUrl, content: string, context?: WriteContext): Promise<void> {
167
- const result = await this.#dispatch("write", url.href, content, context?.signal);
168
- if (result.isError) {
169
- throw new Error(result.error || result.content || `Host URI write failed for ${url.href}`);
170
- }
171
- }
172
-
173
- #dispatch(
174
- operation: "read" | "write",
175
- url: string,
176
- content: string | undefined,
177
- signal: AbortSignal | undefined,
178
- ): Promise<RpcHostUriResult> {
179
- if (signal?.aborted) {
180
- return Promise.reject(new Error(`Host URI ${operation} for ${url} was aborted`));
181
- }
182
-
183
- const id = Snowflake.next() as string;
184
- const { promise, resolve, reject } = Promise.withResolvers<RpcHostUriResult>();
185
- let settled = false;
186
-
187
- const cleanup = () => {
188
- signal?.removeEventListener("abort", onAbort);
189
- this.#pending.delete(id);
190
- };
191
-
192
- const onAbort = () => {
193
- if (settled) return;
194
- settled = true;
195
- cleanup();
196
- this.#output({
197
- type: "host_uri_cancel",
198
- id: Snowflake.next() as string,
199
- targetId: id,
200
- });
201
- reject(new Error(`Host URI ${operation} for ${url} was aborted`));
202
- };
203
-
204
- signal?.addEventListener("abort", onAbort, { once: true });
205
- this.#pending.set(id, {
206
- operation,
207
- url,
208
- resolve: frame => {
209
- if (settled) return;
210
- settled = true;
211
- cleanup();
212
- resolve(frame);
213
- },
214
- reject: err => {
215
- if (settled) return;
216
- settled = true;
217
- cleanup();
218
- reject(err);
219
- },
220
- });
221
-
222
- const frame: RpcHostUriRequest = {
223
- type: "host_uri_request",
224
- id,
225
- operation,
226
- url,
227
- };
228
- if (operation === "write") {
229
- frame.content = content ?? "";
230
- }
231
- this.#output(frame);
232
-
233
- return promise;
234
- }
235
- }
1
+ export * from "../shared/agent-wire/host-uri-bridge";