@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,341 @@
|
|
|
1
|
+
import type { AgentTool } from "@gajae-code/agent-core";
|
|
2
|
+
import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
|
|
3
|
+
import { Snowflake } from "@gajae-code/utils";
|
|
4
|
+
import type { ExtensionUIContext } from "../../../extensibility/extensions";
|
|
5
|
+
import type { AgentSession } from "../../../session/agent-session";
|
|
6
|
+
import type {
|
|
7
|
+
RpcCommand,
|
|
8
|
+
RpcExtensionUIRequest,
|
|
9
|
+
RpcHostToolDefinition,
|
|
10
|
+
RpcHostUriSchemeDefinition,
|
|
11
|
+
RpcResponse,
|
|
12
|
+
RpcSessionState,
|
|
13
|
+
} from "../../rpc/rpc-types";
|
|
14
|
+
import { rpcError, rpcSuccess } from "./responses";
|
|
15
|
+
|
|
16
|
+
export type RpcCommandDispatchOutput = (obj: RpcResponse | RpcExtensionUIRequest | object) => void;
|
|
17
|
+
|
|
18
|
+
export interface RpcHostToolRegistry {
|
|
19
|
+
setTools(tools: RpcHostToolDefinition[]): AgentTool[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RpcHostUriRegistry {
|
|
23
|
+
setSchemes(schemes: RpcHostUriSchemeDefinition[]): string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RpcCommandDispatchContext {
|
|
27
|
+
session: AgentSession;
|
|
28
|
+
output: RpcCommandDispatchOutput;
|
|
29
|
+
hostToolRegistry: RpcHostToolRegistry;
|
|
30
|
+
hostUriRegistry: RpcHostUriRegistry;
|
|
31
|
+
createUiContext: () => Pick<ExtensionUIContext, "notify">;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostToolDefinition[] {
|
|
35
|
+
return tools.map((tool, index) => {
|
|
36
|
+
const name = typeof tool.name === "string" ? tool.name.trim() : "";
|
|
37
|
+
if (!name) {
|
|
38
|
+
throw new Error(`Host tool at index ${index} must provide a non-empty name`);
|
|
39
|
+
}
|
|
40
|
+
const description = typeof tool.description === "string" ? tool.description.trim() : "";
|
|
41
|
+
if (!description) {
|
|
42
|
+
throw new Error(`Host tool "${name}" must provide a non-empty description`);
|
|
43
|
+
}
|
|
44
|
+
if (!tool.parameters || typeof tool.parameters !== "object" || Array.isArray(tool.parameters)) {
|
|
45
|
+
throw new Error(`Host tool "${name}" must provide a JSON Schema object`);
|
|
46
|
+
}
|
|
47
|
+
const label = typeof tool.label === "string" && tool.label.trim() ? tool.label.trim() : name;
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
label,
|
|
51
|
+
description,
|
|
52
|
+
parameters: tool.parameters,
|
|
53
|
+
hidden: tool.hidden === true,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function dispatchRpcCommand(
|
|
59
|
+
command: RpcCommand,
|
|
60
|
+
context: RpcCommandDispatchContext,
|
|
61
|
+
): Promise<RpcResponse> {
|
|
62
|
+
const { session, output, hostToolRegistry, hostUriRegistry, createUiContext } = context;
|
|
63
|
+
const id = command.id;
|
|
64
|
+
|
|
65
|
+
switch (command.type) {
|
|
66
|
+
case "prompt": {
|
|
67
|
+
session
|
|
68
|
+
.prompt(command.message, {
|
|
69
|
+
images: command.images,
|
|
70
|
+
streamingBehavior: command.streamingBehavior,
|
|
71
|
+
})
|
|
72
|
+
.catch(e => output(rpcError(id, "prompt", e.message)));
|
|
73
|
+
return rpcSuccess(id, "prompt");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "steer": {
|
|
77
|
+
await session.steer(command.message, command.images);
|
|
78
|
+
return rpcSuccess(id, "steer");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case "follow_up": {
|
|
82
|
+
await session.followUp(command.message, command.images);
|
|
83
|
+
return rpcSuccess(id, "follow_up");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "abort": {
|
|
87
|
+
await session.abort();
|
|
88
|
+
return rpcSuccess(id, "abort");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case "abort_and_prompt": {
|
|
92
|
+
await session.abort();
|
|
93
|
+
session
|
|
94
|
+
.prompt(command.message, { images: command.images })
|
|
95
|
+
.catch(e => output(rpcError(id, "abort_and_prompt", e.message)));
|
|
96
|
+
return rpcSuccess(id, "abort_and_prompt");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case "new_session": {
|
|
100
|
+
const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
|
|
101
|
+
const cancelled = !(await session.newSession(options));
|
|
102
|
+
return rpcSuccess(id, "new_session", { cancelled });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "get_state": {
|
|
106
|
+
const state: RpcSessionState = {
|
|
107
|
+
model: session.model,
|
|
108
|
+
thinkingLevel: session.thinkingLevel,
|
|
109
|
+
isStreaming: session.isStreaming,
|
|
110
|
+
isCompacting: session.isCompacting,
|
|
111
|
+
steeringMode: session.steeringMode,
|
|
112
|
+
followUpMode: session.followUpMode,
|
|
113
|
+
interruptMode: session.interruptMode,
|
|
114
|
+
sessionFile: session.sessionFile,
|
|
115
|
+
sessionId: session.sessionId,
|
|
116
|
+
sessionName: session.sessionName,
|
|
117
|
+
autoCompactionEnabled: session.autoCompactionEnabled,
|
|
118
|
+
messageCount: session.messages.length,
|
|
119
|
+
queuedMessageCount: session.queuedMessageCount,
|
|
120
|
+
todoPhases: session.getTodoPhases(),
|
|
121
|
+
systemPrompt: session.systemPrompt,
|
|
122
|
+
dumpTools: session.agent.state.tools.map(tool => ({
|
|
123
|
+
name: tool.name,
|
|
124
|
+
description: tool.description,
|
|
125
|
+
parameters: tool.parameters,
|
|
126
|
+
})),
|
|
127
|
+
contextUsage: session.getContextUsage(),
|
|
128
|
+
};
|
|
129
|
+
return rpcSuccess(id, "get_state", state);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case "set_todos": {
|
|
133
|
+
session.setTodoPhases(command.phases);
|
|
134
|
+
return rpcSuccess(id, "set_todos", { todoPhases: session.getTodoPhases() });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case "set_host_tools": {
|
|
138
|
+
const tools = normalizeHostToolDefinitions(command.tools);
|
|
139
|
+
const rpcTools = hostToolRegistry.setTools(tools);
|
|
140
|
+
await session.refreshRpcHostTools(rpcTools);
|
|
141
|
+
return rpcSuccess(id, "set_host_tools", { toolNames: tools.map(tool => tool.name) });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case "set_host_uri_schemes": {
|
|
145
|
+
try {
|
|
146
|
+
const schemes = hostUriRegistry.setSchemes(command.schemes);
|
|
147
|
+
return rpcSuccess(id, "set_host_uri_schemes", { schemes });
|
|
148
|
+
} catch (err) {
|
|
149
|
+
return rpcError(id, "set_host_uri_schemes", err instanceof Error ? err.message : String(err));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case "set_model": {
|
|
154
|
+
const models = session.getAvailableModels();
|
|
155
|
+
const model = models.find(m => m.provider === command.provider && m.id === command.modelId);
|
|
156
|
+
if (!model) {
|
|
157
|
+
return rpcError(id, "set_model", `Model not found: ${command.provider}/${command.modelId}`);
|
|
158
|
+
}
|
|
159
|
+
await session.setModel(model);
|
|
160
|
+
return rpcSuccess(id, "set_model", model);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case "cycle_model": {
|
|
164
|
+
const result = await session.cycleModel();
|
|
165
|
+
if (!result) {
|
|
166
|
+
return rpcSuccess(id, "cycle_model", null);
|
|
167
|
+
}
|
|
168
|
+
return rpcSuccess(id, "cycle_model", result);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case "get_available_models": {
|
|
172
|
+
const models = session.getAvailableModels();
|
|
173
|
+
return rpcSuccess(id, "get_available_models", { models });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
case "set_thinking_level": {
|
|
177
|
+
session.setThinkingLevel(command.level);
|
|
178
|
+
return rpcSuccess(id, "set_thinking_level");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case "cycle_thinking_level": {
|
|
182
|
+
const level = session.cycleThinkingLevel();
|
|
183
|
+
if (!level) {
|
|
184
|
+
return rpcSuccess(id, "cycle_thinking_level", null);
|
|
185
|
+
}
|
|
186
|
+
return rpcSuccess(id, "cycle_thinking_level", { level });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case "set_steering_mode": {
|
|
190
|
+
session.setSteeringMode(command.mode);
|
|
191
|
+
return rpcSuccess(id, "set_steering_mode");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case "set_follow_up_mode": {
|
|
195
|
+
session.setFollowUpMode(command.mode);
|
|
196
|
+
return rpcSuccess(id, "set_follow_up_mode");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "set_interrupt_mode": {
|
|
200
|
+
session.setInterruptMode(command.mode);
|
|
201
|
+
return rpcSuccess(id, "set_interrupt_mode");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case "compact": {
|
|
205
|
+
const result = await session.compact(command.customInstructions);
|
|
206
|
+
return rpcSuccess(id, "compact", result);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case "set_auto_compaction": {
|
|
210
|
+
session.setAutoCompactionEnabled(command.enabled);
|
|
211
|
+
return rpcSuccess(id, "set_auto_compaction");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case "set_auto_retry": {
|
|
215
|
+
session.setAutoRetryEnabled(command.enabled);
|
|
216
|
+
return rpcSuccess(id, "set_auto_retry");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case "abort_retry": {
|
|
220
|
+
session.abortRetry();
|
|
221
|
+
return rpcSuccess(id, "abort_retry");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case "bash": {
|
|
225
|
+
const result = await session.executeBash(command.command);
|
|
226
|
+
return rpcSuccess(id, "bash", result);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case "abort_bash": {
|
|
230
|
+
session.abortBash();
|
|
231
|
+
return rpcSuccess(id, "abort_bash");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case "get_session_stats": {
|
|
235
|
+
const stats = session.getSessionStats();
|
|
236
|
+
return rpcSuccess(id, "get_session_stats", stats);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case "export_html": {
|
|
240
|
+
const path = await session.exportToHtml(command.outputPath);
|
|
241
|
+
return rpcSuccess(id, "export_html", { path });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case "switch_session": {
|
|
245
|
+
const cancelled = !(await session.switchSession(command.sessionPath));
|
|
246
|
+
return rpcSuccess(id, "switch_session", { cancelled });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case "branch": {
|
|
250
|
+
const result = await session.branch(command.entryId);
|
|
251
|
+
return rpcSuccess(id, "branch", { text: result.selectedText, cancelled: result.cancelled });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
case "get_branch_messages": {
|
|
255
|
+
const messages = session.getUserMessagesForBranching();
|
|
256
|
+
return rpcSuccess(id, "get_branch_messages", { messages });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case "get_last_assistant_text": {
|
|
260
|
+
const text = session.getLastAssistantText();
|
|
261
|
+
return rpcSuccess(id, "get_last_assistant_text", { text });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case "set_session_name": {
|
|
265
|
+
const name = command.name.trim();
|
|
266
|
+
if (!name) {
|
|
267
|
+
return rpcError(id, "set_session_name", "Session name cannot be empty");
|
|
268
|
+
}
|
|
269
|
+
const applied = await session.setSessionName(name, "user");
|
|
270
|
+
if (!applied) {
|
|
271
|
+
return rpcError(id, "set_session_name", "Session name cannot be empty");
|
|
272
|
+
}
|
|
273
|
+
return rpcSuccess(id, "set_session_name");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case "handoff": {
|
|
277
|
+
const result = await session.handoff(command.customInstructions);
|
|
278
|
+
return rpcSuccess(id, "handoff", result ? { savedPath: result.savedPath } : null);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
case "get_messages": {
|
|
282
|
+
return rpcSuccess(id, "get_messages", { messages: session.messages });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case "get_login_providers": {
|
|
286
|
+
const providers = getOAuthProviders().map(provider => ({
|
|
287
|
+
id: provider.id,
|
|
288
|
+
name: provider.name,
|
|
289
|
+
available: provider.available,
|
|
290
|
+
authenticated: session.modelRegistry.authStorage.hasAuth(provider.id),
|
|
291
|
+
}));
|
|
292
|
+
return rpcSuccess(id, "get_login_providers", { providers });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case "login": {
|
|
296
|
+
const knownProvider = getOAuthProviders().find(p => p.id === command.providerId);
|
|
297
|
+
if (!knownProvider) {
|
|
298
|
+
return rpcError(id, "login", `Unknown OAuth provider: ${command.providerId}`);
|
|
299
|
+
}
|
|
300
|
+
const uiCtx = createUiContext();
|
|
301
|
+
let authEmitted = false;
|
|
302
|
+
try {
|
|
303
|
+
await session.modelRegistry.authStorage.login(command.providerId, {
|
|
304
|
+
onAuth: info => {
|
|
305
|
+
authEmitted = true;
|
|
306
|
+
output({
|
|
307
|
+
type: "extension_ui_request",
|
|
308
|
+
id: Snowflake.next() as string,
|
|
309
|
+
method: "open_url",
|
|
310
|
+
url: info.url,
|
|
311
|
+
instructions: info.instructions,
|
|
312
|
+
} as RpcExtensionUIRequest);
|
|
313
|
+
},
|
|
314
|
+
onProgress: message => {
|
|
315
|
+
uiCtx.notify(message, "info");
|
|
316
|
+
},
|
|
317
|
+
onPrompt: () => {
|
|
318
|
+
if (!authEmitted) {
|
|
319
|
+
return Promise.reject(
|
|
320
|
+
new Error(
|
|
321
|
+
`Provider '${command.providerId}' requires interactive prompts ` +
|
|
322
|
+
"which are not supported in RPC mode. Use the terminal UI to log in.",
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return new Promise<string>(() => {});
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
await session.modelRegistry.refresh();
|
|
330
|
+
return rpcSuccess(id, "login", { providerId: command.providerId });
|
|
331
|
+
} catch (err: unknown) {
|
|
332
|
+
return rpcError(id, "login", err instanceof Error ? err.message : String(err));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
default: {
|
|
337
|
+
const unknownCommand = command as { type: string };
|
|
338
|
+
return rpcError(undefined, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { RpcCommand } from "../../rpc/rpc-types";
|
|
2
|
+
import { isRpcCommandType } from "./scopes";
|
|
3
|
+
|
|
4
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function optionalString(value: unknown): boolean {
|
|
9
|
+
return value === undefined || typeof value === "string";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function optionalArray(value: unknown): boolean {
|
|
13
|
+
return value === undefined || Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stringField(value: Record<string, unknown>, key: string): boolean {
|
|
17
|
+
return typeof value[key] === "string";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const THINKING_LEVELS = new Set(["inherit", "off", "minimal", "low", "medium", "high", "xhigh"]);
|
|
21
|
+
const TODO_STATUSES = new Set(["pending", "in_progress", "completed", "abandoned"]);
|
|
22
|
+
|
|
23
|
+
function optionalBoolean(value: unknown): boolean {
|
|
24
|
+
return value === undefined || typeof value === "boolean";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stringArray(value: unknown): value is string[] {
|
|
28
|
+
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function todoPhase(value: unknown): boolean {
|
|
32
|
+
if (!isRecord(value) || typeof value.name !== "string" || !Array.isArray(value.tasks)) return false;
|
|
33
|
+
return value.tasks.every(
|
|
34
|
+
task =>
|
|
35
|
+
isRecord(task) &&
|
|
36
|
+
typeof task.content === "string" &&
|
|
37
|
+
typeof task.status === "string" &&
|
|
38
|
+
TODO_STATUSES.has(task.status) &&
|
|
39
|
+
(task.notes === undefined || stringArray(task.notes)),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hostToolDefinition(value: unknown): boolean {
|
|
44
|
+
return (
|
|
45
|
+
isRecord(value) &&
|
|
46
|
+
typeof value.name === "string" &&
|
|
47
|
+
typeof value.description === "string" &&
|
|
48
|
+
isRecord(value.parameters) &&
|
|
49
|
+
optionalString(value.label) &&
|
|
50
|
+
optionalBoolean(value.hidden)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hostUriScheme(value: unknown): boolean {
|
|
55
|
+
return (
|
|
56
|
+
isRecord(value) &&
|
|
57
|
+
typeof value.scheme === "string" &&
|
|
58
|
+
optionalString(value.description) &&
|
|
59
|
+
optionalBoolean(value.writable) &&
|
|
60
|
+
optionalBoolean(value.immutable)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isRpcCommand(value: unknown): value is RpcCommand {
|
|
65
|
+
if (!isRecord(value) || !optionalString(value.id) || !isRpcCommandType(value.type)) return false;
|
|
66
|
+
switch (value.type) {
|
|
67
|
+
case "prompt":
|
|
68
|
+
return (
|
|
69
|
+
stringField(value, "message") &&
|
|
70
|
+
optionalArray(value.images) &&
|
|
71
|
+
(value.streamingBehavior === undefined ||
|
|
72
|
+
value.streamingBehavior === "steer" ||
|
|
73
|
+
value.streamingBehavior === "followUp")
|
|
74
|
+
);
|
|
75
|
+
case "steer":
|
|
76
|
+
case "follow_up":
|
|
77
|
+
return stringField(value, "message") && optionalArray(value.images);
|
|
78
|
+
case "abort":
|
|
79
|
+
case "get_state":
|
|
80
|
+
case "cycle_model":
|
|
81
|
+
case "get_available_models":
|
|
82
|
+
case "cycle_thinking_level":
|
|
83
|
+
case "abort_retry":
|
|
84
|
+
case "abort_bash":
|
|
85
|
+
case "get_session_stats":
|
|
86
|
+
case "get_branch_messages":
|
|
87
|
+
case "get_last_assistant_text":
|
|
88
|
+
case "get_messages":
|
|
89
|
+
case "get_login_providers":
|
|
90
|
+
return true;
|
|
91
|
+
case "abort_and_prompt":
|
|
92
|
+
return stringField(value, "message") && optionalArray(value.images);
|
|
93
|
+
case "new_session":
|
|
94
|
+
return optionalString(value.parentSession);
|
|
95
|
+
case "set_todos":
|
|
96
|
+
return Array.isArray(value.phases) && value.phases.every(todoPhase);
|
|
97
|
+
case "set_host_tools":
|
|
98
|
+
return Array.isArray(value.tools) && value.tools.every(hostToolDefinition);
|
|
99
|
+
case "set_host_uri_schemes":
|
|
100
|
+
return Array.isArray(value.schemes) && value.schemes.every(hostUriScheme);
|
|
101
|
+
case "set_model":
|
|
102
|
+
return stringField(value, "provider") && stringField(value, "modelId");
|
|
103
|
+
case "set_thinking_level":
|
|
104
|
+
return typeof value.level === "string" && THINKING_LEVELS.has(value.level);
|
|
105
|
+
case "set_steering_mode":
|
|
106
|
+
return value.mode === "all" || value.mode === "one-at-a-time";
|
|
107
|
+
case "set_follow_up_mode":
|
|
108
|
+
return value.mode === "all" || value.mode === "one-at-a-time";
|
|
109
|
+
case "set_interrupt_mode":
|
|
110
|
+
return value.mode === "immediate" || value.mode === "wait";
|
|
111
|
+
case "compact":
|
|
112
|
+
return optionalString(value.customInstructions);
|
|
113
|
+
case "set_auto_compaction":
|
|
114
|
+
case "set_auto_retry":
|
|
115
|
+
return typeof value.enabled === "boolean";
|
|
116
|
+
case "bash":
|
|
117
|
+
return stringField(value, "command");
|
|
118
|
+
case "export_html":
|
|
119
|
+
return optionalString(value.outputPath);
|
|
120
|
+
case "switch_session":
|
|
121
|
+
return stringField(value, "sessionPath");
|
|
122
|
+
case "branch":
|
|
123
|
+
return stringField(value, "entryId");
|
|
124
|
+
case "set_session_name":
|
|
125
|
+
return stringField(value, "name");
|
|
126
|
+
case "handoff":
|
|
127
|
+
return optionalString(value.customInstructions);
|
|
128
|
+
case "login":
|
|
129
|
+
return stringField(value, "providerId");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize `AgentSessionEvent`s into versioned bridge wire frames.
|
|
3
|
+
*
|
|
4
|
+
* The mapping is intentionally exhaustive: `agentSessionEventType` switches over
|
|
5
|
+
* every variant of the event union and calls `assertNever` in the default arm,
|
|
6
|
+
* so a newly added event variant fails to compile until it is handled here.
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import type { AgentSessionEvent } from "../../../session/agent-session";
|
|
10
|
+
import {
|
|
11
|
+
type AgentSessionEventType,
|
|
12
|
+
BRIDGE_PROTOCOL_VERSION,
|
|
13
|
+
type BridgeEventFrame,
|
|
14
|
+
type BridgeFrameEnvelope,
|
|
15
|
+
type BridgeFrameType,
|
|
16
|
+
} from "./protocol";
|
|
17
|
+
|
|
18
|
+
function assertNever(value: never): never {
|
|
19
|
+
throw new Error(`Unhandled AgentSessionEvent variant: ${JSON.stringify(value)}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the stable wire event-type for an `AgentSessionEvent`.
|
|
24
|
+
*
|
|
25
|
+
* Exhaustive over the union; adding a variant without a case is a type error.
|
|
26
|
+
*/
|
|
27
|
+
export function agentSessionEventType(event: AgentSessionEvent): AgentSessionEventType {
|
|
28
|
+
switch (event.type) {
|
|
29
|
+
case "agent_start":
|
|
30
|
+
case "agent_end":
|
|
31
|
+
case "turn_start":
|
|
32
|
+
case "turn_end":
|
|
33
|
+
case "message_start":
|
|
34
|
+
case "message_update":
|
|
35
|
+
case "message_end":
|
|
36
|
+
case "tool_execution_start":
|
|
37
|
+
case "tool_execution_update":
|
|
38
|
+
case "tool_execution_end":
|
|
39
|
+
case "auto_compaction_start":
|
|
40
|
+
case "auto_compaction_end":
|
|
41
|
+
case "auto_retry_start":
|
|
42
|
+
case "auto_retry_end":
|
|
43
|
+
case "retry_fallback_applied":
|
|
44
|
+
case "retry_fallback_succeeded":
|
|
45
|
+
case "ttsr_triggered":
|
|
46
|
+
case "todo_reminder":
|
|
47
|
+
case "todo_auto_clear":
|
|
48
|
+
case "irc_message":
|
|
49
|
+
case "notice":
|
|
50
|
+
case "thinking_level_changed":
|
|
51
|
+
case "goal_updated":
|
|
52
|
+
return event.type;
|
|
53
|
+
default:
|
|
54
|
+
return assertNever(event);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Per-session monotonic frame builder. One instance per active session; `seq`
|
|
60
|
+
* starts at 1 and increments per frame so clients can order and resume.
|
|
61
|
+
*/
|
|
62
|
+
export class BridgeFrameSequencer {
|
|
63
|
+
readonly #sessionId: string;
|
|
64
|
+
#seq = 0;
|
|
65
|
+
|
|
66
|
+
constructor(sessionId: string) {
|
|
67
|
+
this.#sessionId = sessionId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The session id stamped onto every frame this sequencer produces. */
|
|
71
|
+
get sessionId(): string {
|
|
72
|
+
return this.#sessionId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The seq assigned to the most recently produced frame (0 before any). */
|
|
76
|
+
get lastSeq(): number {
|
|
77
|
+
return this.#seq;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Build the next envelope of the given type with a fresh seq + frame id. */
|
|
81
|
+
next<TType extends BridgeFrameType, TPayload>(
|
|
82
|
+
type: TType,
|
|
83
|
+
payload: TPayload,
|
|
84
|
+
correlationId?: string,
|
|
85
|
+
): BridgeFrameEnvelope<TType, TPayload> {
|
|
86
|
+
this.#seq += 1;
|
|
87
|
+
const frame: BridgeFrameEnvelope<TType, TPayload> = {
|
|
88
|
+
protocol_version: BRIDGE_PROTOCOL_VERSION,
|
|
89
|
+
session_id: this.#sessionId,
|
|
90
|
+
seq: this.#seq,
|
|
91
|
+
frame_id: randomUUID(),
|
|
92
|
+
type,
|
|
93
|
+
payload,
|
|
94
|
+
};
|
|
95
|
+
if (correlationId !== undefined) {
|
|
96
|
+
frame.correlation_id = correlationId;
|
|
97
|
+
}
|
|
98
|
+
return frame;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Serialize a single `AgentSessionEvent` into an `event` wire frame. */
|
|
103
|
+
export function toBridgeEventFrame(event: AgentSessionEvent, sequencer: BridgeFrameSequencer): BridgeEventFrame {
|
|
104
|
+
return sequencer.next("event", {
|
|
105
|
+
event_type: agentSessionEventType(event),
|
|
106
|
+
event,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { BRIDGE_PROTOCOL_VERSION, type BridgeFrameType } from "./protocol";
|
|
2
|
+
import type { BridgeCommandScope } from "./scopes";
|
|
3
|
+
|
|
4
|
+
export type BridgeCapability =
|
|
5
|
+
| "events"
|
|
6
|
+
| "prompt"
|
|
7
|
+
| "permission"
|
|
8
|
+
| "elicitation"
|
|
9
|
+
| "ui.declarative"
|
|
10
|
+
| "ui.editor"
|
|
11
|
+
| "ui.terminal_input"
|
|
12
|
+
| "host_tools"
|
|
13
|
+
| "host_uri"
|
|
14
|
+
| "client_bridge.read_text_file"
|
|
15
|
+
| "client_bridge.write_text_file"
|
|
16
|
+
| "client_bridge.create_terminal";
|
|
17
|
+
|
|
18
|
+
export interface BridgeProtocolRange {
|
|
19
|
+
min: number;
|
|
20
|
+
max: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BridgeHandshakeRequest {
|
|
24
|
+
protocol_version_range: BridgeProtocolRange;
|
|
25
|
+
capabilities: BridgeCapability[];
|
|
26
|
+
requested_scopes: BridgeCommandScope[];
|
|
27
|
+
last_seq?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BridgeEndpointDescriptor {
|
|
31
|
+
events: string;
|
|
32
|
+
commands: string;
|
|
33
|
+
uiResponses: string;
|
|
34
|
+
claimControl: string;
|
|
35
|
+
disconnectControl: string;
|
|
36
|
+
hostToolResults: string;
|
|
37
|
+
hostUriResults: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BridgeHandshakeAccepted {
|
|
41
|
+
status: "accepted";
|
|
42
|
+
protocol_version: typeof BRIDGE_PROTOCOL_VERSION;
|
|
43
|
+
session_id: string;
|
|
44
|
+
accepted_capabilities: BridgeCapability[];
|
|
45
|
+
accepted_scopes: BridgeCommandScope[];
|
|
46
|
+
unsupported: BridgeCapability[];
|
|
47
|
+
endpoints: BridgeEndpointDescriptor;
|
|
48
|
+
frame_types: BridgeFrameType[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BridgeHandshakeRejected {
|
|
52
|
+
status: "rejected";
|
|
53
|
+
reason: "incompatible_version" | "unauthorized" | "invalid_request";
|
|
54
|
+
message: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type BridgeHandshakeResponse = BridgeHandshakeAccepted | BridgeHandshakeRejected;
|
|
58
|
+
export function isBridgeHandshakeRequest(value: unknown): value is BridgeHandshakeRequest {
|
|
59
|
+
if (!value || typeof value !== "object") return false;
|
|
60
|
+
const request = value as {
|
|
61
|
+
protocol_version_range?: unknown;
|
|
62
|
+
capabilities?: unknown;
|
|
63
|
+
requested_scopes?: unknown;
|
|
64
|
+
last_seq?: unknown;
|
|
65
|
+
};
|
|
66
|
+
const range = request.protocol_version_range as { min?: unknown; max?: unknown } | undefined;
|
|
67
|
+
return (
|
|
68
|
+
!!range &&
|
|
69
|
+
typeof range.min === "number" &&
|
|
70
|
+
Number.isInteger(range.min) &&
|
|
71
|
+
typeof range.max === "number" &&
|
|
72
|
+
Number.isInteger(range.max) &&
|
|
73
|
+
Array.isArray(request.capabilities) &&
|
|
74
|
+
request.capabilities.every(capability => typeof capability === "string") &&
|
|
75
|
+
Array.isArray(request.requested_scopes) &&
|
|
76
|
+
request.requested_scopes.every(scope => typeof scope === "string") &&
|
|
77
|
+
(request.last_seq === undefined || (typeof request.last_seq === "number" && Number.isInteger(request.last_seq)))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function negotiateBridgeHandshake(
|
|
82
|
+
request: BridgeHandshakeRequest,
|
|
83
|
+
server: {
|
|
84
|
+
sessionId: string;
|
|
85
|
+
capabilities: readonly BridgeCapability[];
|
|
86
|
+
scopes: readonly BridgeCommandScope[];
|
|
87
|
+
endpoints: BridgeEndpointDescriptor;
|
|
88
|
+
frameTypes: readonly BridgeFrameType[];
|
|
89
|
+
},
|
|
90
|
+
): BridgeHandshakeResponse {
|
|
91
|
+
if (
|
|
92
|
+
request.protocol_version_range.min > BRIDGE_PROTOCOL_VERSION ||
|
|
93
|
+
request.protocol_version_range.max < BRIDGE_PROTOCOL_VERSION
|
|
94
|
+
) {
|
|
95
|
+
return {
|
|
96
|
+
status: "rejected",
|
|
97
|
+
reason: "incompatible_version",
|
|
98
|
+
message: `Bridge protocol v${BRIDGE_PROTOCOL_VERSION} is outside client range ${request.protocol_version_range.min}..${request.protocol_version_range.max}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const serverCapabilities = new Set(server.capabilities);
|
|
102
|
+
const acceptedCapabilities = request.capabilities.filter(capability => serverCapabilities.has(capability));
|
|
103
|
+
const acceptedSet = new Set(acceptedCapabilities);
|
|
104
|
+
const unsupported = request.capabilities.filter(capability => !acceptedSet.has(capability));
|
|
105
|
+
const serverScopes = new Set(server.scopes);
|
|
106
|
+
const acceptedScopes = request.requested_scopes.filter(scope => serverScopes.has(scope));
|
|
107
|
+
return {
|
|
108
|
+
status: "accepted",
|
|
109
|
+
protocol_version: BRIDGE_PROTOCOL_VERSION,
|
|
110
|
+
session_id: server.sessionId,
|
|
111
|
+
accepted_capabilities: acceptedCapabilities,
|
|
112
|
+
accepted_scopes: acceptedScopes,
|
|
113
|
+
unsupported,
|
|
114
|
+
endpoints: server.endpoints,
|
|
115
|
+
frame_types: [...server.frameTypes],
|
|
116
|
+
};
|
|
117
|
+
}
|