@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.
- package/CHANGELOG.md +250 -0
- package/LICENSE +22 -0
- package/README.md +161 -0
- package/package.json +58 -0
- package/src/bridge/http-server.ts +236 -0
- package/src/bridge/protocol.ts +198 -0
- package/src/bridge/reserved.ts +9 -0
- package/src/bridges/agent-bridge.ts +197 -0
- package/src/bridges/output-bridge.ts +96 -0
- package/src/bridges/schema-injection.ts +3 -0
- package/src/codemode/runtime.ts +258 -0
- package/src/codemode/tools.ts +106 -0
- package/src/completion/handler.ts +192 -0
- package/src/completion/tool-bridge.ts +55 -0
- package/src/config/settings.ts +215 -0
- package/src/extension/runtime-factory.ts +114 -0
- package/src/extension/session-manager-proxy.ts +116 -0
- package/src/extension/session-manager.ts +215 -0
- package/src/host-sdk.ts +1 -0
- package/src/index.ts +181 -0
- package/src/interpreters/detect.ts +161 -0
- package/src/kernels/jl/kernel.ts +37 -0
- package/src/kernels/jl/prelude.jl +283 -0
- package/src/kernels/jl/runner.jl +327 -0
- package/src/kernels/js/context-manager.ts +296 -0
- package/src/kernels/js/inline-worker-entry.js +23 -0
- package/src/kernels/js/inline-worker.ts +15 -0
- package/src/kernels/js/kernel-contract.ts +38 -0
- package/src/kernels/js/local-module-loader.ts +108 -0
- package/src/kernels/js/prelude.ts +15 -0
- package/src/kernels/js/rewrite-imports.ts +164 -0
- package/src/kernels/js/run-queue.ts +82 -0
- package/src/kernels/js/worker-core.d.ts +18 -0
- package/src/kernels/js/worker-core.js +94 -0
- package/src/kernels/js/worker-entry.js +23 -0
- package/src/kernels/js/worker-host.ts +117 -0
- package/src/kernels/js/worker-indirect-eval.js +88 -0
- package/src/kernels/js/worker-runtime.js +401 -0
- package/src/kernels/py/kernel-contract.ts +32 -0
- package/src/kernels/py/kernel.ts +290 -0
- package/src/kernels/py/prelude.py +954 -0
- package/src/kernels/py/process.ts +119 -0
- package/src/kernels/py/transport.ts +237 -0
- package/src/kernels/rb/kernel.ts +26 -0
- package/src/kernels/rb/prelude.rb +270 -0
- package/src/kernels/rb/runner.rb +204 -0
- package/src/kernels/shared/subprocess-contract.ts +22 -0
- package/src/kernels/shared/subprocess-kernel.ts +266 -0
- package/src/kernels/shared/subprocess-process.ts +174 -0
- package/src/kernels/shared/subprocess-queue.ts +101 -0
- package/src/kernels/shared/subprocess-run.ts +98 -0
- package/src/output/output-meta.ts +89 -0
- package/src/output/streaming-output.ts +296 -0
- package/src/prompt/eval-prompt.ts +319 -0
- package/src/timeouts/bridge-timeout.ts +16 -0
- package/src/timeouts/idle-timeout.ts +84 -0
- package/src/tool/cell-handler.ts +279 -0
- package/src/tool/eval-tool.ts +285 -0
- package/src/tool/image.ts +274 -0
- package/src/tool/json-tree.ts +247 -0
- package/src/tool/render.ts +876 -0
- package/src/tool/status-events.ts +12 -0
- package/src/tool/types.ts +114 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
|
+
import type { EvalKernel } from "../tool/types.ts";
|
|
4
|
+
import type { CompletionRequest, CompletionResult } from "./handler.ts";
|
|
5
|
+
|
|
6
|
+
export interface CompletionToolCallOptions {
|
|
7
|
+
readonly message: Extract<KernelToHostMessage, { type: "tool-call" }>;
|
|
8
|
+
readonly kernel: EvalKernel;
|
|
9
|
+
readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
10
|
+
readonly ctx: ExtensionContext;
|
|
11
|
+
readonly isActive: () => boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type CompletionToolCallSummary = { readonly ok: true } | { readonly ok: false; readonly error: string };
|
|
15
|
+
|
|
16
|
+
export async function handleCompletionToolCall(options: CompletionToolCallOptions): Promise<CompletionToolCallSummary> {
|
|
17
|
+
try {
|
|
18
|
+
const result = await options.complete(toCompletionRequest(options.message.args), options.ctx);
|
|
19
|
+
if (!options.isActive()) return { ok: false, error: "completion() result ignored after eval finalization" };
|
|
20
|
+
options.kernel.deliverToolReply({
|
|
21
|
+
type: "tool-reply",
|
|
22
|
+
callId: options.message.callId,
|
|
23
|
+
ok: true,
|
|
24
|
+
value: completionReplyValue(result),
|
|
25
|
+
});
|
|
26
|
+
return { ok: true };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
if (!options.isActive()) return { ok: false, error: message };
|
|
30
|
+
options.kernel.deliverToolReply({
|
|
31
|
+
type: "tool-reply",
|
|
32
|
+
callId: options.message.callId,
|
|
33
|
+
ok: false,
|
|
34
|
+
error: { message },
|
|
35
|
+
});
|
|
36
|
+
return { ok: false, error: message };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toCompletionRequest(value: unknown): CompletionRequest {
|
|
41
|
+
if (typeof value === "object" && value !== null && "prompt" in value && typeof value.prompt === "string") {
|
|
42
|
+
return {
|
|
43
|
+
prompt: value.prompt,
|
|
44
|
+
opts: "opts" in value ? value.opts : undefined,
|
|
45
|
+
model: "model" in value && typeof value.model === "string" ? value.model : undefined,
|
|
46
|
+
system: "system" in value && typeof value.system === "string" ? value.system : undefined,
|
|
47
|
+
schema: "schema" in value ? value.schema : undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
throw new Error("completion() received invalid arguments");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function completionReplyValue(result: CompletionResult): unknown {
|
|
54
|
+
return "value" in result ? result.value : result.text;
|
|
55
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { Static } from "typebox";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { Check } from "typebox/value";
|
|
7
|
+
|
|
8
|
+
export const codemodeSettingsSchema = Type.Object(
|
|
9
|
+
{
|
|
10
|
+
languages: Type.Optional(
|
|
11
|
+
Type.Object(
|
|
12
|
+
{
|
|
13
|
+
py: Type.Optional(Type.Boolean()),
|
|
14
|
+
js: Type.Optional(Type.Boolean()),
|
|
15
|
+
rb: Type.Optional(Type.Boolean()),
|
|
16
|
+
jl: Type.Optional(Type.Boolean()),
|
|
17
|
+
},
|
|
18
|
+
{ additionalProperties: false },
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
cellTimeoutSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
22
|
+
parallelPoolWidth: Type.Optional(Type.Number({ minimum: 1 })),
|
|
23
|
+
taskTools: Type.Optional(
|
|
24
|
+
Type.Object(
|
|
25
|
+
{
|
|
26
|
+
task: Type.Optional(Type.String()),
|
|
27
|
+
output: Type.Optional(Type.String()),
|
|
28
|
+
},
|
|
29
|
+
{ additionalProperties: false },
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
outputSink: Type.Optional(
|
|
33
|
+
Type.Object(
|
|
34
|
+
{
|
|
35
|
+
headBytes: Type.Optional(Type.Number({ minimum: 0 })),
|
|
36
|
+
maxColumns: Type.Optional(Type.Number({ minimum: 0 })),
|
|
37
|
+
},
|
|
38
|
+
{ additionalProperties: false },
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
statusEvents: Type.Optional(Type.Boolean()),
|
|
42
|
+
},
|
|
43
|
+
{ additionalProperties: false },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export type CodemodeSettingsInput = Static<typeof codemodeSettingsSchema>;
|
|
47
|
+
|
|
48
|
+
export interface CodemodeTaskTools {
|
|
49
|
+
readonly task: string;
|
|
50
|
+
readonly output: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CodemodeOutputSink {
|
|
54
|
+
readonly headBytes: number;
|
|
55
|
+
readonly maxColumns: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CodemodeSettings {
|
|
59
|
+
readonly languages: {
|
|
60
|
+
readonly py: boolean;
|
|
61
|
+
readonly js: boolean;
|
|
62
|
+
readonly rb: boolean;
|
|
63
|
+
readonly jl: boolean;
|
|
64
|
+
};
|
|
65
|
+
readonly cellTimeoutSeconds: number;
|
|
66
|
+
readonly parallelPoolWidth: number;
|
|
67
|
+
readonly taskTools?: CodemodeTaskTools;
|
|
68
|
+
readonly outputSink?: CodemodeOutputSink;
|
|
69
|
+
readonly statusEvents?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ResolvedCodemodeSettings = CodemodeSettings & {
|
|
73
|
+
readonly taskTools: CodemodeTaskTools;
|
|
74
|
+
readonly outputSink: CodemodeOutputSink;
|
|
75
|
+
readonly statusEvents: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export interface LoadCodemodeSettingsOptions {
|
|
79
|
+
readonly cwd?: string;
|
|
80
|
+
readonly homeDir?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface LoadedCodemodeSettings {
|
|
84
|
+
readonly settings: ResolvedCodemodeSettings;
|
|
85
|
+
readonly source: string | null;
|
|
86
|
+
readonly warnings: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// OMP settings-schema.ts:3211-3299 has language/path settings only; eval.ts:427
|
|
90
|
+
// defaults timeout to 30s, and codemode pins concurrency-bridge.ts:30 width to 4.
|
|
91
|
+
export const defaultCodemodeSettings: ResolvedCodemodeSettings = {
|
|
92
|
+
languages: {
|
|
93
|
+
py: true,
|
|
94
|
+
js: true,
|
|
95
|
+
rb: false,
|
|
96
|
+
jl: false,
|
|
97
|
+
},
|
|
98
|
+
cellTimeoutSeconds: 30,
|
|
99
|
+
parallelPoolWidth: 4,
|
|
100
|
+
taskTools: {
|
|
101
|
+
task: "task",
|
|
102
|
+
output: "task_output",
|
|
103
|
+
},
|
|
104
|
+
outputSink: {
|
|
105
|
+
headBytes: 20_480,
|
|
106
|
+
maxColumns: 768,
|
|
107
|
+
},
|
|
108
|
+
statusEvents: true,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const languageEnvironmentFlags = {
|
|
112
|
+
py: "SENPI_CODEMODE_PY",
|
|
113
|
+
js: "SENPI_CODEMODE_JS",
|
|
114
|
+
rb: "SENPI_CODEMODE_RB",
|
|
115
|
+
jl: "SENPI_CODEMODE_JL",
|
|
116
|
+
} as const;
|
|
117
|
+
|
|
118
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
119
|
+
|
|
120
|
+
export async function loadCodemodeSettings(options: LoadCodemodeSettingsOptions = {}): Promise<LoadedCodemodeSettings> {
|
|
121
|
+
const cwd = options.cwd ?? process.cwd();
|
|
122
|
+
const homeDir = options.homeDir ?? homedir();
|
|
123
|
+
const candidates = [join(cwd, ".senpi", "codemode.json"), join(homeDir, ".senpi", "agent", "codemode.json")];
|
|
124
|
+
|
|
125
|
+
for (const candidate of candidates) {
|
|
126
|
+
if (!(await fileExists(candidate))) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
return loadSettingsFile(candidate);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { settings: defaultCodemodeSettings, source: null, warnings: [] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resolveEnabledLanguages(
|
|
136
|
+
settings: CodemodeSettings,
|
|
137
|
+
env: Environment = process.env,
|
|
138
|
+
): CodemodeSettings["languages"] {
|
|
139
|
+
return {
|
|
140
|
+
py: resolveLanguage(settings.languages.py, env[languageEnvironmentFlags.py]),
|
|
141
|
+
js: resolveLanguage(settings.languages.js, env[languageEnvironmentFlags.js]),
|
|
142
|
+
rb: resolveLanguage(settings.languages.rb, env[languageEnvironmentFlags.rb]),
|
|
143
|
+
jl: resolveLanguage(settings.languages.jl, env[languageEnvironmentFlags.jl]),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function loadSettingsFile(path: string): Promise<LoadedCodemodeSettings> {
|
|
148
|
+
const raw = await readFile(path, "utf8");
|
|
149
|
+
let parsed: unknown;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(raw);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
154
|
+
return {
|
|
155
|
+
settings: defaultCodemodeSettings,
|
|
156
|
+
source: path,
|
|
157
|
+
warnings: [`Invalid JSON in ${path}: ${message}. Falling back to codemode defaults.`],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!Check(codemodeSettingsSchema, parsed)) {
|
|
162
|
+
return {
|
|
163
|
+
settings: defaultCodemodeSettings,
|
|
164
|
+
source: path,
|
|
165
|
+
warnings: [`Invalid codemode settings in ${path}. Falling back to codemode defaults.`],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { settings: mergeSettings(parsed), source: path, warnings: [] };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function mergeSettings(input: CodemodeSettingsInput): ResolvedCodemodeSettings {
|
|
173
|
+
return {
|
|
174
|
+
languages: {
|
|
175
|
+
py: input.languages?.py ?? defaultCodemodeSettings.languages.py,
|
|
176
|
+
js: input.languages?.js ?? defaultCodemodeSettings.languages.js,
|
|
177
|
+
rb: input.languages?.rb ?? defaultCodemodeSettings.languages.rb,
|
|
178
|
+
jl: input.languages?.jl ?? defaultCodemodeSettings.languages.jl,
|
|
179
|
+
},
|
|
180
|
+
cellTimeoutSeconds: input.cellTimeoutSeconds ?? defaultCodemodeSettings.cellTimeoutSeconds,
|
|
181
|
+
parallelPoolWidth: input.parallelPoolWidth ?? defaultCodemodeSettings.parallelPoolWidth,
|
|
182
|
+
taskTools: {
|
|
183
|
+
task: input.taskTools?.task ?? defaultCodemodeSettings.taskTools.task,
|
|
184
|
+
output: input.taskTools?.output ?? defaultCodemodeSettings.taskTools.output,
|
|
185
|
+
},
|
|
186
|
+
outputSink: {
|
|
187
|
+
headBytes: input.outputSink?.headBytes ?? defaultCodemodeSettings.outputSink.headBytes,
|
|
188
|
+
maxColumns: input.outputSink?.maxColumns ?? defaultCodemodeSettings.outputSink.maxColumns,
|
|
189
|
+
},
|
|
190
|
+
statusEvents: input.statusEvents ?? defaultCodemodeSettings.statusEvents,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveLanguage(fileSetting: boolean, environmentValue: string | undefined): boolean {
|
|
195
|
+
if (environmentValue === undefined) return fileSetting;
|
|
196
|
+
switch (environmentValue.trim().toLowerCase()) {
|
|
197
|
+
case "0":
|
|
198
|
+
case "false":
|
|
199
|
+
return false;
|
|
200
|
+
case "1":
|
|
201
|
+
case "true":
|
|
202
|
+
return true;
|
|
203
|
+
default:
|
|
204
|
+
return fileSetting;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
209
|
+
try {
|
|
210
|
+
await access(path);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
3
|
+
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
4
|
+
import {
|
|
5
|
+
type CodemodeSettings,
|
|
6
|
+
loadCodemodeSettings,
|
|
7
|
+
type ResolvedCodemodeSettings,
|
|
8
|
+
resolveEnabledLanguages,
|
|
9
|
+
} from "../config/settings.ts";
|
|
10
|
+
import {
|
|
11
|
+
createInterpreterDetector,
|
|
12
|
+
getInterpreterAvailability,
|
|
13
|
+
type InterpreterAvailability,
|
|
14
|
+
} from "../interpreters/detect.ts";
|
|
15
|
+
import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
|
|
16
|
+
import type { EnabledEvalLanguages, EvalLanguage } from "../tool/types.ts";
|
|
17
|
+
import {
|
|
18
|
+
type CodemodeSessionManager,
|
|
19
|
+
type CreateCodemodeSessionManagerOptions,
|
|
20
|
+
createCodemodeSessionManager,
|
|
21
|
+
} from "./session-manager.ts";
|
|
22
|
+
|
|
23
|
+
export interface CodemodeRuntimeAPI {
|
|
24
|
+
readonly executeTool: AgentExecuteTool;
|
|
25
|
+
getActiveTools(): string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RuntimeFactoryOptions {
|
|
29
|
+
readonly createSessionManager?: (
|
|
30
|
+
options: CreateCodemodeSessionManagerOptions,
|
|
31
|
+
) => CodemodeSessionManager | Promise<CodemodeSessionManager>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SessionRuntime = {
|
|
35
|
+
readonly sessionId: string;
|
|
36
|
+
readonly cwd: string;
|
|
37
|
+
readonly parallelPoolWidth: number;
|
|
38
|
+
readonly manager: CodemodeSessionManager;
|
|
39
|
+
readonly enabledLanguages: EnabledEvalLanguages;
|
|
40
|
+
readonly settings: ResolvedCodemodeSettings;
|
|
41
|
+
readonly artifactsDir: string;
|
|
42
|
+
readonly executeTool: AgentExecuteTool;
|
|
43
|
+
readonly spawns: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export async function createRuntime(
|
|
47
|
+
pi: CodemodeRuntimeAPI,
|
|
48
|
+
ctx: ExtensionContext,
|
|
49
|
+
event: unknown,
|
|
50
|
+
complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>,
|
|
51
|
+
options: RuntimeFactoryOptions,
|
|
52
|
+
): Promise<SessionRuntime> {
|
|
53
|
+
const loaded = await loadCodemodeSettings({ cwd: ctx.cwd });
|
|
54
|
+
const settings: ResolvedCodemodeSettings = {
|
|
55
|
+
...loaded.settings,
|
|
56
|
+
languages: resolveEnabledLanguages(loaded.settings),
|
|
57
|
+
};
|
|
58
|
+
const availability = await getInterpreterAvailability(settings, createInterpreterDetector());
|
|
59
|
+
const enabledLanguages = enabledLanguagesFrom(settings, availability);
|
|
60
|
+
const artifacts = resolveSessionArtifactsDir(ctx.sessionManager.getSessionFile());
|
|
61
|
+
const activeTools = new Set(pi.getActiveTools());
|
|
62
|
+
const executeTool = createExecuteTool(pi, activeTools);
|
|
63
|
+
const create = options.createSessionManager ?? createCodemodeSessionManager;
|
|
64
|
+
const sessionId = sessionIdFrom(event);
|
|
65
|
+
const configuredPoolWidth = settings.parallelPoolWidth;
|
|
66
|
+
const parallelPoolWidth = Number.isFinite(configuredPoolWidth) ? Math.max(1, Math.trunc(configuredPoolWidth)) : 1;
|
|
67
|
+
const manager = await create({
|
|
68
|
+
sessionId,
|
|
69
|
+
cwd: ctx.cwd,
|
|
70
|
+
settings,
|
|
71
|
+
availability,
|
|
72
|
+
artifactsDir: artifacts.dir,
|
|
73
|
+
executeTool,
|
|
74
|
+
complete,
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
sessionId,
|
|
78
|
+
cwd: ctx.cwd,
|
|
79
|
+
parallelPoolWidth,
|
|
80
|
+
manager,
|
|
81
|
+
enabledLanguages,
|
|
82
|
+
settings,
|
|
83
|
+
artifactsDir: artifacts.dir,
|
|
84
|
+
executeTool,
|
|
85
|
+
spawns: activeTools.has(settings.taskTools.task),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createExecuteTool(pi: CodemodeRuntimeAPI, activeTools?: ReadonlySet<string>): AgentExecuteTool {
|
|
90
|
+
const executeTool: AgentExecuteTool = (toolName, params, executeOptions) =>
|
|
91
|
+
pi.executeTool(toolName, params, executeOptions);
|
|
92
|
+
return Object.assign(executeTool, {
|
|
93
|
+
isToolAvailable: (name: string): boolean => activeTools?.has(name) ?? pi.getActiveTools().includes(name),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sessionIdFrom(event: unknown): string {
|
|
98
|
+
if (typeof event === "object" && event !== null && "sessionId" in event && typeof event.sessionId === "string") {
|
|
99
|
+
return event.sessionId;
|
|
100
|
+
}
|
|
101
|
+
return crypto.randomUUID();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function enabledLanguagesFrom(
|
|
105
|
+
settings: CodemodeSettings,
|
|
106
|
+
availability: InterpreterAvailability,
|
|
107
|
+
): Record<EvalLanguage, boolean> {
|
|
108
|
+
return {
|
|
109
|
+
py: settings.languages.py && availability.py.detected.ok,
|
|
110
|
+
js: settings.languages.js && availability.js.detected.ok,
|
|
111
|
+
rb: settings.languages.rb && availability.rb.detected.ok,
|
|
112
|
+
jl: settings.languages.jl && availability.jl.detected.ok,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
|
+
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
4
|
+
import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
|
|
5
|
+
import {
|
|
6
|
+
CodemodeSessionDisposedError,
|
|
7
|
+
type CodemodeSessionManager,
|
|
8
|
+
type EvalExecutionTracker,
|
|
9
|
+
} from "./session-manager.ts";
|
|
10
|
+
|
|
11
|
+
type TrackedExecution = {
|
|
12
|
+
readonly promise: Promise<unknown>;
|
|
13
|
+
readonly controller: AbortController;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class CodemodeSessionNotStartedError extends Error {
|
|
17
|
+
readonly name = "CodemodeSessionNotStartedError";
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
super("codemode session has not started");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SessionManagerProxy implements CodemodeSessionManager, EvalExecutionTracker {
|
|
25
|
+
#current: CodemodeSessionManager | undefined;
|
|
26
|
+
#generation = 0;
|
|
27
|
+
#started = false;
|
|
28
|
+
#acceptingExecutions = false;
|
|
29
|
+
readonly #executions = new Set<TrackedExecution>();
|
|
30
|
+
|
|
31
|
+
beginReplacement(): number {
|
|
32
|
+
this.#generation++;
|
|
33
|
+
this.#acceptingExecutions = false;
|
|
34
|
+
this.#abortExecutions();
|
|
35
|
+
return this.#generation;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async replace(generation: number, next: CodemodeSessionManager): Promise<boolean> {
|
|
39
|
+
if (generation !== this.#generation) {
|
|
40
|
+
await next.dispose();
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
await this.#settleExecutions();
|
|
44
|
+
if (generation !== this.#generation) {
|
|
45
|
+
await next.dispose();
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const current = this.#current;
|
|
49
|
+
this.#current = undefined;
|
|
50
|
+
await current?.dispose();
|
|
51
|
+
if (generation !== this.#generation) {
|
|
52
|
+
await next.dispose();
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
this.#current = next;
|
|
56
|
+
this.#started = true;
|
|
57
|
+
this.#acceptingExecutions = true;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
assertEvalExecutionAllowed(): void {
|
|
62
|
+
if (this.#acceptingExecutions && this.#current !== undefined) return;
|
|
63
|
+
if (this.#started) throw new CodemodeSessionDisposedError();
|
|
64
|
+
throw new CodemodeSessionNotStartedError();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async trackEvalExecution<Result>(execution: Promise<Result>, controller: AbortController): Promise<Result> {
|
|
68
|
+
this.assertEvalExecutionAllowed();
|
|
69
|
+
const tracked: TrackedExecution = { promise: execution, controller };
|
|
70
|
+
this.#executions.add(tracked);
|
|
71
|
+
try {
|
|
72
|
+
return await execution;
|
|
73
|
+
} finally {
|
|
74
|
+
this.#executions.delete(tracked);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getKernel(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): Promise<EvalKernel> {
|
|
79
|
+
this.assertEvalExecutionAllowed();
|
|
80
|
+
const current = this.#current;
|
|
81
|
+
if (current === undefined) throw new CodemodeSessionNotStartedError();
|
|
82
|
+
return await current.getKernel(language, onMessage);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async complete(request: CompletionRequest, ctx: ExtensionContext): Promise<CompletionResult> {
|
|
86
|
+
this.assertEvalExecutionAllowed();
|
|
87
|
+
const current = this.#current;
|
|
88
|
+
if (current === undefined) throw new CodemodeSessionNotStartedError();
|
|
89
|
+
return await current.complete(request, ctx);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setContext(ctx: ExtensionContext): void {
|
|
93
|
+
this.#current?.setContext?.(ctx);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async dispose(): Promise<void> {
|
|
97
|
+
this.#generation++;
|
|
98
|
+
this.#acceptingExecutions = false;
|
|
99
|
+
this.#abortExecutions();
|
|
100
|
+
await this.#settleExecutions();
|
|
101
|
+
const current = this.#current;
|
|
102
|
+
this.#current = undefined;
|
|
103
|
+
await current?.dispose();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#abortExecutions(): void {
|
|
107
|
+
if (this.#executions.size === 0) return;
|
|
108
|
+
const error = new CodemodeSessionDisposedError();
|
|
109
|
+
for (const execution of this.#executions) execution.controller.abort(error);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async #settleExecutions(): Promise<void> {
|
|
113
|
+
if (this.#executions.size === 0) return;
|
|
114
|
+
await Promise.allSettled([...this.#executions].map((execution) => execution.promise));
|
|
115
|
+
}
|
|
116
|
+
}
|