@code-yeongyu/senpi-codemode 2026.7.25-2

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 (63) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +22 -0
  3. package/README.md +161 -0
  4. package/package.json +58 -0
  5. package/src/bridge/http-server.ts +236 -0
  6. package/src/bridge/protocol.ts +198 -0
  7. package/src/bridge/reserved.ts +9 -0
  8. package/src/bridges/agent-bridge.ts +197 -0
  9. package/src/bridges/output-bridge.ts +96 -0
  10. package/src/bridges/schema-injection.ts +3 -0
  11. package/src/codemode/runtime.ts +258 -0
  12. package/src/codemode/tools.ts +106 -0
  13. package/src/completion/handler.ts +192 -0
  14. package/src/completion/tool-bridge.ts +55 -0
  15. package/src/config/settings.ts +215 -0
  16. package/src/extension/runtime-factory.ts +114 -0
  17. package/src/extension/session-manager-proxy.ts +116 -0
  18. package/src/extension/session-manager.ts +215 -0
  19. package/src/host-sdk.ts +1 -0
  20. package/src/index.ts +181 -0
  21. package/src/interpreters/detect.ts +161 -0
  22. package/src/kernels/jl/kernel.ts +37 -0
  23. package/src/kernels/jl/prelude.jl +283 -0
  24. package/src/kernels/jl/runner.jl +327 -0
  25. package/src/kernels/js/context-manager.ts +296 -0
  26. package/src/kernels/js/inline-worker-entry.js +23 -0
  27. package/src/kernels/js/inline-worker.ts +15 -0
  28. package/src/kernels/js/kernel-contract.ts +38 -0
  29. package/src/kernels/js/local-module-loader.ts +108 -0
  30. package/src/kernels/js/prelude.ts +15 -0
  31. package/src/kernels/js/rewrite-imports.ts +164 -0
  32. package/src/kernels/js/run-queue.ts +82 -0
  33. package/src/kernels/js/worker-core.d.ts +18 -0
  34. package/src/kernels/js/worker-core.js +94 -0
  35. package/src/kernels/js/worker-entry.js +23 -0
  36. package/src/kernels/js/worker-host.ts +117 -0
  37. package/src/kernels/js/worker-indirect-eval.js +88 -0
  38. package/src/kernels/js/worker-runtime.js +401 -0
  39. package/src/kernels/py/kernel-contract.ts +32 -0
  40. package/src/kernels/py/kernel.ts +290 -0
  41. package/src/kernels/py/prelude.py +954 -0
  42. package/src/kernels/py/process.ts +119 -0
  43. package/src/kernels/py/transport.ts +237 -0
  44. package/src/kernels/rb/kernel.ts +26 -0
  45. package/src/kernels/rb/prelude.rb +270 -0
  46. package/src/kernels/rb/runner.rb +204 -0
  47. package/src/kernels/shared/subprocess-contract.ts +22 -0
  48. package/src/kernels/shared/subprocess-kernel.ts +266 -0
  49. package/src/kernels/shared/subprocess-process.ts +174 -0
  50. package/src/kernels/shared/subprocess-queue.ts +101 -0
  51. package/src/kernels/shared/subprocess-run.ts +98 -0
  52. package/src/output/output-meta.ts +89 -0
  53. package/src/output/streaming-output.ts +296 -0
  54. package/src/prompt/eval-prompt.ts +319 -0
  55. package/src/timeouts/bridge-timeout.ts +16 -0
  56. package/src/timeouts/idle-timeout.ts +84 -0
  57. package/src/tool/cell-handler.ts +279 -0
  58. package/src/tool/eval-tool.ts +285 -0
  59. package/src/tool/image.ts +274 -0
  60. package/src/tool/json-tree.ts +247 -0
  61. package/src/tool/render.ts +876 -0
  62. package/src/tool/status-events.ts +12 -0
  63. package/src/tool/types.ts +114 -0
@@ -0,0 +1,96 @@
1
+ import type { AgentToolResult } from "@code-yeongyu/senpi";
2
+ import { type Static, Type } from "typebox";
3
+ import { Check, Errors } from "typebox/value";
4
+ import type { ExecuteTool } from "../tool/types.ts";
5
+
6
+ const outputArgsSchema = Type.Object(
7
+ {
8
+ ids: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
9
+ format: Type.Optional(Type.Union([Type.Literal("raw"), Type.Literal("tail")])),
10
+ offset: Type.Optional(Type.Integer({ minimum: 1 })),
11
+ limit: Type.Optional(Type.Integer({ minimum: 1 })),
12
+ },
13
+ { additionalProperties: false },
14
+ );
15
+
16
+ type OutputArgs = Static<typeof outputArgsSchema>;
17
+
18
+ export type OutputExecuteTool = ExecuteTool & {
19
+ readonly isToolAvailable?: (name: string) => boolean;
20
+ };
21
+
22
+ export type MarshalledToolResult = {
23
+ readonly text: string;
24
+ };
25
+
26
+ export interface RunEvalOutputOptions {
27
+ readonly taskOutputToolName: string;
28
+ readonly executeTool: OutputExecuteTool;
29
+ readonly signal?: AbortSignal;
30
+ readonly marshalToolResult: (result: AgentToolResult<unknown>) => MarshalledToolResult;
31
+ }
32
+
33
+ class OutputArgumentsError extends Error {
34
+ readonly name = "OutputArgumentsError";
35
+
36
+ constructor(summary: string) {
37
+ super(`output() received invalid arguments: ${summary}`);
38
+ }
39
+ }
40
+
41
+ class OutputUnavailableError extends Error {
42
+ readonly name = "OutputUnavailableError";
43
+
44
+ constructor(toolName: string) {
45
+ super(`output() unavailable: no "${toolName}" tool is registered in this session`);
46
+ }
47
+ }
48
+
49
+ export async function runEvalOutput(args: unknown, options: RunEvalOutputOptions): Promise<string | readonly string[]> {
50
+ const parsed = parseOutputArgs(args);
51
+ if (options.executeTool.isToolAvailable?.(options.taskOutputToolName) === false) {
52
+ throw new OutputUnavailableError(options.taskOutputToolName);
53
+ }
54
+
55
+ const mode = parsed.format === "tail" ? "tail" : "full";
56
+ const transcripts = await Promise.all(
57
+ parsed.ids.map(async (id) => {
58
+ let result: AgentToolResult<unknown>;
59
+ try {
60
+ result = await options.executeTool(
61
+ options.taskOutputToolName,
62
+ {
63
+ ...(id.startsWith("st_") ? { task_id: id } : { name: id }),
64
+ mode,
65
+ block: true,
66
+ },
67
+ options.signal === undefined ? undefined : { signal: options.signal },
68
+ );
69
+ } catch (error) {
70
+ if (isUnavailableToolError(error)) throw new OutputUnavailableError(options.taskOutputToolName);
71
+ throw error;
72
+ }
73
+
74
+ // ADAPTATION: task_output owns transcripts, so no AgentOutputManager cache or
75
+ // oh-my-pi query/json/stripped formats exist on this bridge.
76
+ const transcript = options.marshalToolResult(result).text;
77
+ const lines = transcript.split(/\r?\n/u);
78
+ const start = (parsed.offset ?? 1) - 1;
79
+ return lines.slice(start, parsed.limit === undefined ? undefined : start + parsed.limit).join("\n");
80
+ }),
81
+ );
82
+ return transcripts.length === 1 ? transcripts[0] : transcripts;
83
+ }
84
+
85
+ function parseOutputArgs(value: unknown): OutputArgs {
86
+ if (Check(outputArgsSchema, value)) return value;
87
+ const summary = Errors(outputArgsSchema, value)
88
+ .map((error) => `${error.instancePath || "/"} ${error.message}`)
89
+ .join("; ");
90
+ throw new OutputArgumentsError(summary || "invalid value");
91
+ }
92
+
93
+ function isUnavailableToolError(error: unknown): boolean {
94
+ if (typeof error !== "object" || error === null || Array.isArray(error)) return false;
95
+ return "code" in error && (error.code === "unknown_tool" || error.code === "inactive_tool");
96
+ }
@@ -0,0 +1,3 @@
1
+ export function injectSchemaInstruction(prompt: string, schema: unknown): string {
2
+ return `${prompt}\n\nRespond ONLY with JSON matching this JSON-Schema:\n${JSON.stringify(schema)}`;
3
+ }
@@ -0,0 +1,258 @@
1
+ import type { KernelToHostMessage } from "../bridge/protocol.ts";
2
+ import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
3
+ import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
4
+ import { marshalToolResult } from "../tool/image.ts";
5
+
6
+ const MAX_ACTIVE_CELLS = 4;
7
+ const MAX_PENDING_OUTPUT_CHARS = 100_000;
8
+ const MAX_TERMINAL_ERROR_CHARS = 4_096;
9
+ const RECURSIVE_TOOLS = new Set(["eval", "exec", "wait"]);
10
+
11
+ export type CodeModeCellState = "yielded" | "result" | "terminated" | "error" | "missing";
12
+
13
+ export interface CodeModeObservation {
14
+ readonly cellId: string;
15
+ readonly state: CodeModeCellState;
16
+ readonly output: string;
17
+ readonly error?: string;
18
+ }
19
+
20
+ export interface CodeModeRuntimeOptions {
21
+ readonly sessionId: string;
22
+ readonly cwd: string;
23
+ readonly parallelPoolWidth: number;
24
+ readonly executeTool: AgentExecuteTool;
25
+ }
26
+
27
+ export class CodeModeCapacityError extends Error {
28
+ readonly name = "CodeModeCapacityError";
29
+
30
+ constructor() {
31
+ super(`Code Mode supports at most ${MAX_ACTIVE_CELLS} active cells`);
32
+ }
33
+ }
34
+
35
+ export class CodeModeSessionRuntime {
36
+ readonly #options: CodeModeRuntimeOptions;
37
+ readonly #cells = new Map<string, CodeModeCell>();
38
+ #nextCell = 0;
39
+ #disposed = false;
40
+
41
+ constructor(options: CodeModeRuntimeOptions) {
42
+ this.#options = options;
43
+ }
44
+
45
+ async execute(code: string, yieldTimeMs: number, signal: AbortSignal | undefined): Promise<CodeModeObservation> {
46
+ this.#assertActive();
47
+ const cellId = `exec-${++this.#nextCell}`;
48
+ if (signal?.aborted) return { cellId, state: "terminated", output: "" };
49
+ if (this.#cells.size >= MAX_ACTIVE_CELLS) throw new CodeModeCapacityError();
50
+ const cell = new CodeModeCell(cellId, this.#options);
51
+ this.#cells.set(cellId, cell);
52
+ cell.start(code);
53
+ const observation = await cell.observe(yieldTimeMs, signal);
54
+ this.#releaseIfTerminal(observation);
55
+ return observation;
56
+ }
57
+
58
+ async wait(
59
+ cellId: string,
60
+ yieldTimeMs: number,
61
+ terminate: boolean,
62
+ signal: AbortSignal | undefined,
63
+ ): Promise<CodeModeObservation> {
64
+ const cell = this.#cells.get(cellId);
65
+ if (cell === undefined) return { cellId, state: "missing", output: "" };
66
+ const observation = terminate ? await cell.terminate() : await cell.observe(yieldTimeMs, signal);
67
+ this.#releaseIfTerminal(observation);
68
+ return observation;
69
+ }
70
+
71
+ async dispose(): Promise<void> {
72
+ if (this.#disposed) return;
73
+ this.#disposed = true;
74
+ const cells = [...this.#cells.values()];
75
+ this.#cells.clear();
76
+ await Promise.all(cells.map((cell) => cell.terminate()));
77
+ }
78
+
79
+ #releaseIfTerminal(observation: CodeModeObservation): void {
80
+ if (observation.state !== "yielded") this.#cells.delete(observation.cellId);
81
+ }
82
+
83
+ #assertActive(): void {
84
+ if (this.#disposed) throw new Error("Code Mode session has been disposed");
85
+ }
86
+ }
87
+
88
+ class CodeModeCell {
89
+ readonly #id: string;
90
+ readonly #options: CodeModeRuntimeOptions;
91
+ readonly #kernel: JavaScriptKernel;
92
+ readonly #abort = new AbortController();
93
+ #output = "";
94
+ #outputTruncated = false;
95
+ #completion: Promise<void> | undefined;
96
+ #state: "running" | "result" | "terminated" | "error" = "running";
97
+ #error: string | undefined;
98
+
99
+ constructor(id: string, options: CodeModeRuntimeOptions) {
100
+ this.#id = id;
101
+ this.#options = options;
102
+ this.#kernel = new JavaScriptKernel({
103
+ sessionId: `${options.sessionId}:${id}`,
104
+ cwd: options.cwd,
105
+ parallelPoolWidth: options.parallelPoolWidth,
106
+ onMessage: (message) => this.#handleMessage(message),
107
+ });
108
+ }
109
+
110
+ start(code: string): void {
111
+ this.#completion = this.#kernel
112
+ .run({ cellId: this.#id, code })
113
+ .then((result) => {
114
+ if (this.#state !== "running") return;
115
+ if (result.ok) {
116
+ this.#state = "result";
117
+ if (result.valueRepr) this.#appendOutput(`${result.valueRepr}\n`);
118
+ return;
119
+ }
120
+ this.#state = "error";
121
+ this.#error = truncateTerminalError(result.error.message);
122
+ })
123
+ .catch((error: unknown) => {
124
+ if (this.#state !== "running") return;
125
+ this.#state = "error";
126
+ this.#error = truncateTerminalError(error instanceof Error ? error.message : String(error));
127
+ })
128
+ .finally(async () => {
129
+ if (this.#state !== "running") await this.#kernel.close();
130
+ });
131
+ }
132
+
133
+ async observe(yieldTimeMs: number, signal: AbortSignal | undefined): Promise<CodeModeObservation> {
134
+ if (signal?.aborted) return await this.terminate();
135
+ if (this.#state !== "running") return this.#observation();
136
+ const completion = this.#completion;
137
+ if (completion === undefined) throw new Error("Code Mode cell has not started");
138
+ const timeout = Math.max(1, Math.trunc(yieldTimeMs));
139
+ let timer: ReturnType<typeof setTimeout> | undefined;
140
+ let abortListener: (() => void) | undefined;
141
+ const timedOut = new Promise<"yielded">((resolve) => {
142
+ timer = setTimeout(() => resolve("yielded"), timeout);
143
+ });
144
+ const completed = completion.then(() => "completed" as const);
145
+ const aborted = new Promise<"aborted">((resolve) => {
146
+ if (!signal) return;
147
+ abortListener = () => resolve("aborted");
148
+ signal.addEventListener("abort", abortListener, { once: true });
149
+ });
150
+ try {
151
+ const outcome = await Promise.race([completed, timedOut, aborted]);
152
+ if (outcome === "aborted") return await this.terminate();
153
+ return this.#observation();
154
+ } finally {
155
+ if (timer !== undefined) clearTimeout(timer);
156
+ if (abortListener && signal) signal.removeEventListener("abort", abortListener);
157
+ }
158
+ }
159
+
160
+ async terminate(): Promise<CodeModeObservation> {
161
+ if (this.#state === "terminated") return this.#observation();
162
+ if (this.#state === "result" || this.#state === "error") return this.#observation();
163
+ this.#state = "terminated";
164
+ this.#abort.abort(new Error("Code Mode cell terminated"));
165
+ try {
166
+ await this.#kernel.interrupt("Code Mode cell terminated");
167
+ } finally {
168
+ await this.#kernel.close();
169
+ }
170
+ return this.#observation();
171
+ }
172
+
173
+ #handleMessage(message: KernelToHostMessage): void {
174
+ if (message.type === "text") {
175
+ this.#appendOutput(message.data);
176
+ return;
177
+ }
178
+ if (message.type === "display") {
179
+ this.#appendOutput(`[display ${message.mimeType}]\n`);
180
+ return;
181
+ }
182
+ if (message.type === "tool-call") void this.#invokeTool(message);
183
+ }
184
+
185
+ async #invokeTool(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
186
+ if (RECURSIVE_TOOLS.has(message.toolName)) {
187
+ this.#kernel.deliverToolReply({
188
+ type: "tool-reply",
189
+ callId: message.callId,
190
+ ok: false,
191
+ error: { message: `recursive Code Mode tool "${message.toolName}" is not allowed` },
192
+ });
193
+ return;
194
+ }
195
+ if (this.#options.executeTool.isToolAvailable?.(message.toolName) === false) {
196
+ this.#kernel.deliverToolReply({
197
+ type: "tool-reply",
198
+ callId: message.callId,
199
+ ok: false,
200
+ error: { message: `nested tool "${message.toolName}" is not active` },
201
+ });
202
+ return;
203
+ }
204
+ try {
205
+ const result = await this.#options.executeTool(message.toolName, message.args, { signal: this.#abort.signal });
206
+ this.#kernel.deliverToolReply({
207
+ type: "tool-reply",
208
+ callId: message.callId,
209
+ ok: true,
210
+ value: marshalToolResult(result),
211
+ });
212
+ } catch (error) {
213
+ this.#kernel.deliverToolReply({
214
+ type: "tool-reply",
215
+ callId: message.callId,
216
+ ok: false,
217
+ error: { message: error instanceof Error ? error.message : String(error) },
218
+ });
219
+ }
220
+ }
221
+
222
+ #observation(): CodeModeObservation {
223
+ let output = this.#output;
224
+ this.#output = "";
225
+ if (this.#outputTruncated) {
226
+ output += "\n[output truncated]\n";
227
+ this.#outputTruncated = false;
228
+ }
229
+ if (this.#state === "running") return { cellId: this.#id, state: "yielded", output };
230
+ if (this.#state === "terminated") return { cellId: this.#id, state: "terminated", output };
231
+ if (this.#state === "error") {
232
+ const error = this.#error ?? "Code Mode cell failed";
233
+ if (output !== "") output += output.endsWith("\n") ? error : `\n${error}`;
234
+ else output = error;
235
+ return { cellId: this.#id, state: "error", output, error };
236
+ }
237
+ return { cellId: this.#id, state: "result", output };
238
+ }
239
+
240
+ #appendOutput(value: string): void {
241
+ const remaining = MAX_PENDING_OUTPUT_CHARS - this.#output.length;
242
+ if (remaining <= 0) {
243
+ this.#outputTruncated = true;
244
+ return;
245
+ }
246
+ if (value.length > remaining) {
247
+ this.#output += value.slice(0, remaining);
248
+ this.#outputTruncated = true;
249
+ return;
250
+ }
251
+ this.#output += value;
252
+ }
253
+ }
254
+
255
+ function truncateTerminalError(message: string): string {
256
+ if (message.length <= MAX_TERMINAL_ERROR_CHARS) return message;
257
+ return `${message.slice(0, MAX_TERMINAL_ERROR_CHARS)}\n[error truncated]`;
258
+ }
@@ -0,0 +1,106 @@
1
+ import type { AgentToolResult, ToolDefinition } from "@code-yeongyu/senpi";
2
+ import { type Static, Type } from "typebox";
3
+ import type { CodeModeCellState, CodeModeObservation, CodeModeSessionRuntime } from "./runtime.ts";
4
+
5
+ const DEFAULT_YIELD_TIME_MS = 10_000;
6
+
7
+ const execInputSchema = Type.Object({
8
+ code: Type.String({ minLength: 1, description: "JavaScript program to execute in a dedicated Code Mode cell." }),
9
+ yield_time_ms: Type.Optional(
10
+ Type.Integer({ minimum: 1, maximum: 60_000, description: "Return a yielded cell after this many milliseconds." }),
11
+ ),
12
+ });
13
+
14
+ const waitInputSchema = Type.Object({
15
+ cell_id: Type.String({ minLength: 1, description: "Code Mode cell id returned by exec." }),
16
+ yield_time_ms: Type.Optional(
17
+ Type.Integer({ minimum: 1, maximum: 60_000, description: "Return again after this many milliseconds." }),
18
+ ),
19
+ terminate: Type.Optional(Type.Boolean({ description: "Interrupt and close the cell instead of waiting." })),
20
+ });
21
+
22
+ type ExecInput = Static<typeof execInputSchema>;
23
+ type WaitInput = Static<typeof waitInputSchema>;
24
+
25
+ export interface CodeModeToolDetails {
26
+ readonly cellId: string;
27
+ readonly state: CodeModeCellState;
28
+ readonly isError?: boolean;
29
+ }
30
+
31
+ export type CodeModeTool =
32
+ | ToolDefinition<typeof execInputSchema, CodeModeToolDetails>
33
+ | ToolDefinition<typeof waitInputSchema, CodeModeToolDetails>;
34
+
35
+ export interface CreateCodeModeToolsOptions {
36
+ readonly runtime: CodeModeSessionRuntime;
37
+ }
38
+
39
+ export function createCodeModeTools(options: CreateCodeModeToolsOptions): {
40
+ readonly exec: ToolDefinition<typeof execInputSchema, CodeModeToolDetails>;
41
+ readonly wait: ToolDefinition<typeof waitInputSchema, CodeModeToolDetails>;
42
+ } {
43
+ return {
44
+ exec: {
45
+ name: "exec",
46
+ label: "GPT Code Mode Exec",
47
+ description:
48
+ "Run JavaScript in a dedicated GPT Code Mode cell. Call active Senpi tools as `tools.<name>(args)`. " +
49
+ "If the cell yields, call wait with its cell_id.",
50
+ promptSnippet: "Execute JavaScript that composes active tools in a dedicated Code Mode cell.",
51
+ promptGuidelines: [
52
+ "Use exec for bounded JavaScript composition of active tools; use eval for persistent multi-language analysis.",
53
+ "Call wait only when exec reports a yielded cell, and terminate abandoned cells with wait({ cell_id, terminate: true }).",
54
+ ],
55
+ parameters: execInputSchema,
56
+ executionMode: "sequential",
57
+ async execute(_toolCallId, params: ExecInput, signal) {
58
+ return resultFrom(
59
+ await options.runtime.execute(params.code, params.yield_time_ms ?? DEFAULT_YIELD_TIME_MS, signal),
60
+ );
61
+ },
62
+ },
63
+ wait: {
64
+ name: "wait",
65
+ label: "GPT Code Mode Wait",
66
+ description: "Observe a yielded GPT Code Mode cell or terminate it.",
67
+ promptSnippet: "Wait for, or terminate, a yielded GPT Code Mode cell.",
68
+ parameters: waitInputSchema,
69
+ executionMode: "sequential",
70
+ async execute(_toolCallId, params: WaitInput, signal) {
71
+ return resultFrom(
72
+ await options.runtime.wait(
73
+ params.cell_id,
74
+ params.yield_time_ms ?? DEFAULT_YIELD_TIME_MS,
75
+ params.terminate ?? false,
76
+ signal,
77
+ ),
78
+ );
79
+ },
80
+ },
81
+ };
82
+ }
83
+
84
+ function resultFrom(observation: CodeModeObservation): AgentToolResult<CodeModeToolDetails> {
85
+ const text =
86
+ observation.output ||
87
+ (observation.state === "yielded"
88
+ ? `Code Mode cell ${observation.cellId} is still running. Call wait with this cell_id.`
89
+ : observation.state === "missing"
90
+ ? `Code Mode cell ${observation.cellId} does not exist.`
91
+ : observation.state === "terminated"
92
+ ? `Code Mode cell ${observation.cellId} was terminated.`
93
+ : (observation.error ?? `Code Mode cell ${observation.cellId} completed.`));
94
+ return {
95
+ content: [{ type: "text", text }],
96
+ details: {
97
+ cellId: observation.cellId,
98
+ state: observation.state,
99
+ ...(observation.state === "error" || observation.state === "missing" ? { isError: true } : {}),
100
+ },
101
+ };
102
+ }
103
+
104
+ export function isGptCodeModeModel(modelId: string | undefined): boolean {
105
+ return modelId !== undefined && /(^|[/.:])gpt[-.]/iu.test(modelId);
106
+ }
@@ -0,0 +1,192 @@
1
+ import type { ExtensionContext } from "@code-yeongyu/senpi";
2
+ import type { Api, AssistantMessage, Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
3
+ import { completeSimple } from "@earendil-works/pi-ai/compat";
4
+ import { injectSchemaInstruction } from "../bridges/schema-injection.ts";
5
+
6
+ export interface CompletionRequest {
7
+ readonly prompt: string;
8
+ readonly model?: string;
9
+ readonly system?: string;
10
+ readonly schema?: unknown;
11
+ readonly opts?: unknown;
12
+ }
13
+
14
+ export type CompletionResult =
15
+ | { readonly text: string; readonly details: CompletionDetails }
16
+ | { readonly value: unknown; readonly details: CompletionDetails };
17
+
18
+ export type CompleteSimple = (
19
+ model: Model<Api>,
20
+ context: Context,
21
+ options?: SimpleStreamOptions,
22
+ ) => Promise<AssistantMessage>;
23
+
24
+ type CompletionDetails = {
25
+ readonly model: string;
26
+ readonly structured: boolean;
27
+ };
28
+
29
+ type CompletionTier = "smol" | "default" | "slow";
30
+
31
+ class CompletionUnknownTierError extends Error {
32
+ readonly name = "CompletionUnknownTierError";
33
+
34
+ constructor(tier: string) {
35
+ super(`completion() could not resolve the "${tier}" model tier; expected "smol", "default", or "slow".`);
36
+ }
37
+ }
38
+
39
+ class CompletionTierUnavailableError extends Error {
40
+ readonly name = "CompletionTierUnavailableError";
41
+
42
+ constructor(tier: Exclude<CompletionTier, "default">) {
43
+ super(`completion() could not resolve the "${tier}" model tier: no configured models are available.`);
44
+ }
45
+ }
46
+
47
+ export function createCompletionHandler(
48
+ complete: CompleteSimple = completeSimple,
49
+ ): (ctx: ExtensionContext) => (request: CompletionRequest) => Promise<CompletionResult> {
50
+ return (ctx) => async (request) => runCompletion(ctx, normalizeRequest(request), complete);
51
+ }
52
+
53
+ async function runCompletion(
54
+ ctx: ExtensionContext,
55
+ request: CompletionRequest,
56
+ complete: CompleteSimple,
57
+ ): Promise<CompletionResult> {
58
+ const tier = resolveCompletionTier(request.model);
59
+ const model = resolveRequestedModel(ctx, tier);
60
+ if (!model) throw unavailableModelError(tier);
61
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
62
+ if (!auth.ok) throw noModelCredentialsError(auth.error);
63
+ if (!auth.apiKey) throw noModelCredentialsError();
64
+ const requestModel = auth.upstreamModelId ? { ...model, id: auth.upstreamModelId } : model;
65
+ const structured = request.schema !== undefined;
66
+ const prompt = structured ? injectSchemaInstruction(request.prompt, request.schema) : request.prompt;
67
+ const message = await complete(
68
+ requestModel,
69
+ {
70
+ systemPrompt: request.system ?? "You are a helpful assistant.",
71
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }],
72
+ },
73
+ {
74
+ apiKey: auth.apiKey,
75
+ headers: auth.headers,
76
+ env: auth.env,
77
+ extraBody: auth.extraBody,
78
+ signal: ctx.signal,
79
+ },
80
+ );
81
+ return formatCompletion(message, model, structured);
82
+ }
83
+
84
+ function normalizeRequest(request: CompletionRequest): CompletionRequest {
85
+ if (request.opts === undefined || !isRecord(request.opts)) return request;
86
+ return {
87
+ ...request,
88
+ model: typeof request.opts.model === "string" ? request.opts.model : request.model,
89
+ system: typeof request.opts.system === "string" ? request.opts.system : request.system,
90
+ schema: "schema" in request.opts ? request.opts.schema : request.schema,
91
+ };
92
+ }
93
+
94
+ function resolveCompletionTier(requested: string | undefined): CompletionTier {
95
+ switch (requested) {
96
+ case undefined:
97
+ case "default":
98
+ return "default";
99
+ case "smol":
100
+ return "smol";
101
+ case "slow":
102
+ return "slow";
103
+ default:
104
+ throw new CompletionUnknownTierError(requested);
105
+ }
106
+ }
107
+
108
+ function resolveRequestedModel(ctx: ExtensionContext, tier: CompletionTier): Model<Api> | undefined {
109
+ switch (tier) {
110
+ case "default":
111
+ return ctx.model;
112
+ case "smol":
113
+ return lowestCostModel(ctx.modelRegistry.getAvailable());
114
+ case "slow":
115
+ return highestCostModel(ctx.modelRegistry.getAvailable());
116
+ default:
117
+ return assertNever(tier);
118
+ }
119
+ }
120
+
121
+ function unavailableModelError(tier: CompletionTier): Error {
122
+ switch (tier) {
123
+ case "default":
124
+ return noModelCredentialsError();
125
+ case "smol":
126
+ case "slow":
127
+ return new CompletionTierUnavailableError(tier);
128
+ default:
129
+ return assertNever(tier);
130
+ }
131
+ }
132
+
133
+ function lowestCostModel(models: readonly Model<Api>[]): Model<Api> | undefined {
134
+ let selected: Model<Api> | undefined;
135
+ for (const model of models) {
136
+ if (selected === undefined || promptAndResponseCost(model) < promptAndResponseCost(selected)) selected = model;
137
+ }
138
+ return selected;
139
+ }
140
+
141
+ function highestCostModel(models: readonly Model<Api>[]): Model<Api> | undefined {
142
+ let selected: Model<Api> | undefined;
143
+ for (const model of models) {
144
+ if (selected === undefined || promptAndResponseCost(model) > promptAndResponseCost(selected)) selected = model;
145
+ }
146
+ return selected;
147
+ }
148
+
149
+ function promptAndResponseCost(model: Model<Api>): number {
150
+ return model.cost.input + model.cost.output;
151
+ }
152
+
153
+ function formatCompletion(message: AssistantMessage, model: Model<Api>, structured: boolean): CompletionResult {
154
+ if (message.stopReason === "error") throw new Error(message.errorMessage ?? "completion() request failed.");
155
+ if (message.stopReason === "aborted") throw new Error("completion() request aborted.");
156
+ const text = extractText(message);
157
+ const details = { model: formatModel(model), structured };
158
+ if (!structured) return { text, details };
159
+ try {
160
+ const value: unknown = JSON.parse(text);
161
+ return { value, details };
162
+ } catch (error) {
163
+ if (error instanceof SyntaxError) return { value: { parseError: error.message }, details };
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ function extractText(message: AssistantMessage): string {
169
+ const parts: string[] = [];
170
+ for (const part of message.content) {
171
+ if (part.type === "text") parts.push(part.text);
172
+ }
173
+ const text = parts.join("\n");
174
+ if (text.length === 0) throw new Error("completion() returned no text output.");
175
+ return text;
176
+ }
177
+
178
+ function noModelCredentialsError(reason?: string): Error {
179
+ return new Error(`completion() has no model/credentials${reason ? `: ${reason}` : ""}`);
180
+ }
181
+
182
+ function formatModel(model: Model<Api>): string {
183
+ return `${model.provider}/${model.id}`;
184
+ }
185
+
186
+ function isRecord(value: unknown): value is Record<string, unknown> {
187
+ return typeof value === "object" && value !== null;
188
+ }
189
+
190
+ function assertNever(value: never): never {
191
+ throw new CompletionUnknownTierError(String(value));
192
+ }