@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.
- package/CHANGELOG.md +18 -0
- package/dist/types/async/job-manager.d.ts +7 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +4 -4
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +1 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
- package/dist/types/hooks/skill-state.d.ts +21 -0
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +143 -11
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +36 -0
- package/src/cli/args.ts +9 -2
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +289 -19
- package/src/commands/launch.ts +2 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +22 -4
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +6 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
- package/src/gjc-runtime/ralplan-runtime.ts +213 -36
- package/src/gjc-runtime/state-migrations.ts +54 -7
- package/src/gjc-runtime/state-runtime.ts +461 -64
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-writer.ts +32 -1
- package/src/gjc-runtime/team-runtime.ts +177 -105
- package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
- package/src/gjc-runtime/workflow-manifest.ts +3 -1
- package/src/harness-control-plane/control-endpoint.ts +19 -8
- package/src/harness-control-plane/owner.ts +57 -10
- package/src/harness-control-plane/state-machine.ts +2 -1
- package/src/hooks/skill-state.ts +176 -26
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +21 -5
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/hook-selector.ts +61 -18
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/extension-ui-controller.ts +39 -3
- package/src/modes/controllers/input-controller.ts +97 -9
- package/src/modes/controllers/selector-controller.ts +29 -0
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +27 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/tools/subagent.md +12 -7
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +214 -38
- package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
- package/src/skill-state/workflow-state-contract.ts +7 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/executor.ts +29 -5
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +257 -67
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +48 -131
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +48 -7
- package/src/tools/ask.ts +73 -33
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +237 -84
|
@@ -0,0 +1,194 @@
|
|
|
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 {
|
|
6
|
+
RpcHostToolCallRequest,
|
|
7
|
+
RpcHostToolCancelRequest,
|
|
8
|
+
RpcHostToolDefinition,
|
|
9
|
+
RpcHostToolResult,
|
|
10
|
+
RpcHostToolUpdate,
|
|
11
|
+
} from "../../rpc/rpc-types";
|
|
12
|
+
import type { Theme } from "../../theme/theme";
|
|
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 (
|
|
26
|
+
Array.isArray(content) &&
|
|
27
|
+
content.every(item => !!item && typeof item === "object" && typeof (item as { type?: unknown }).type === "string")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isRpcHostToolResult(value: unknown): value is RpcHostToolResult {
|
|
32
|
+
if (!value || typeof value !== "object") return false;
|
|
33
|
+
const frame = value as { type?: unknown; id?: unknown; result?: unknown; isError?: unknown };
|
|
34
|
+
return (
|
|
35
|
+
frame.type === "host_tool_result" &&
|
|
36
|
+
typeof frame.id === "string" &&
|
|
37
|
+
isAgentToolResult(frame.result) &&
|
|
38
|
+
(frame.isError === undefined || typeof frame.isError === "boolean")
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isRpcHostToolUpdate(value: unknown): value is RpcHostToolUpdate {
|
|
43
|
+
if (!value || typeof value !== "object") return false;
|
|
44
|
+
const frame = value as { type?: unknown; id?: unknown; partialResult?: unknown };
|
|
45
|
+
return frame.type === "host_tool_update" && typeof frame.id === "string" && isAgentToolResult(frame.partialResult);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class RpcHostToolAdapter<TParams extends TSchema = TSchema, TTheme extends Theme = Theme>
|
|
49
|
+
implements AgentTool<TParams, unknown, TTheme>
|
|
50
|
+
{
|
|
51
|
+
declare name: string;
|
|
52
|
+
declare label: string;
|
|
53
|
+
declare description: string;
|
|
54
|
+
declare parameters: TParams;
|
|
55
|
+
readonly strict = true;
|
|
56
|
+
concurrency: "shared" | "exclusive" = "shared";
|
|
57
|
+
#bridge: RpcHostToolBridge;
|
|
58
|
+
#definition: RpcHostToolDefinition;
|
|
59
|
+
|
|
60
|
+
constructor(definition: RpcHostToolDefinition, bridge: RpcHostToolBridge) {
|
|
61
|
+
this.#definition = definition;
|
|
62
|
+
this.#bridge = bridge;
|
|
63
|
+
applyToolProxy(definition, this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
execute(
|
|
67
|
+
toolCallId: string,
|
|
68
|
+
params: Static<TParams>,
|
|
69
|
+
signal?: AbortSignal,
|
|
70
|
+
onUpdate?: AgentToolUpdateCallback<unknown>,
|
|
71
|
+
): Promise<AgentToolResult<unknown>> {
|
|
72
|
+
return this.#bridge.requestExecution(
|
|
73
|
+
this.#definition,
|
|
74
|
+
toolCallId,
|
|
75
|
+
params as Record<string, unknown>,
|
|
76
|
+
signal,
|
|
77
|
+
onUpdate,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class RpcHostToolBridge {
|
|
83
|
+
#output: RpcHostToolOutput;
|
|
84
|
+
#definitions = new Map<string, RpcHostToolDefinition>();
|
|
85
|
+
#pendingCalls = new Map<string, PendingHostToolCall>();
|
|
86
|
+
|
|
87
|
+
constructor(output: RpcHostToolOutput) {
|
|
88
|
+
this.#output = output;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getToolNames(): string[] {
|
|
92
|
+
return Array.from(this.#definitions.keys());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setTools(tools: RpcHostToolDefinition[]): AgentTool[] {
|
|
96
|
+
this.#definitions = new Map(tools.map(tool => [tool.name, tool]));
|
|
97
|
+
return tools.map(tool => new RpcHostToolAdapter(tool, this));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
handleResult(frame: RpcHostToolResult): boolean {
|
|
101
|
+
const pending = this.#pendingCalls.get(frame.id);
|
|
102
|
+
if (!pending) return false;
|
|
103
|
+
this.#pendingCalls.delete(frame.id);
|
|
104
|
+
if (frame.isError) {
|
|
105
|
+
const text = frame.result.content
|
|
106
|
+
.filter(
|
|
107
|
+
(item): item is { type: "text"; text: string } => item.type === "text" && typeof item.text === "string",
|
|
108
|
+
)
|
|
109
|
+
.map(item => item.text)
|
|
110
|
+
.join("\n")
|
|
111
|
+
.trim();
|
|
112
|
+
pending.reject(new Error(text || "Host tool execution failed"));
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
pending.resolve(frame.result);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
handleUpdate(frame: RpcHostToolUpdate): boolean {
|
|
120
|
+
const pending = this.#pendingCalls.get(frame.id);
|
|
121
|
+
if (!pending) return false;
|
|
122
|
+
pending.onUpdate?.(frame.partialResult);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
requestExecution(
|
|
127
|
+
definition: RpcHostToolDefinition,
|
|
128
|
+
toolCallId: string,
|
|
129
|
+
args: Record<string, unknown>,
|
|
130
|
+
signal?: AbortSignal,
|
|
131
|
+
onUpdate?: AgentToolUpdateCallback<unknown>,
|
|
132
|
+
): Promise<AgentToolResult<unknown>> {
|
|
133
|
+
if (signal?.aborted) {
|
|
134
|
+
return Promise.reject(new Error(`Host tool "${definition.name}" was aborted`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const id = Snowflake.next() as string;
|
|
138
|
+
const { promise, resolve, reject } = Promise.withResolvers<AgentToolResult<unknown>>();
|
|
139
|
+
let settled = false;
|
|
140
|
+
|
|
141
|
+
const cleanup = () => {
|
|
142
|
+
signal?.removeEventListener("abort", onAbort);
|
|
143
|
+
this.#pendingCalls.delete(id);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const onAbort = () => {
|
|
147
|
+
if (settled) return;
|
|
148
|
+
settled = true;
|
|
149
|
+
cleanup();
|
|
150
|
+
this.#output({
|
|
151
|
+
type: "host_tool_cancel",
|
|
152
|
+
id: Snowflake.next() as string,
|
|
153
|
+
targetId: id,
|
|
154
|
+
});
|
|
155
|
+
reject(new Error(`Host tool "${definition.name}" was aborted`));
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
159
|
+
this.#pendingCalls.set(id, {
|
|
160
|
+
resolve: result => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
settled = true;
|
|
163
|
+
cleanup();
|
|
164
|
+
resolve(result);
|
|
165
|
+
},
|
|
166
|
+
reject: error => {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
cleanup();
|
|
170
|
+
reject(error);
|
|
171
|
+
},
|
|
172
|
+
onUpdate,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
this.#output({
|
|
176
|
+
type: "host_tool_call",
|
|
177
|
+
id,
|
|
178
|
+
toolCallId,
|
|
179
|
+
toolName: definition.name,
|
|
180
|
+
arguments: args,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return promise;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
rejectAllPending(message: string): void {
|
|
187
|
+
const error = new Error(message);
|
|
188
|
+
const pendingCalls = Array.from(this.#pendingCalls.values());
|
|
189
|
+
this.#pendingCalls.clear();
|
|
190
|
+
for (const pending of pendingCalls) {
|
|
191
|
+
pending.reject(error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
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/rpc-types";
|
|
16
|
+
|
|
17
|
+
const RESERVED_INTERNAL_URI_SCHEMES = new Set(["gjc", "agent", "artifact", "memory", "local", "rule", "issue", "pr"]);
|
|
18
|
+
|
|
19
|
+
type RpcHostUriOutput = (frame: RpcHostUriRequest | RpcHostUriCancelRequest) => void;
|
|
20
|
+
|
|
21
|
+
type PendingUriRequest = {
|
|
22
|
+
operation: "read" | "write";
|
|
23
|
+
url: string;
|
|
24
|
+
resolve: (frame: RpcHostUriResult) => void;
|
|
25
|
+
reject: (error: Error) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function isRpcHostUriResult(value: unknown): value is RpcHostUriResult {
|
|
29
|
+
if (!value || typeof value !== "object") return false;
|
|
30
|
+
const frame = value as {
|
|
31
|
+
type?: unknown;
|
|
32
|
+
id?: unknown;
|
|
33
|
+
content?: unknown;
|
|
34
|
+
contentType?: unknown;
|
|
35
|
+
notes?: unknown;
|
|
36
|
+
immutable?: unknown;
|
|
37
|
+
isError?: unknown;
|
|
38
|
+
error?: unknown;
|
|
39
|
+
};
|
|
40
|
+
return (
|
|
41
|
+
frame.type === "host_uri_result" &&
|
|
42
|
+
typeof frame.id === "string" &&
|
|
43
|
+
(frame.content === undefined || typeof frame.content === "string") &&
|
|
44
|
+
(frame.contentType === undefined ||
|
|
45
|
+
frame.contentType === "text/markdown" ||
|
|
46
|
+
frame.contentType === "application/json" ||
|
|
47
|
+
frame.contentType === "text/plain") &&
|
|
48
|
+
(frame.notes === undefined ||
|
|
49
|
+
(Array.isArray(frame.notes) && frame.notes.every(note => typeof note === "string"))) &&
|
|
50
|
+
(frame.immutable === undefined || typeof frame.immutable === "boolean") &&
|
|
51
|
+
(frame.isError === undefined || typeof frame.isError === "boolean") &&
|
|
52
|
+
(frame.error === undefined || typeof frame.error === "string")
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class RpcHostUriProtocolHandler implements ProtocolHandler {
|
|
57
|
+
readonly scheme: string;
|
|
58
|
+
readonly immutable: boolean;
|
|
59
|
+
readonly write?: (url: InternalUrl, content: string, context?: WriteContext) => Promise<void>;
|
|
60
|
+
readonly #bridge: RpcHostUriBridge;
|
|
61
|
+
|
|
62
|
+
constructor(definition: RpcHostUriSchemeDefinition, bridge: RpcHostUriBridge) {
|
|
63
|
+
this.scheme = definition.scheme;
|
|
64
|
+
this.immutable = definition.immutable === true;
|
|
65
|
+
this.#bridge = bridge;
|
|
66
|
+
if (definition.writable === true) {
|
|
67
|
+
this.write = (url, content, context) => this.#bridge.requestWrite(this.scheme, url, content, context);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
72
|
+
return this.#bridge.requestRead(this.scheme, url, context);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class RpcHostUriBridge {
|
|
77
|
+
#output: RpcHostUriOutput;
|
|
78
|
+
#router: InternalUrlRouter;
|
|
79
|
+
#definitions = new Map<string, RpcHostUriSchemeDefinition>();
|
|
80
|
+
#pending = new Map<string, PendingUriRequest>();
|
|
81
|
+
|
|
82
|
+
constructor(output: RpcHostUriOutput, router: InternalUrlRouter = InternalUrlRouter.instance()) {
|
|
83
|
+
this.#output = output;
|
|
84
|
+
this.#router = router;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getSchemes(): string[] {
|
|
88
|
+
return Array.from(this.#definitions.keys());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setSchemes(schemes: RpcHostUriSchemeDefinition[]): string[] {
|
|
92
|
+
const normalized = new Map<string, RpcHostUriSchemeDefinition>();
|
|
93
|
+
for (const raw of schemes) {
|
|
94
|
+
const scheme = typeof raw?.scheme === "string" ? raw.scheme.trim().toLowerCase() : "";
|
|
95
|
+
if (!scheme) {
|
|
96
|
+
throw new Error("Host URI scheme must be a non-empty string");
|
|
97
|
+
}
|
|
98
|
+
if (!/^[a-z][a-z0-9+.-]*$/.test(scheme)) {
|
|
99
|
+
throw new Error(`Host URI scheme contains invalid characters: ${raw.scheme}`);
|
|
100
|
+
}
|
|
101
|
+
if (RESERVED_INTERNAL_URI_SCHEMES.has(scheme)) {
|
|
102
|
+
throw new Error(`Host URI scheme is reserved: ${scheme}`);
|
|
103
|
+
}
|
|
104
|
+
normalized.set(scheme, {
|
|
105
|
+
scheme,
|
|
106
|
+
description: typeof raw.description === "string" ? raw.description : undefined,
|
|
107
|
+
writable: raw.writable === true,
|
|
108
|
+
immutable: raw.immutable === true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const previous of this.#definitions.keys()) {
|
|
113
|
+
if (!normalized.has(previous)) {
|
|
114
|
+
this.#router.unregister(previous);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const definition of normalized.values()) {
|
|
118
|
+
this.#router.register(new RpcHostUriProtocolHandler(definition, this));
|
|
119
|
+
}
|
|
120
|
+
this.#definitions = normalized;
|
|
121
|
+
return Array.from(normalized.keys());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
clear(message: string = "Host URI bridge shut down"): void {
|
|
125
|
+
for (const scheme of this.#definitions.keys()) {
|
|
126
|
+
this.#router.unregister(scheme);
|
|
127
|
+
}
|
|
128
|
+
this.#definitions.clear();
|
|
129
|
+
this.rejectAllPending(message);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
handleResult(frame: RpcHostUriResult): boolean {
|
|
133
|
+
const pending = this.#pending.get(frame.id);
|
|
134
|
+
if (!pending) return false;
|
|
135
|
+
this.#pending.delete(frame.id);
|
|
136
|
+
pending.resolve(frame);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
rejectAllPending(message: string): void {
|
|
141
|
+
const error = new Error(message);
|
|
142
|
+
const pending = Array.from(this.#pending.values());
|
|
143
|
+
this.#pending.clear();
|
|
144
|
+
for (const entry of pending) {
|
|
145
|
+
entry.reject(error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async requestRead(scheme: string, url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
150
|
+
const result = await this.#dispatch("read", url.href, undefined, context?.signal);
|
|
151
|
+
if (result.isError) {
|
|
152
|
+
throw new Error(result.error || result.content || `Host URI read failed for ${url.href}`);
|
|
153
|
+
}
|
|
154
|
+
const content = result.content ?? "";
|
|
155
|
+
const contentType = result.contentType ?? "text/plain";
|
|
156
|
+
const definition = this.#definitions.get(scheme);
|
|
157
|
+
return {
|
|
158
|
+
url: url.href,
|
|
159
|
+
content,
|
|
160
|
+
contentType,
|
|
161
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
162
|
+
notes: result.notes && result.notes.length > 0 ? [...result.notes] : undefined,
|
|
163
|
+
immutable: result.immutable ?? definition?.immutable === true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async requestWrite(_scheme: string, url: InternalUrl, content: string, context?: WriteContext): Promise<void> {
|
|
168
|
+
const result = await this.#dispatch("write", url.href, content, context?.signal);
|
|
169
|
+
if (result.isError) {
|
|
170
|
+
throw new Error(result.error || result.content || `Host URI write failed for ${url.href}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#dispatch(
|
|
175
|
+
operation: "read" | "write",
|
|
176
|
+
url: string,
|
|
177
|
+
content: string | undefined,
|
|
178
|
+
signal: AbortSignal | undefined,
|
|
179
|
+
): Promise<RpcHostUriResult> {
|
|
180
|
+
if (signal?.aborted) {
|
|
181
|
+
return Promise.reject(new Error(`Host URI ${operation} for ${url} was aborted`));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const id = Snowflake.next() as string;
|
|
185
|
+
const { promise, resolve, reject } = Promise.withResolvers<RpcHostUriResult>();
|
|
186
|
+
let settled = false;
|
|
187
|
+
|
|
188
|
+
const cleanup = () => {
|
|
189
|
+
signal?.removeEventListener("abort", onAbort);
|
|
190
|
+
this.#pending.delete(id);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const onAbort = () => {
|
|
194
|
+
if (settled) return;
|
|
195
|
+
settled = true;
|
|
196
|
+
cleanup();
|
|
197
|
+
this.#output({
|
|
198
|
+
type: "host_uri_cancel",
|
|
199
|
+
id: Snowflake.next() as string,
|
|
200
|
+
targetId: id,
|
|
201
|
+
});
|
|
202
|
+
reject(new Error(`Host URI ${operation} for ${url} was aborted`));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
206
|
+
this.#pending.set(id, {
|
|
207
|
+
operation,
|
|
208
|
+
url,
|
|
209
|
+
resolve: frame => {
|
|
210
|
+
if (settled) return;
|
|
211
|
+
settled = true;
|
|
212
|
+
cleanup();
|
|
213
|
+
resolve(frame);
|
|
214
|
+
},
|
|
215
|
+
reject: err => {
|
|
216
|
+
if (settled) return;
|
|
217
|
+
settled = true;
|
|
218
|
+
cleanup();
|
|
219
|
+
reject(err);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const frame: RpcHostUriRequest = {
|
|
224
|
+
type: "host_uri_request",
|
|
225
|
+
id,
|
|
226
|
+
operation,
|
|
227
|
+
url,
|
|
228
|
+
};
|
|
229
|
+
if (operation === "write") {
|
|
230
|
+
frame.content = content ?? "";
|
|
231
|
+
}
|
|
232
|
+
this.#output(frame);
|
|
233
|
+
|
|
234
|
+
return promise;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared agent-wire protocol primitives for GJC bridge surfaces.
|
|
3
|
+
*
|
|
4
|
+
* This module is the transport-agnostic, versioned frame contract that the
|
|
5
|
+
* RPC mode and the (in-progress) `--mode bridge` wiring site both build on.
|
|
6
|
+
* It carries the SEMANTIC agent surface — events, responses, and UI/permission
|
|
7
|
+
* requests — never pixels. See `.gjc/specs/deep-interview-gjc-backend-bridge.md`
|
|
8
|
+
* and `.gjc/plans/ralplan/gjc-backend-bridge/pending-approval.md`.
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentSessionEvent } from "../../../session/agent-session";
|
|
11
|
+
|
|
12
|
+
/** Wire protocol version. Bump on breaking envelope/semantic changes. */
|
|
13
|
+
export const BRIDGE_PROTOCOL_VERSION = 1 as const;
|
|
14
|
+
|
|
15
|
+
/** The discriminant of every `AgentSessionEvent` the agent can emit. */
|
|
16
|
+
export type AgentSessionEventType = AgentSessionEvent["type"];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compile-time exhaustive registry of every `AgentSessionEvent` variant.
|
|
20
|
+
*
|
|
21
|
+
* Adding a new variant to `AgentSessionEvent` without registering it here is a
|
|
22
|
+
* type error. This keeps the bridge wire surface in lockstep with the agent
|
|
23
|
+
* event union — the "event/element drift → silent incompleteness" mitigation
|
|
24
|
+
* from the plan's pre-mortem.
|
|
25
|
+
*/
|
|
26
|
+
const AGENT_SESSION_EVENT_TYPE_REGISTRY: Record<AgentSessionEventType, true> = {
|
|
27
|
+
agent_start: true,
|
|
28
|
+
agent_end: true,
|
|
29
|
+
turn_start: true,
|
|
30
|
+
turn_end: true,
|
|
31
|
+
message_start: true,
|
|
32
|
+
message_update: true,
|
|
33
|
+
message_end: true,
|
|
34
|
+
tool_execution_start: true,
|
|
35
|
+
tool_execution_update: true,
|
|
36
|
+
tool_execution_end: true,
|
|
37
|
+
auto_compaction_start: true,
|
|
38
|
+
auto_compaction_end: true,
|
|
39
|
+
auto_retry_start: true,
|
|
40
|
+
auto_retry_end: true,
|
|
41
|
+
retry_fallback_applied: true,
|
|
42
|
+
retry_fallback_succeeded: true,
|
|
43
|
+
ttsr_triggered: true,
|
|
44
|
+
todo_reminder: true,
|
|
45
|
+
todo_auto_clear: true,
|
|
46
|
+
irc_message: true,
|
|
47
|
+
notice: true,
|
|
48
|
+
thinking_level_changed: true,
|
|
49
|
+
goal_updated: true,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Every agent-session event type, derived from the exhaustive registry. */
|
|
53
|
+
export const AGENT_SESSION_EVENT_TYPES: readonly AgentSessionEventType[] = Object.keys(
|
|
54
|
+
AGENT_SESSION_EVENT_TYPE_REGISTRY,
|
|
55
|
+
) as AgentSessionEventType[];
|
|
56
|
+
|
|
57
|
+
/** Top-level frame categories carried over any bridge transport. */
|
|
58
|
+
export type BridgeFrameType =
|
|
59
|
+
| "ready"
|
|
60
|
+
| "event"
|
|
61
|
+
| "response"
|
|
62
|
+
| "ui_request"
|
|
63
|
+
| "permission_request"
|
|
64
|
+
| "host_tool_call"
|
|
65
|
+
| "host_uri_request"
|
|
66
|
+
| "reset"
|
|
67
|
+
| "error";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Universal frame envelope. Every frame on every transport carries these
|
|
71
|
+
* fields so clients can order (`seq`), resume (`seq` cursor), and correlate
|
|
72
|
+
* request/response pairs (`correlation_id`). `session_id` is present from v1
|
|
73
|
+
* even though v1 runs one session per process, so in-process multiplexing is
|
|
74
|
+
* an additive, non-breaking change later.
|
|
75
|
+
*/
|
|
76
|
+
export interface BridgeFrameEnvelope<TType extends BridgeFrameType = BridgeFrameType, TPayload = unknown> {
|
|
77
|
+
protocol_version: typeof BRIDGE_PROTOCOL_VERSION;
|
|
78
|
+
session_id: string;
|
|
79
|
+
/** Monotonic per-session sequence number, starting at 1. */
|
|
80
|
+
seq: number;
|
|
81
|
+
/** Unique id for this frame. */
|
|
82
|
+
frame_id: string;
|
|
83
|
+
/** Ties a request frame to its response frame, when applicable. */
|
|
84
|
+
correlation_id?: string;
|
|
85
|
+
type: TType;
|
|
86
|
+
payload: TPayload;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Payload carried by an `event` frame. */
|
|
90
|
+
export interface BridgeEventPayload {
|
|
91
|
+
event_type: AgentSessionEventType;
|
|
92
|
+
event: AgentSessionEvent;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** An `AgentSessionEvent` serialized into a versioned wire frame. */
|
|
96
|
+
export type BridgeEventFrame = BridgeFrameEnvelope<"event", BridgeEventPayload>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Shared RPC-compatible response helpers for agent-wire consumers. */
|
|
2
|
+
import type { RpcCommand, RpcResponse } from "../../rpc/rpc-types";
|
|
3
|
+
|
|
4
|
+
export function rpcSuccess<T extends RpcCommand["type"]>(
|
|
5
|
+
id: string | undefined,
|
|
6
|
+
command: T,
|
|
7
|
+
data?: object | null,
|
|
8
|
+
): RpcResponse {
|
|
9
|
+
if (data === undefined) {
|
|
10
|
+
return { id, type: "response", command, success: true } as RpcResponse;
|
|
11
|
+
}
|
|
12
|
+
return { id, type: "response", command, success: true, data } as RpcResponse;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function rpcError(id: string | undefined, command: string, message: string): RpcResponse {
|
|
16
|
+
return { id, type: "response", command, success: false, error: message };
|
|
17
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coarse bridge authorization scopes for RPC commands.
|
|
3
|
+
*
|
|
4
|
+
* The v1 bridge exposes a network-reachable control surface, so every
|
|
5
|
+
* `RpcCommand` must be assigned to one coarse scope before any REST handler can
|
|
6
|
+
* dispatch it. The registry is intentionally typed as `Record<RpcCommandType,
|
|
7
|
+
* BridgeCommandScope>` so adding a new RPC command without a scope is a compile
|
|
8
|
+
* failure.
|
|
9
|
+
*/
|
|
10
|
+
import type { RpcCommand } from "../../rpc/rpc-types";
|
|
11
|
+
|
|
12
|
+
export type RpcCommandType = RpcCommand["type"];
|
|
13
|
+
|
|
14
|
+
export type BridgeCommandScope =
|
|
15
|
+
| "prompt"
|
|
16
|
+
| "control"
|
|
17
|
+
| "bash"
|
|
18
|
+
| "export"
|
|
19
|
+
| "session"
|
|
20
|
+
| "model"
|
|
21
|
+
| "message:read"
|
|
22
|
+
| "host_tools"
|
|
23
|
+
| "host_uri"
|
|
24
|
+
| "admin";
|
|
25
|
+
export const BRIDGE_COMMAND_SCOPES: readonly BridgeCommandScope[] = [
|
|
26
|
+
"prompt",
|
|
27
|
+
"control",
|
|
28
|
+
"bash",
|
|
29
|
+
"export",
|
|
30
|
+
"session",
|
|
31
|
+
"model",
|
|
32
|
+
"message:read",
|
|
33
|
+
"host_tools",
|
|
34
|
+
"host_uri",
|
|
35
|
+
"admin",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const RPC_COMMAND_SCOPE_REGISTRY: Record<RpcCommandType, BridgeCommandScope> = {
|
|
39
|
+
prompt: "prompt",
|
|
40
|
+
steer: "prompt",
|
|
41
|
+
follow_up: "prompt",
|
|
42
|
+
abort: "prompt",
|
|
43
|
+
abort_and_prompt: "prompt",
|
|
44
|
+
new_session: "session",
|
|
45
|
+
get_state: "message:read",
|
|
46
|
+
set_todos: "control",
|
|
47
|
+
set_host_tools: "host_tools",
|
|
48
|
+
set_host_uri_schemes: "host_uri",
|
|
49
|
+
set_model: "model",
|
|
50
|
+
cycle_model: "model",
|
|
51
|
+
get_available_models: "model",
|
|
52
|
+
set_thinking_level: "model",
|
|
53
|
+
cycle_thinking_level: "model",
|
|
54
|
+
set_steering_mode: "control",
|
|
55
|
+
set_follow_up_mode: "control",
|
|
56
|
+
set_interrupt_mode: "control",
|
|
57
|
+
compact: "control",
|
|
58
|
+
set_auto_compaction: "control",
|
|
59
|
+
set_auto_retry: "control",
|
|
60
|
+
abort_retry: "control",
|
|
61
|
+
bash: "bash",
|
|
62
|
+
abort_bash: "bash",
|
|
63
|
+
get_session_stats: "message:read",
|
|
64
|
+
export_html: "export",
|
|
65
|
+
switch_session: "session",
|
|
66
|
+
branch: "session",
|
|
67
|
+
get_branch_messages: "session",
|
|
68
|
+
get_last_assistant_text: "message:read",
|
|
69
|
+
set_session_name: "session",
|
|
70
|
+
handoff: "admin",
|
|
71
|
+
get_messages: "message:read",
|
|
72
|
+
get_login_providers: "admin",
|
|
73
|
+
login: "admin",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const RPC_COMMAND_TYPES: readonly RpcCommandType[] = Object.keys(RPC_COMMAND_SCOPE_REGISTRY) as RpcCommandType[];
|
|
77
|
+
export function isRpcCommandType(value: unknown): value is RpcCommandType {
|
|
78
|
+
return typeof value === "string" && value in RPC_COMMAND_SCOPE_REGISTRY;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const MANDATORY_FLOOR_COMMAND_SCOPES: readonly BridgeCommandScope[] = ["prompt"];
|
|
82
|
+
|
|
83
|
+
export function scopeForRpcCommand(type: RpcCommandType): BridgeCommandScope {
|
|
84
|
+
return RPC_COMMAND_SCOPE_REGISTRY[type];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isRpcCommandAllowed(type: RpcCommandType, scopes: ReadonlySet<BridgeCommandScope>): boolean {
|
|
88
|
+
return scopes.has(scopeForRpcCommand(type));
|
|
89
|
+
}
|