dsh-loop-engine 1.0.0-rc2

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 (46) hide show
  1. package/README.md +151 -0
  2. package/README.zh.md +93 -0
  3. package/lib/client.js +403 -0
  4. package/lib/index.js +4310 -0
  5. package/lib/invariant.js +83 -0
  6. package/lib/types/client/LoopEngineBadge.d.ts +34 -0
  7. package/lib/types/client/LoopEngineSection.d.ts +34 -0
  8. package/lib/types/client/index.d.ts +28 -0
  9. package/lib/types/client/locales.d.ts +40 -0
  10. package/lib/types/client/store.d.ts +45 -0
  11. package/lib/types/commands.d.ts +32 -0
  12. package/lib/types/driver-core/ownership.d.ts +41 -0
  13. package/lib/types/driver-core/permission-knobs.d.ts +26 -0
  14. package/lib/types/driver-core/prompt.d.ts +23 -0
  15. package/lib/types/driver-core/skill-inject.d.ts +59 -0
  16. package/lib/types/engine-claude/agent.d.ts +102 -0
  17. package/lib/types/engine-claude/loop.d.ts +89 -0
  18. package/lib/types/engine-claude/mapping.d.ts +83 -0
  19. package/lib/types/engine-claude/permission.d.ts +41 -0
  20. package/lib/types/engine-claude/process.d.ts +59 -0
  21. package/lib/types/engine-claude/sdk.d.ts +57 -0
  22. package/lib/types/engine-claude/types.d.ts +18 -0
  23. package/lib/types/engine-codex/agent.d.ts +109 -0
  24. package/lib/types/engine-codex/appserver/client.d.ts +49 -0
  25. package/lib/types/engine-codex/appserver/mapping.d.ts +67 -0
  26. package/lib/types/engine-codex/appserver/thread.d.ts +66 -0
  27. package/lib/types/engine-codex/appserver/types.d.ts +215 -0
  28. package/lib/types/engine-codex/loop.d.ts +92 -0
  29. package/lib/types/engine-codex/permission.d.ts +32 -0
  30. package/lib/types/engine-codex/skills.d.ts +26 -0
  31. package/lib/types/engine-codex/types.d.ts +19 -0
  32. package/lib/types/engine-pi/agent.d.ts +125 -0
  33. package/lib/types/engine-pi/loop.d.ts +96 -0
  34. package/lib/types/engine-pi/permission.d.ts +43 -0
  35. package/lib/types/engine-pi/rpc/client.d.ts +105 -0
  36. package/lib/types/engine-pi/rpc/mapping.d.ts +37 -0
  37. package/lib/types/engine-pi/rpc/types.d.ts +235 -0
  38. package/lib/types/engine-pi/skills.d.ts +26 -0
  39. package/lib/types/engine-pi/types.d.ts +27 -0
  40. package/lib/types/index.d.ts +96 -0
  41. package/lib/types/invariant.d.ts +23 -0
  42. package/lib/types/namespace.d.ts +9 -0
  43. package/lib/types/patch-manager.d.ts +47 -0
  44. package/lib/types/settings.d.ts +29 -0
  45. package/lib/types/skills.d.ts +77 -0
  46. package/package.json +103 -0
package/lib/index.js ADDED
@@ -0,0 +1,4310 @@
1
+ var __knownSymbol = (name2, symbol) => (symbol = Symbol[name2]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name2);
2
+ var __typeError = (msg) => {
3
+ throw TypeError(msg);
4
+ };
5
+ var __using = (stack, value, async) => {
6
+ if (value != null) {
7
+ if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
8
+ var dispose, inner;
9
+ if (async) dispose = value[__knownSymbol("asyncDispose")];
10
+ if (dispose === void 0) {
11
+ dispose = value[__knownSymbol("dispose")];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") __typeError("Object not disposable");
15
+ if (inner) dispose = function() {
16
+ try {
17
+ inner.call(this);
18
+ } catch (e) {
19
+ return Promise.reject(e);
20
+ }
21
+ };
22
+ stack.push([async, dispose, value]);
23
+ } else if (async) {
24
+ stack.push([async]);
25
+ }
26
+ return value;
27
+ };
28
+ var __callDispose = (stack, error, hasError) => {
29
+ var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
30
+ return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
31
+ };
32
+ var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
33
+ var next = (it) => {
34
+ while (it = stack.pop()) {
35
+ try {
36
+ var result = it[1] && it[1].call(it[2]);
37
+ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
38
+ } catch (e) {
39
+ fail(e);
40
+ }
41
+ }
42
+ if (hasError) throw error;
43
+ };
44
+ return next();
45
+ };
46
+
47
+ // src/index.ts
48
+ import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
49
+ import { mkdir, readFile as readFile4, rename, writeFile } from "node:fs/promises";
50
+ import { randomUUID } from "node:crypto";
51
+ import { dirname as dirname3, join as join6 } from "node:path";
52
+ import z5 from "@deepseek-ai/schemastery";
53
+ import { installSettingsSection } from "@deepseek-ai/dsh-settings";
54
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
55
+
56
+ // src/engine-claude/loop.ts
57
+ import { Service } from "@deepseek-ai/cordis";
58
+ import z from "@deepseek-ai/schemastery";
59
+ import { emitAgentEvent } from "@deepseek-ai/dsh-agent";
60
+ import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
61
+ import { SessionPreparation } from "@deepseek-ai/dsh-session";
62
+
63
+ // src/engine-claude/agent.ts
64
+ import { Inbox, agentEvents } from "@deepseek-ai/dsh-agent";
65
+ import { LlmError, createAssistantMessage, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
66
+ import { createScope } from "@deepseek-ai/dsh-scope";
67
+ import { canonicalHeader } from "@deepseek-ai/dsh-session";
68
+ import { query as officialQuery } from "@anthropic-ai/claude-agent-sdk";
69
+
70
+ // src/engine-claude/mapping.ts
71
+ import {
72
+ CallId,
73
+ createToolResultMessage
74
+ } from "@deepseek-ai/dsh-llm";
75
+ function stringifyToolInput(input) {
76
+ try {
77
+ return JSON.stringify(input) ?? "null";
78
+ } catch {
79
+ return "[unserializable tool input]";
80
+ }
81
+ }
82
+ function mapAssistantMessage(message) {
83
+ const content = [];
84
+ const toolCalls = [];
85
+ for (const block of message.content) {
86
+ switch (block.type) {
87
+ case "text":
88
+ content.push({ type: "text", text: block.text });
89
+ break;
90
+ case "tool_use": {
91
+ const callId = CallId(block.id);
92
+ content.push({
93
+ type: "tool-call",
94
+ id: callId,
95
+ name: block.name,
96
+ arguments: stringifyToolInput(block.input)
97
+ });
98
+ toolCalls.push({
99
+ callId,
100
+ name: block.name,
101
+ arguments: stringifyToolInput(block.input)
102
+ });
103
+ break;
104
+ }
105
+ case "thinking":
106
+ content.push({ type: "reasoning", text: block.thinking });
107
+ break;
108
+ default:
109
+ break;
110
+ }
111
+ }
112
+ const usage = message.usage === void 0 ? void 0 : mapUsage(message.usage);
113
+ return {
114
+ content,
115
+ toolCalls,
116
+ usage,
117
+ model: message.model
118
+ };
119
+ }
120
+ function mapToolResults(message) {
121
+ const content = typeof message.content === "string" ? [] : message.content;
122
+ const results = [];
123
+ for (const block of content) {
124
+ if (block.type !== "tool_result") continue;
125
+ results.push(createToolResultMessage({
126
+ callId: CallId(block.tool_use_id),
127
+ content: toolResultContent(block.content),
128
+ isError: block.is_error === true
129
+ }));
130
+ }
131
+ return results;
132
+ }
133
+ function toolResultContent(content) {
134
+ const blocks = [];
135
+ if (typeof content === "string") {
136
+ blocks.push({ type: "text", text: content });
137
+ return blocks;
138
+ }
139
+ if (Array.isArray(content)) {
140
+ for (const block of content) {
141
+ const candidate = block;
142
+ if (candidate === null || candidate.type !== "text") continue;
143
+ if (typeof candidate.text !== "string") continue;
144
+ blocks.push({ type: "text", text: candidate.text });
145
+ }
146
+ }
147
+ if (blocks.length === 0) blocks.push({ type: "text", text: "(no content)" });
148
+ return blocks;
149
+ }
150
+ function mapUsage(usage) {
151
+ return {
152
+ inputTokens: usage.input_tokens,
153
+ outputTokens: usage.output_tokens,
154
+ ...usage.cache_read_input_tokens == null ? {} : { cacheReadTokens: usage.cache_read_input_tokens },
155
+ ...usage.cache_creation_input_tokens == null ? {} : { cacheWriteTokens: usage.cache_creation_input_tokens }
156
+ };
157
+ }
158
+ function mapStreamEvent(event, toolCalls) {
159
+ switch (event.type) {
160
+ case "content_block_start": {
161
+ const block = event.content_block;
162
+ if (block.type === "text") {
163
+ return [{ type: "block-start", index: event.index, blockType: "text" }];
164
+ }
165
+ if (block.type === "thinking") {
166
+ return [{ type: "block-start", index: event.index, blockType: "reasoning" }];
167
+ }
168
+ if (block.type === "tool_use") {
169
+ toolCalls.set(event.index, { callId: CallId(block.id), name: block.name });
170
+ return [{ type: "block-start", index: event.index, blockType: "tool-call" }];
171
+ }
172
+ return [];
173
+ }
174
+ case "content_block_delta": {
175
+ const delta = event.delta;
176
+ if (delta.type === "text_delta") {
177
+ return [{ type: "text-delta", index: event.index, text: delta.text }];
178
+ }
179
+ if (delta.type === "thinking_delta") {
180
+ return [{ type: "reasoning-delta", index: event.index, text: delta.thinking }];
181
+ }
182
+ if (delta.type === "input_json_delta") {
183
+ const call = toolCalls.get(event.index);
184
+ return [{
185
+ type: "tool-call-delta",
186
+ index: event.index,
187
+ id: call?.callId ?? CallId(`call-${event.index}`),
188
+ ...call === void 0 ? {} : { name: call.name },
189
+ argumentsDelta: delta.partial_json
190
+ }];
191
+ }
192
+ return [];
193
+ }
194
+ default:
195
+ return [];
196
+ }
197
+ }
198
+
199
+ // src/driver-core/prompt.ts
200
+ var OMITTED_IMAGE_TEXT = "[image omitted: the driver does not transcribe images; read the file when a path is available]";
201
+ function frame(tag, body) {
202
+ return `<${tag}>
203
+ ${body}
204
+ </${tag}>`;
205
+ }
206
+ function renderAssistantBlocks(blocks) {
207
+ const sections = [];
208
+ for (const block of blocks) {
209
+ switch (block.type) {
210
+ case "text":
211
+ sections.push(block.text);
212
+ break;
213
+ case "tool-call":
214
+ sections.push(`[tool call: ${block.name}(${block.arguments})]`);
215
+ break;
216
+ case "image":
217
+ sections.push(OMITTED_IMAGE_TEXT);
218
+ break;
219
+ default:
220
+ break;
221
+ }
222
+ }
223
+ return sections.join("\n\n");
224
+ }
225
+ function renderToolResult(message) {
226
+ const block = message.content[0];
227
+ const body = block.content.map((child) => {
228
+ switch (child.type) {
229
+ case "text":
230
+ return child.text;
231
+ case "image":
232
+ return OMITTED_IMAGE_TEXT;
233
+ default:
234
+ return "";
235
+ }
236
+ }).filter((section) => section !== "").join("\n\n");
237
+ const tag = block.isError === true ? "tool-result-error" : "tool-result";
238
+ return frame(tag, body || "(no content)");
239
+ }
240
+ function serializeHistory(messages) {
241
+ const sections = [];
242
+ for (const message of messages) {
243
+ switch (message.role) {
244
+ case "assistant": {
245
+ const body = renderAssistantBlocks(message.content);
246
+ if (body !== "") sections.push(frame("assistant", body));
247
+ break;
248
+ }
249
+ case "user": {
250
+ const user = message;
251
+ if (user.source.kind === "tool") {
252
+ sections.push(renderToolResult(user));
253
+ } else {
254
+ const body = user.content.map((block) => {
255
+ switch (block.type) {
256
+ case "text":
257
+ return block.text;
258
+ case "image":
259
+ return OMITTED_IMAGE_TEXT;
260
+ default:
261
+ return "";
262
+ }
263
+ }).filter((section) => section !== "").join("\n\n");
264
+ sections.push(frame("user", body || "(no content)"));
265
+ }
266
+ break;
267
+ }
268
+ default:
269
+ break;
270
+ }
271
+ }
272
+ return sections.join("\n\n");
273
+ }
274
+
275
+ // src/driver-core/permission-knobs.ts
276
+ var SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
277
+ var APPROVAL_POLICIES = ["ask", "never"];
278
+ function sessionSandboxMode(events) {
279
+ for (let index = events.length - 1; index >= 0; index -= 1) {
280
+ const event = events[index];
281
+ if (event.type !== "sandbox/mode") continue;
282
+ const mode = event.data.mode;
283
+ return SANDBOX_MODES.includes(mode) ? mode : void 0;
284
+ }
285
+ return void 0;
286
+ }
287
+ function sessionApprovalPolicy(events) {
288
+ for (let index = events.length - 1; index >= 0; index -= 1) {
289
+ const event = events[index];
290
+ if (event.type !== "approval/policy") continue;
291
+ const policy = event.data.policy;
292
+ return APPROVAL_POLICIES.includes(policy) ? policy : void 0;
293
+ }
294
+ return void 0;
295
+ }
296
+
297
+ // src/engine-claude/permission.ts
298
+ function resolveSessionPermission(events) {
299
+ if (sessionSandboxMode(events) === "danger-full-access") return { kind: "bypass" };
300
+ if (sessionApprovalPolicy(events) === "ask") return { kind: "ask" };
301
+ return { kind: "deny" };
302
+ }
303
+ var REASON_INPUT_CAP = 200;
304
+ function approvalReason(toolName, input) {
305
+ const excerpt = JSON.stringify(input);
306
+ const bounded = excerpt.length > REASON_INPUT_CAP ? `${excerpt.slice(0, REASON_INPUT_CAP - 3)}...` : excerpt;
307
+ return `Claude Code requests permission to run ${toolName}: ${bounded}`;
308
+ }
309
+
310
+ // src/engine-claude/sdk.ts
311
+ import { scrubbedParentEnv as scrubbedParentEnv2 } from "@deepseek-ai/dsh-subprocess";
312
+
313
+ // src/engine-claude/process.ts
314
+ import { EventEmitter } from "node:events";
315
+ import {
316
+ scrubbedParentEnv
317
+ } from "@deepseek-ai/dsh-subprocess";
318
+ function thrown(value) {
319
+ return value instanceof Error ? value : new Error(String(value));
320
+ }
321
+ function sdkEnvironmentOverlay(env) {
322
+ const overlay = { ...env };
323
+ for (const name2 of Object.keys(scrubbedParentEnv())) {
324
+ if (!(name2 in env)) overlay[name2] = void 0;
325
+ }
326
+ return overlay;
327
+ }
328
+ function claudeSpawnSpec(options, graceMs) {
329
+ if (options.cwd === void 0 || options.cwd.length === 0) {
330
+ throw new Error("agent-loop-claude-code: SDK spawn request omitted its workspace");
331
+ }
332
+ return {
333
+ argv: [options.command, ...options.args],
334
+ cwd: options.cwd,
335
+ stdio: { stdin: "pipe", stdout: "pipe", stderr: "inherit" },
336
+ graceMs,
337
+ signal: options.signal,
338
+ env: sdkEnvironmentOverlay(options.env)
339
+ };
340
+ }
341
+ var ManagedClaudeCodeProcess = class {
342
+ /**
343
+ * Project a managed process with piped stdin and stdout.
344
+ * @param child - shared handle that remains the process-tree authority.
345
+ */
346
+ constructor(child) {
347
+ this.child = child;
348
+ this.stdin = child.stdin;
349
+ this.stdout = child.stdout;
350
+ this.events.on("error", () => {
351
+ });
352
+ void child.done.then(
353
+ (outcome) => {
354
+ this.outcomeValue = outcome;
355
+ this.events.emit("exit", outcome.exitCode, outcome.signal);
356
+ },
357
+ (error) => {
358
+ this.events.emit("error", thrown(error));
359
+ }
360
+ );
361
+ }
362
+ child;
363
+ stdin;
364
+ stdout;
365
+ events = new EventEmitter();
366
+ outcomeValue;
367
+ killRequested = false;
368
+ /** Whether the SDK has requested managed tree termination. */
369
+ get killed() {
370
+ return this.killRequested;
371
+ }
372
+ /** Direct-child exit code, or null while running or after signal exit. */
373
+ get exitCode() {
374
+ return this.outcomeValue?.exitCode ?? null;
375
+ }
376
+ /** Direct-child terminating signal, if any. */
377
+ get signalCode() {
378
+ return this.outcomeValue?.signal ?? null;
379
+ }
380
+ /** Exact managed-process outcome after exit, or undefined while running. */
381
+ get outcome() {
382
+ return this.outcomeValue;
383
+ }
384
+ /**
385
+ * Route the SDK's termination request to the tree-scoped process owner.
386
+ * @param _signal - SDK-selected signal; the shared seam owns its escalation ladder.
387
+ * @returns false only after exit or a previous termination request.
388
+ */
389
+ kill(_signal) {
390
+ if (this.killRequested || this.outcomeValue !== void 0) {
391
+ return false;
392
+ }
393
+ this.killRequested = true;
394
+ this.child.terminate();
395
+ return true;
396
+ }
397
+ /** Register a persistent process lifecycle listener. */
398
+ on(event, listener) {
399
+ this.events.on(event, listener);
400
+ }
401
+ /** Register a one-shot process lifecycle listener. */
402
+ once(event, listener) {
403
+ this.events.once(event, listener);
404
+ }
405
+ /** Remove a process lifecycle listener. */
406
+ off(event, listener) {
407
+ this.events.off(event, listener);
408
+ }
409
+ };
410
+
411
+ // src/engine-claude/sdk.ts
412
+ var DEFAULT_PERMISSION_MODE = "dontAsk";
413
+ var DEFAULT_DISPOSE_GRACE_MS = 3e3;
414
+ var UNATTENDED_DIALOG_KINDS = ["refusal_fallback_prompt"];
415
+ function unattendedDiagnostic(mode, kind, answer, why) {
416
+ return `claude-code: ${kind} ${answer} (mode ${mode}): ${why}`;
417
+ }
418
+ function claudeQueryOptions(spec, controller) {
419
+ const report = spec.onUnattended ?? (() => {
420
+ });
421
+ const forward = spec.onToolPermission;
422
+ return {
423
+ abortController: controller,
424
+ cwd: spec.cwd,
425
+ env: {
426
+ ...scrubbedParentEnv2(),
427
+ ...spec.env
428
+ },
429
+ // Emit `stream_event` partial messages so the loop can forward token
430
+ // deltas to the dsh session as `assistant/chunk` events (the web surface
431
+ // streams those). Without it the SDK yields only complete `assistant`
432
+ // messages, so the surface renders each response all at once.
433
+ includePartialMessages: true,
434
+ persistSession: false,
435
+ disallowedTools: spec.permissionMode === "plan" ? ["AskUserQuestion", "ExitPlanMode"] : ["AskUserQuestion"],
436
+ permissionMode: spec.permissionMode,
437
+ ...spec.model === void 0 ? {} : { model: spec.model },
438
+ ...spec.maxTurns === void 0 ? {} : { maxTurns: spec.maxTurns },
439
+ ...spec.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {
440
+ canUseTool: forward === void 0 ? () => {
441
+ report(unattendedDiagnostic(
442
+ spec.permissionMode,
443
+ "tool permission",
444
+ "denied",
445
+ "the Claude Code driver does not request human approval"
446
+ ));
447
+ return Promise.resolve({
448
+ behavior: "deny",
449
+ message: "This unattended Claude Code driver cannot request human approval."
450
+ });
451
+ } : async (toolName, input, { signal }) => {
452
+ const verdict = await forward(toolName, input, signal);
453
+ return verdict === "allow" ? { behavior: "allow", updatedInput: input } : { behavior: "deny", message: "The dsh user rejected this action." };
454
+ }
455
+ },
456
+ onElicitation: () => {
457
+ report(unattendedDiagnostic(
458
+ spec.permissionMode,
459
+ "MCP elicitation",
460
+ "declined",
461
+ "the driver does not collect interactive MCP input"
462
+ ));
463
+ return Promise.resolve({ action: "decline" });
464
+ },
465
+ onUserDialog: () => {
466
+ report(unattendedDiagnostic(
467
+ spec.permissionMode,
468
+ "user dialog",
469
+ "cancelled",
470
+ "the driver does not render blocking dialogs"
471
+ ));
472
+ return Promise.resolve({ behavior: "cancelled" });
473
+ },
474
+ supportedDialogKinds: UNATTENDED_DIALOG_KINDS,
475
+ spawnClaudeCodeProcess: (options) => {
476
+ const child = spec.spawn(claudeSpawnSpec(options, spec.disposeGraceMs));
477
+ return new ManagedClaudeCodeProcess(child);
478
+ }
479
+ };
480
+ }
481
+
482
+ // src/driver-core/skill-inject.ts
483
+ var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
484
+ var SKILL_GESTURE = /(^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/g;
485
+ function isSkillName(name2) {
486
+ return SKILL_NAME_RE.test(name2);
487
+ }
488
+ function escapeText(value) {
489
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
490
+ }
491
+ function escapeAttr(value) {
492
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
493
+ }
494
+ function renderSkillContent(skill) {
495
+ return [
496
+ `<skill_content name="${escapeAttr(skill.name)}">`,
497
+ "<skill_resources>",
498
+ skill.resourceBase !== void 0 && skill.resourceBase.kind === "directory" ? `Base directory for this skill: ${escapeText(skill.resourceBase.path)}. Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.` : `Resources for this skill are managed by provider "${escapeText(skill.provider)}". Load referenced resources only as needed.`,
499
+ "</skill_resources>",
500
+ "",
501
+ "<skill_instructions>",
502
+ skill.content,
503
+ "</skill_instructions>",
504
+ "</skill_content>"
505
+ ].join("\n");
506
+ }
507
+ function invokedSkillNames(messages) {
508
+ const names = [];
509
+ for (const message of messages) {
510
+ if (message.source.kind !== "user") continue;
511
+ for (const block of message.content) {
512
+ if (block.type !== "text") continue;
513
+ for (const match of block.text.matchAll(SKILL_GESTURE)) {
514
+ const name2 = match[2];
515
+ if (name2 !== void 0 && !names.includes(name2)) names.push(name2);
516
+ }
517
+ }
518
+ }
519
+ return names;
520
+ }
521
+
522
+ // src/engine-claude/agent.ts
523
+ var PROVIDER = "claude-code";
524
+ var NATIVE_MODEL_LABEL = "claude-code-native";
525
+ function failureCode(subtype) {
526
+ switch (subtype) {
527
+ case "error_during_execution":
528
+ case "error_max_turns":
529
+ case "error_max_budget_usd":
530
+ case "error_max_structured_output_retries":
531
+ return `CLAUDE_CODE_${subtype.toUpperCase()}`;
532
+ default:
533
+ return "CLAUDE_CODE_ERROR";
534
+ }
535
+ }
536
+ var ClaudeCodeAgent = class {
537
+ constructor(loopCtx, id, options, session, config) {
538
+ this.loopCtx = loopCtx;
539
+ this.id = id;
540
+ this.options = options;
541
+ this.session = session;
542
+ this.config = config;
543
+ this.dispatch = agentEvents(loopCtx, this);
544
+ this.inbox = new Inbox(session, {
545
+ inserted: (message) => {
546
+ this.dispatch.emit("agent/inbox/inserted", { message });
547
+ },
548
+ discarded: (message) => {
549
+ this.dispatch.emit("agent/inbox/discarded", { message });
550
+ },
551
+ claimed: (message, turn) => {
552
+ this.dispatch.emit("agent/inbox/claimed", { message, turn });
553
+ }
554
+ });
555
+ const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
556
+ this.phase = { kind: "idle", lastTurn };
557
+ this.scope = createScope(loopCtx, this);
558
+ this.ctx = this.scope.ctx.extend({ agent: this });
559
+ }
560
+ loopCtx;
561
+ id;
562
+ options;
563
+ session;
564
+ config;
565
+ inbox;
566
+ phase;
567
+ activityDone = Promise.resolve();
568
+ /** The agent-scoped registration boundary; the lifecycle owner unwinds it after the driver exits. */
569
+ scope;
570
+ ctx;
571
+ /** Fused dispatcher, built once in the constructor so hot-path dispatches never allocate. */
572
+ dispatch;
573
+ /** Whether this loop instance has appended its initial/resume request anchor. */
574
+ requestHeaderLogged = false;
575
+ get status() {
576
+ return this.phase.kind === "idle" || this.phase.kind === "maintenance" ? "idle" : "running";
577
+ }
578
+ /** Commit a phase and publish its externally visible status transition. */
579
+ setPhase(next) {
580
+ const previousStatus = this.status;
581
+ this.phase = next;
582
+ const status = this.status;
583
+ if (status !== previousStatus) {
584
+ this.dispatch.emit("agent/status", { status });
585
+ }
586
+ }
587
+ send(message, target, wakeup) {
588
+ const wakingAfterAbort = wakeup && this.phase.kind !== "idle" && this.phase.abort.signal.aborted;
589
+ const resolvedTarget = wakingAfterAbort ? "next-turn" : target;
590
+ this.inbox.splice(resolvedTarget, Infinity, 0, [message]);
591
+ if (wakeup) this.wakeDriver(wakingAfterAbort);
592
+ }
593
+ /**
594
+ * Queue a message for the next turn and wake the driver.
595
+ * @param input - the user message to deliver.
596
+ */
597
+ followup(input) {
598
+ this.send(input, "next-turn", true);
599
+ }
600
+ /**
601
+ * Queue a message for the running step and wake the driver.
602
+ * @param input - the user message to deliver.
603
+ */
604
+ steer(input) {
605
+ this.send(input, "next-step", true);
606
+ }
607
+ /**
608
+ * Queue a message for the running step without waking the driver.
609
+ * @param input - the user message to deliver.
610
+ */
611
+ inject(input) {
612
+ this.send(input, "next-step", false);
613
+ }
614
+ cancel(cause, options = {}) {
615
+ if (!options.keepInbox) {
616
+ this.inbox.clear();
617
+ if (this.phase.kind !== "idle") this.phase.wakeRequested = false;
618
+ }
619
+ if (this.phase.kind !== "idle") this.phase.abort.abort(cause);
620
+ }
621
+ /**
622
+ * Run a maintenance job while the agent is idle.
623
+ * @param job - the maintenance operation, receiving the phase abort signal.
624
+ * @returns the maintenance result.
625
+ */
626
+ runMaintenance(job) {
627
+ if (this.phase.kind !== "idle") throw new Error(`agent "${this.id}" already has active work`);
628
+ const done = Promise.withResolvers();
629
+ const maintenance = {
630
+ kind: "maintenance",
631
+ abort: new AbortController(),
632
+ lastTurn: this.phase.lastTurn,
633
+ wakeRequested: false
634
+ };
635
+ this.setPhase(maintenance);
636
+ this.activityDone = done.promise;
637
+ return (async () => {
638
+ try {
639
+ return await job(maintenance.abort.signal);
640
+ } finally {
641
+ this.setPhase({ kind: "idle", lastTurn: maintenance.lastTurn });
642
+ if (maintenance.wakeRequested && this.inbox.hasPending) this.wakeDriver();
643
+ done.resolve();
644
+ }
645
+ })();
646
+ }
647
+ /**
648
+ * Start one driver, or latch its wake behind maintenance or an aborted
649
+ * activity. A wake sent while idle always opens its turn boundary, even
650
+ * when its message was cleared; only a latched replay is suppressed when
651
+ * the queue no longer holds the wake.
652
+ * @param wakeAfterAbort - the {@link send} classification, captured before
653
+ * the inbox insertion so a reentrant cancel cannot reclassify it.
654
+ */
655
+ wakeDriver(wakeAfterAbort = false) {
656
+ if (this.phase.kind !== "idle") {
657
+ const reason = this.phase.abort.signal.reason;
658
+ if (reason?.kind !== "disposed" && (this.phase.kind === "maintenance" || wakeAfterAbort)) {
659
+ this.phase.wakeRequested = true;
660
+ }
661
+ return;
662
+ }
663
+ const driver = Promise.withResolvers();
664
+ this.activityDone = driver.promise;
665
+ this.setPhase({
666
+ kind: "running",
667
+ abort: new AbortController(),
668
+ turn: this.phase.lastTurn,
669
+ step: 0,
670
+ wakeRequested: false
671
+ });
672
+ this.loopCtx.agents.withInitiator(this, () => this.kick()).then(driver.resolve, driver.reject);
673
+ }
674
+ async whenIdle() {
675
+ let activity;
676
+ do {
677
+ await (activity = this.activityDone);
678
+ } while (activity !== this.activityDone);
679
+ }
680
+ /** Report one failure at its live boundary, then preserve it for driver containment. */
681
+ throwError(error) {
682
+ const turn = this.phase.kind === "running" ? this.phase.turn : this.phase.lastTurn;
683
+ const step = this.phase.kind === "running" ? this.phase.step : 0;
684
+ this.dispatch.emit("agent/error", { turn, step, error });
685
+ throw error;
686
+ }
687
+ async kick() {
688
+ try {
689
+ while (await this.turn()) {
690
+ }
691
+ } catch (_error) {
692
+ } finally {
693
+ if (this.phase.kind === "running") {
694
+ const { turn, wakeRequested } = this.phase;
695
+ this.setPhase({ kind: "idle", lastTurn: turn });
696
+ if (wakeRequested && this.inbox.hasPending) this.wakeDriver();
697
+ }
698
+ }
699
+ }
700
+ async preStep(target, position) {
701
+ if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": pre-step outside running phase`);
702
+ const signal = this.phase.abort.signal;
703
+ const claimed = this.inbox.claim(target, position.turn);
704
+ const decision = await this.dispatch.waterfall(
705
+ "agent/pre-step",
706
+ { messages: claimed, ...position, signal },
707
+ () => Promise.resolve({ kind: "enter", messages: claimed })
708
+ );
709
+ signal.throwIfAborted();
710
+ if (decision.kind === "reject") return decision;
711
+ const injected = await this.injectSkills(decision.messages, signal);
712
+ signal.throwIfAborted();
713
+ return injected !== decision.messages ? { kind: "enter", messages: [...injected] } : { ...decision };
714
+ }
715
+ /**
716
+ * Scan the step's user messages for `/name` skill gestures, load each
717
+ * matching skill, and inject the rendered skill content into the message
718
+ * batch. This mirrors what dsh-tool-skill does for the in-process engine.
719
+ * @param messages - the current step's message batch.
720
+ * @param signal - cancellation signal (aborted loads are silently dropped).
721
+ * @returns the original batch when no skill was invoked, or an extended
722
+ * batch with injected skill-content messages appended.
723
+ */
724
+ async injectSkills(messages, signal) {
725
+ const names = invokedSkillNames(messages);
726
+ if (names.length === 0) return messages;
727
+ const skills = this.loopCtx.get("skills");
728
+ if (skills === void 0) return messages;
729
+ const cwd = this.session.header.cwd;
730
+ const injections = [];
731
+ for (const name2 of names) {
732
+ if (!isSkillName(name2)) continue;
733
+ let skill;
734
+ try {
735
+ skill = await skills.get(name2, { signal, scope: this, ...cwd === void 0 ? {} : { cwd } });
736
+ } catch {
737
+ continue;
738
+ }
739
+ if (skill === void 0 || !skill.invocation.userInvocable) continue;
740
+ if (signal.aborted) return messages;
741
+ injections.push(createUserMessage({
742
+ content: [{ type: "text", text: renderSkillContent(skill) }],
743
+ source: { kind: "skill-invocation", name: name2, form: "instructions" }
744
+ }));
745
+ }
746
+ return injections.length > 0 ? [...messages, ...injections] : messages;
747
+ }
748
+ /**
749
+ * Resolve the native permission handling for one query. A deployment-pinned
750
+ * mode wins outright; otherwise the session's durable dsh permission knobs
751
+ * decide per query (mid-session preset switches included): full access
752
+ * bypasses native checks, an `ask` policy forwards each native permission
753
+ * request to the dsh approval seam, and anything else fails closed with the
754
+ * unattended deny-all stance.
755
+ * @returns the permission fields of the query spec.
756
+ */
757
+ queryPermission() {
758
+ if (this.config.permissionMode !== void 0) return { permissionMode: this.config.permissionMode };
759
+ const permission = resolveSessionPermission(this.session.events);
760
+ if (permission.kind === "bypass") return { permissionMode: "bypassPermissions" };
761
+ if (permission.kind === "ask") {
762
+ const approval = this.loopCtx.get("approval");
763
+ if (approval !== void 0) {
764
+ return {
765
+ permissionMode: "default",
766
+ onToolPermission: async (toolName, input, signal) => {
767
+ const outcome = await approval.request({
768
+ agent: this,
769
+ toolName,
770
+ reason: approvalReason(toolName, input),
771
+ signal
772
+ });
773
+ return outcome === "allowed-once" ? "allow" : "deny";
774
+ }
775
+ };
776
+ }
777
+ }
778
+ return { permissionMode: DEFAULT_PERMISSION_MODE };
779
+ }
780
+ /** Open one turn before claiming its first proposed step. */
781
+ async turn() {
782
+ if (this.phase.kind !== "running") {
783
+ this.throwError(new Error(`agent "${this.id}": turn without driver reservation`));
784
+ }
785
+ const phase = this.phase;
786
+ const { signal } = phase.abort;
787
+ signal.throwIfAborted();
788
+ const turn = phase.turn + 1;
789
+ try {
790
+ this.session.append("turn/start", { turn });
791
+ } catch (error) {
792
+ this.throwError(error);
793
+ }
794
+ phase.turn = turn;
795
+ let turnEnds = null;
796
+ let target = "next-turn";
797
+ try {
798
+ while (true) {
799
+ signal.throwIfAborted();
800
+ const step = phase.step + 1;
801
+ const decision = await this.preStep(target, { turn, step });
802
+ if (decision.kind === "reject") {
803
+ turnEnds = { kind: "blocked" };
804
+ return false;
805
+ }
806
+ if (turnEnds && decision.messages.length === 0) break;
807
+ if (phase.step === 0 && decision.messages.length === 0) {
808
+ turnEnds = { kind: "completed" };
809
+ return false;
810
+ }
811
+ signal.throwIfAborted();
812
+ this.session.append("step/start", { turn, step });
813
+ phase.step = step;
814
+ try {
815
+ for (const message of decision.messages) {
816
+ this.session.append("user/message", message, { surfaceOp: "append" });
817
+ }
818
+ const stepEnd = await this.step();
819
+ if (turnEnds === null) turnEnds = stepEnd;
820
+ } finally {
821
+ this.session.append("step/end", { turn, step });
822
+ }
823
+ signal.throwIfAborted();
824
+ if (turnEnds && this.inbox.nextStep.length === 0) {
825
+ await this.dispatch.serial("agent/turn-stopping", { turn, signal });
826
+ signal.throwIfAborted();
827
+ }
828
+ if (turnEnds && this.inbox.nextStep.length === 0) break;
829
+ target = "next-step";
830
+ }
831
+ } catch (error) {
832
+ if (signal.aborted) {
833
+ turnEnds = { kind: "aborted", reason: signal.reason };
834
+ throw error;
835
+ }
836
+ turnEnds = {
837
+ kind: "error",
838
+ error: error instanceof LlmError ? error.failure : { message: errorChain(error), code: "UNKNOWN" }
839
+ };
840
+ this.throwError(error);
841
+ } finally {
842
+ try {
843
+ this.session.append("turn/end", { turn, reason: turnEnds });
844
+ } catch (error) {
845
+ this.throwError(error);
846
+ }
847
+ }
848
+ if (!this.inbox.hasPending) return false;
849
+ phase.abort = new AbortController();
850
+ phase.wakeRequested = false;
851
+ phase.step = 0;
852
+ return true;
853
+ }
854
+ /** Model label recorded in the request header for one lifecycle. */
855
+ modelLabel() {
856
+ return this.config.model ?? NATIVE_MODEL_LABEL;
857
+ }
858
+ /** Append the request header snapshot once per loop instance. */
859
+ assertRequestHeader() {
860
+ if (this.requestHeaderLogged) return;
861
+ const header = canonicalHeader({
862
+ config: { provider: PROVIDER, model: this.modelLabel() }
863
+ });
864
+ const baseline = this.session.requestHeader();
865
+ this.session.append("request/header", {
866
+ header,
867
+ reason: baseline === void 0 ? "initial" : "resume"
868
+ });
869
+ this.requestHeaderLogged = true;
870
+ }
871
+ /** Run one Claude Code query for the current step and map its transcript into the session log. */
872
+ async step() {
873
+ if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
874
+ const { turn, step, abort: { signal } } = this.phase;
875
+ signal.throwIfAborted();
876
+ const cwd = this.session.header.cwd;
877
+ if (cwd === void 0 || cwd.length === 0) {
878
+ throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
879
+ }
880
+ const history = this.session.deriveMessages();
881
+ const prompt = serializeHistory(history);
882
+ if (prompt.length === 0) {
883
+ throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
884
+ }
885
+ this.assertRequestHeader();
886
+ signal.throwIfAborted();
887
+ const controller = new AbortController();
888
+ const cancel = () => {
889
+ if (!controller.signal.aborted) {
890
+ controller.abort(signal.reason instanceof Error ? signal.reason : new Error(`agent "${this.id}" query aborted`));
891
+ }
892
+ };
893
+ signal.addEventListener("abort", cancel, { once: true });
894
+ const diagnostics = [];
895
+ try {
896
+ const options = claudeQueryOptions({
897
+ cwd,
898
+ ...this.queryPermission(),
899
+ env: this.config.env,
900
+ disposeGraceMs: this.config.disposeGraceMs,
901
+ ...this.config.model === void 0 ? {} : { model: this.config.model },
902
+ ...this.config.maxTurns === void 0 ? {} : { maxTurns: this.config.maxTurns },
903
+ spawn: (spec) => this.loopCtx.subprocess.spawn(spec),
904
+ onUnattended: (line) => {
905
+ diagnostics.push(line);
906
+ }
907
+ }, controller);
908
+ const query = officialQuery({ prompt, options });
909
+ let finished = false;
910
+ const chunkSeqs = [];
911
+ const toolCalls = /* @__PURE__ */ new Map();
912
+ const reasoningByIndex = /* @__PURE__ */ new Map();
913
+ let pendingUsage;
914
+ signal.throwIfAborted();
915
+ for await (const message of query) {
916
+ signal.throwIfAborted();
917
+ switch (message.type) {
918
+ case "stream_event": {
919
+ for (const chunk of mapStreamEvent(message.event, toolCalls)) {
920
+ chunkSeqs.push(this.session.append("assistant/chunk", { turn, step, chunk }).seq);
921
+ if (chunk.type === "reasoning-delta") {
922
+ reasoningByIndex.set(chunk.index, (reasoningByIndex.get(chunk.index) ?? "") + chunk.text);
923
+ }
924
+ }
925
+ break;
926
+ }
927
+ case "assistant": {
928
+ const mapped = mapAssistantMessage(message.message);
929
+ const isReasoningOnly = mapped.content.length > 0 && mapped.content.every((block) => block.type === "reasoning");
930
+ if (isReasoningOnly) {
931
+ const reasoning = mapped.content;
932
+ reasoningByIndex.clear();
933
+ reasoning.forEach((block, index) => {
934
+ reasoningByIndex.set(index, block.text);
935
+ });
936
+ pendingUsage = mapped.usage;
937
+ break;
938
+ }
939
+ let content = mapped.content;
940
+ if (reasoningByIndex.size > 0 && !content.some((block) => block.type === "reasoning")) {
941
+ const synthesized = [...reasoningByIndex.entries()].sort((a, b) => a[0] - b[0]).map(([, text]) => ({ type: "reasoning", text }));
942
+ content = [...synthesized, ...content];
943
+ }
944
+ if (content.length > 0) {
945
+ reasoningByIndex.clear();
946
+ const usage = mapped.usage ?? pendingUsage;
947
+ pendingUsage = void 0;
948
+ this.session.append("assistant/message", {
949
+ turn,
950
+ step,
951
+ message: createAssistantMessage({
952
+ content,
953
+ source: { provider: PROVIDER, model: mapped.model }
954
+ }),
955
+ ...usage === void 0 ? {} : { usage }
956
+ }, {
957
+ surfaceOp: "append",
958
+ // Link the durable message to the chunks that streamed it, so
959
+ // replay can reconstruct the partial exactly as shown.
960
+ ...chunkSeqs.length === 0 ? {} : { sourceEventSeqs: chunkSeqs }
961
+ });
962
+ }
963
+ for (const call of mapped.toolCalls) {
964
+ this.session.append("tool/call", {
965
+ turn,
966
+ step,
967
+ callId: call.callId,
968
+ name: call.name,
969
+ arguments: call.arguments
970
+ });
971
+ }
972
+ break;
973
+ }
974
+ case "user": {
975
+ for (const result of mapToolResults(message.message)) {
976
+ this.session.append("tool/result", { turn, step, message: result }, { surfaceOp: "append" });
977
+ }
978
+ break;
979
+ }
980
+ case "result": {
981
+ if (reasoningByIndex.size > 0) {
982
+ const trailing = [...reasoningByIndex.entries()].sort((a, b) => a[0] - b[0]).map(([, text]) => ({ type: "reasoning", text }));
983
+ reasoningByIndex.clear();
984
+ this.session.append("assistant/message", {
985
+ turn,
986
+ step,
987
+ message: createAssistantMessage({
988
+ content: trailing,
989
+ source: { provider: PROVIDER, model: NATIVE_MODEL_LABEL }
990
+ }),
991
+ ...pendingUsage === void 0 ? {} : { usage: pendingUsage }
992
+ }, { surfaceOp: "append" });
993
+ pendingUsage = void 0;
994
+ }
995
+ if (message.subtype === "success") {
996
+ finished = true;
997
+ } else {
998
+ const summary = message.errors[0] ?? `claude code query failed (${message.subtype})`;
999
+ throw new LlmError(summary, failureCode(message.subtype));
1000
+ }
1001
+ break;
1002
+ }
1003
+ default:
1004
+ break;
1005
+ }
1006
+ }
1007
+ if (!finished) {
1008
+ throw new LlmError(
1009
+ `agent "${this.id}": claude-code query ended without a result message`,
1010
+ "CLAUDE_CODE_NO_RESULT"
1011
+ );
1012
+ }
1013
+ return { kind: "completed" };
1014
+ } finally {
1015
+ signal.removeEventListener("abort", cancel);
1016
+ controller.abort();
1017
+ for (const line of diagnostics) this.ctx.logger.warn("%s", line);
1018
+ }
1019
+ }
1020
+ };
1021
+
1022
+ // src/driver-core/ownership.ts
1023
+ import { FiberState } from "@deepseek-ai/cordis";
1024
+ var INACTIVE_STATES = /* @__PURE__ */ new Set([
1025
+ FiberState.UNLOADING,
1026
+ FiberState.DISPOSED,
1027
+ FiberState.FAILED
1028
+ ]);
1029
+ var FactoryOwnership = class {
1030
+ constructor(fiber) {
1031
+ this.fiber = fiber;
1032
+ }
1033
+ fiber;
1034
+ accepting = true;
1035
+ teardown = new AbortController();
1036
+ inactive = Promise.withResolvers();
1037
+ liveAgents = /* @__PURE__ */ new Set();
1038
+ startupTasks = /* @__PURE__ */ new Set();
1039
+ /** Aborts (reason: `agent loop is not active` error) when factory teardown begins. */
1040
+ get signal() {
1041
+ return this.teardown.signal;
1042
+ }
1043
+ isActive() {
1044
+ return this.accepting && !INACTIVE_STATES.has(this.fiber.state);
1045
+ }
1046
+ /** Track one live agent's shared teardown until it has run. */
1047
+ track(dispose) {
1048
+ this.liveAgents.add(dispose);
1049
+ return () => {
1050
+ this.liveAgents.delete(dispose);
1051
+ };
1052
+ }
1053
+ /** Join config startup work that begins before an agent exists. */
1054
+ trackStartup(job) {
1055
+ this.startupTasks.add(job);
1056
+ const forget = () => {
1057
+ this.startupTasks.delete(job);
1058
+ };
1059
+ void job.then(forget, forget);
1060
+ }
1061
+ /** Join one public create/resume continuation; factory dispose awaits its settlement. */
1062
+ trackWrapper(job) {
1063
+ this.trackStartup(job.then(() => void 0, () => void 0));
1064
+ }
1065
+ async dispose() {
1066
+ this.accepting = false;
1067
+ this.teardown.abort(new Error("agent loop is not active"));
1068
+ this.inactive.resolve();
1069
+ await Promise.all([
1070
+ ...[...this.liveAgents].map((dispose) => dispose()),
1071
+ ...this.startupTasks
1072
+ ]);
1073
+ }
1074
+ };
1075
+ async function raceAbort(operation, signal, id) {
1076
+ const toAbortError = () => {
1077
+ return signal.reason instanceof Error ? signal.reason : new Error(`agent "${id}" creation aborted`, { cause: signal.reason });
1078
+ };
1079
+ if (signal.aborted) throw toAbortError();
1080
+ const aborted = Promise.withResolvers();
1081
+ const listener = () => {
1082
+ aborted.reject(toAbortError());
1083
+ };
1084
+ signal.addEventListener("abort", listener, { once: true });
1085
+ try {
1086
+ return await Promise.race([Promise.resolve(operation), aborted.promise]);
1087
+ } finally {
1088
+ signal.removeEventListener("abort", listener);
1089
+ }
1090
+ }
1091
+ async function raceAbortCall(operation, signal, id, releaseAbandoned) {
1092
+ if (signal.aborted) {
1093
+ throw signal.reason instanceof Error ? signal.reason : new Error(`agent "${id}" creation aborted`, { cause: signal.reason });
1094
+ }
1095
+ const pending = Promise.resolve().then(operation);
1096
+ try {
1097
+ return await raceAbort(pending, signal, id);
1098
+ } catch (error) {
1099
+ if (signal.aborted && releaseAbandoned !== void 0) {
1100
+ void pending.then(releaseAbandoned, () => void 0);
1101
+ }
1102
+ throw error;
1103
+ }
1104
+ }
1105
+
1106
+ // src/engine-claude/loop.ts
1107
+ var CLAUDE_CODE_PERMISSION_MODES = [
1108
+ "dontAsk",
1109
+ "acceptEdits",
1110
+ "auto",
1111
+ "plan",
1112
+ "bypassPermissions"
1113
+ ];
1114
+ var Config = z.object({
1115
+ permissionMode: z.union([...CLAUDE_CODE_PERMISSION_MODES]),
1116
+ env: z.dict(z.string()).default({}),
1117
+ model: z.string(),
1118
+ disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
1119
+ maxTurns: z.number().step(1).min(1)
1120
+ });
1121
+ function resolveConfig(config) {
1122
+ const disposeGraceMs = config.disposeGraceMs ?? DEFAULT_DISPOSE_GRACE_MS;
1123
+ if (!Number.isFinite(disposeGraceMs) || disposeGraceMs <= 0) {
1124
+ throw new Error("agent-loop-claude-code: disposeGraceMs must be a positive finite number");
1125
+ }
1126
+ if (disposeGraceMs > MAX_TIMER_DELAY_MS) {
1127
+ throw new Error(
1128
+ `agent-loop-claude-code: disposeGraceMs must be no greater than ${MAX_TIMER_DELAY_MS}`
1129
+ );
1130
+ }
1131
+ return {
1132
+ permissionMode: config.permissionMode,
1133
+ env: config.env ?? {},
1134
+ model: config.model,
1135
+ disposeGraceMs,
1136
+ maxTurns: config.maxTurns
1137
+ };
1138
+ }
1139
+ var ClaudeCodeLoop = class extends Service {
1140
+ /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
1141
+ static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
1142
+ /** Validated configuration owned by the loop plugin. */
1143
+ config;
1144
+ ownership;
1145
+ /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
1146
+ runtime;
1147
+ constructor(ctx, config) {
1148
+ super(ctx, "agentLoopClaudeCode");
1149
+ this.config = resolveConfig(config);
1150
+ this.ownership = new FactoryOwnership(ctx.fiber);
1151
+ this.runtime = { ctx };
1152
+ ctx.effect(() => () => this.ownership.dispose(), "agentLoopClaudeCode.transactions()");
1153
+ ctx.effect(() => ctx.agents.setFactory(this), "agentLoopClaudeCode.setFactory()");
1154
+ ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
1155
+ ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
1156
+ ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
1157
+ }
1158
+ /**
1159
+ * Construct the driver, scope, and one memoized reverse teardown for a new
1160
+ * agent. The teardown is registered with the factory and the owner fiber
1161
+ * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
1162
+ * fuses caller cancellation with lifecycle teardown for setup awaits.
1163
+ */
1164
+ /* jscpd:ignore-start -- ownership/transaction machinery mirrors the default agent-loop factory; depending on agent-loop is forbidden. */
1165
+ prepare(ownerCtx, id, options, session, callerSignal) {
1166
+ ownerCtx.fiber.assertActive();
1167
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
1168
+ if (callerSignal?.aborted) {
1169
+ throw callerSignal.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal.reason });
1170
+ }
1171
+ const loopCtx = this.runtime.ctx;
1172
+ const abort = new AbortController();
1173
+ const onCallerAbort = () => {
1174
+ abort.abort(callerSignal?.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal?.reason }));
1175
+ };
1176
+ const onFactoryTeardown = () => {
1177
+ abort.abort(this.ownership.signal.reason);
1178
+ };
1179
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
1180
+ this.ownership.signal.addEventListener("abort", onFactoryTeardown, { once: true });
1181
+ let machine;
1182
+ let detachSession;
1183
+ let detachAgent;
1184
+ let disposing;
1185
+ const machineReady = Promise.withResolvers();
1186
+ const dispose = (ownerTriggered = false) => disposing ??= (async () => {
1187
+ abort.abort(new Error(`agent "${id}" lifecycle disposed`));
1188
+ callerSignal?.removeEventListener("abort", onCallerAbort);
1189
+ this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
1190
+ try {
1191
+ if (machine === void 0) await machineReady.promise;
1192
+ if (machine !== void 0) {
1193
+ machine.cancel({ kind: "disposed" });
1194
+ await machine.whenIdle();
1195
+ await machine.scope.dispose();
1196
+ }
1197
+ } finally {
1198
+ try {
1199
+ detachAgent?.();
1200
+ detachSession?.();
1201
+ } finally {
1202
+ untrack();
1203
+ if (!ownerTriggered) await unfollowOwner();
1204
+ }
1205
+ }
1206
+ })();
1207
+ const untrack = this.ownership.track(dispose);
1208
+ let unfollowOwner;
1209
+ try {
1210
+ unfollowOwner = ownerCtx.effect(() => () => {
1211
+ if (disposing !== void 0) return;
1212
+ abort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
1213
+ return dispose(true);
1214
+ }, `agentLoopClaudeCode.lifecycle(${id})`);
1215
+ } catch (error) {
1216
+ untrack();
1217
+ callerSignal?.removeEventListener("abort", onCallerAbort);
1218
+ this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
1219
+ throw error;
1220
+ }
1221
+ const assertLive = () => {
1222
+ if (!abort.signal.aborted) return;
1223
+ throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
1224
+ };
1225
+ try {
1226
+ const agent = machine = new ClaudeCodeAgent(loopCtx, id, options, session, this.config);
1227
+ machineReady.resolve();
1228
+ assertLive();
1229
+ return {
1230
+ agent,
1231
+ signal: abort.signal,
1232
+ publish: (source) => {
1233
+ assertLive();
1234
+ detachSession = agent.ctx.sessions.enter(session);
1235
+ detachAgent = loopCtx.agents.enter(agent, ownerCtx.agent);
1236
+ agent.ctx.sessions.announce(session);
1237
+ assertLive();
1238
+ loopCtx.agents.announce(agent);
1239
+ assertLive();
1240
+ emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
1241
+ assertLive();
1242
+ return { agent, dispose };
1243
+ },
1244
+ dispose
1245
+ };
1246
+ } catch (error) {
1247
+ machineReady.resolve();
1248
+ void dispose();
1249
+ throw error;
1250
+ }
1251
+ }
1252
+ /** Prepare one Agent around an acquired Session, run setup, and publish it. */
1253
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
1254
+ var _stack = [];
1255
+ try {
1256
+ const ownedPreparation = __using(_stack, preparation);
1257
+ const session = ownedPreparation.session;
1258
+ const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
1259
+ try {
1260
+ const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
1261
+ setupCommit?.commit();
1262
+ return prepared.publish(source);
1263
+ } catch (error) {
1264
+ await prepared.dispose();
1265
+ throw error;
1266
+ }
1267
+ } catch (_) {
1268
+ var _error = _, _hasError = true;
1269
+ } finally {
1270
+ __callDispose(_stack, _error, _hasError);
1271
+ }
1272
+ }
1273
+ /**
1274
+ * Create an agent and session under one caller-supplied identity, owned by
1275
+ * the accessing fiber.
1276
+ * @param ownerCtx - caller context that structurally owns the lifecycle.
1277
+ * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
1278
+ * @returns the published handle.
1279
+ */
1280
+ async createAgent(ownerCtx, options) {
1281
+ const preparation = SessionPreparation.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
1282
+ ...options.seed === void 0 ? {} : { seed: options.seed },
1283
+ ...options.meta === void 0 ? {} : { meta: options.meta }
1284
+ }));
1285
+ const published = this.setupAndPublish(
1286
+ ownerCtx,
1287
+ options.sessionId,
1288
+ preparation,
1289
+ options.agentOptions ?? {},
1290
+ options.setup,
1291
+ options.signal,
1292
+ "startup"
1293
+ );
1294
+ this.ownership.trackWrapper(published);
1295
+ return published;
1296
+ }
1297
+ /**
1298
+ * Resume an owned agent from the configured persistence service.
1299
+ * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
1300
+ * @param options - persisted identity, loop options, setup, and cancellation.
1301
+ * @returns the published handle.
1302
+ */
1303
+ async resume(ownerCtx, options) {
1304
+ const persistence = this.runtime.ctx.get("sessionPersistence");
1305
+ if (persistence === void 0) {
1306
+ throw new Error("cannot resume: session persistence is not configured (load a dsh-session-persistence backend)");
1307
+ }
1308
+ return this.resumeWith(ownerCtx, persistence, options);
1309
+ }
1310
+ /** Resume through an explicit persistence handle. */
1311
+ async resumeWith(ownerCtx, persistence, options) {
1312
+ const id = options.resumeSessionId;
1313
+ let preparation;
1314
+ try {
1315
+ const ownerAbort = new AbortController();
1316
+ const unfollowOwner = ownerCtx.effect(() => () => {
1317
+ ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
1318
+ }, `agentLoopClaudeCode.resume-load(${id})`);
1319
+ const fused = AbortSignal.any([
1320
+ ...options.signal === void 0 ? [] : [options.signal],
1321
+ ownerAbort.signal,
1322
+ this.ownership.signal
1323
+ ]);
1324
+ try {
1325
+ preparation = await raceAbortCall(
1326
+ () => persistence.prepare(id, fused),
1327
+ fused,
1328
+ id,
1329
+ (abandoned) => {
1330
+ abandoned[Symbol.dispose]();
1331
+ }
1332
+ );
1333
+ } finally {
1334
+ await unfollowOwner();
1335
+ }
1336
+ ownerCtx.fiber.assertActive();
1337
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
1338
+ return await this.setupAndPublish(
1339
+ ownerCtx,
1340
+ id,
1341
+ preparation,
1342
+ options.agentOptions ?? {},
1343
+ options.setup,
1344
+ options.signal,
1345
+ "resume"
1346
+ );
1347
+ } finally {
1348
+ preparation?.[Symbol.dispose]();
1349
+ }
1350
+ }
1351
+ };
1352
+
1353
+ // src/engine-codex/loop.ts
1354
+ import { Service as Service2 } from "@deepseek-ai/cordis";
1355
+ import z2 from "@deepseek-ai/schemastery";
1356
+ import { emitAgentEvent as emitAgentEvent2 } from "@deepseek-ai/dsh-agent";
1357
+ import { SessionPreparation as SessionPreparation2 } from "@deepseek-ai/dsh-session";
1358
+
1359
+ // src/engine-codex/agent.ts
1360
+ import { Inbox as Inbox2, agentEvents as agentEvents2 } from "@deepseek-ai/dsh-agent";
1361
+ import { LlmError as LlmError2, createAssistantMessage as createAssistantMessage2, createUserMessage as createUserMessage2, errorChain as errorChain2 } from "@deepseek-ai/dsh-llm";
1362
+ import { createScope as createScope2 } from "@deepseek-ai/dsh-scope";
1363
+ import { canonicalHeader as canonicalHeader2 } from "@deepseek-ai/dsh-session";
1364
+
1365
+ // src/engine-codex/permission.ts
1366
+ var DEFAULT_CODEX_PERMISSION = {
1367
+ sandboxMode: "read-only",
1368
+ approvalPolicy: "never"
1369
+ };
1370
+ function resolveSessionPermission2(events) {
1371
+ if (sessionSandboxMode(events) === "danger-full-access") {
1372
+ return { sandboxMode: "danger-full-access", approvalPolicy: "never" };
1373
+ }
1374
+ if (sessionApprovalPolicy(events) === "ask") {
1375
+ return { sandboxMode: "workspace-write", approvalPolicy: "on-request" };
1376
+ }
1377
+ return DEFAULT_CODEX_PERMISSION;
1378
+ }
1379
+
1380
+ // src/engine-codex/appserver/client.ts
1381
+ import { spawn } from "node:child_process";
1382
+ import { createRequire } from "node:module";
1383
+ import { dirname, join } from "node:path";
1384
+ import { createInterface } from "node:readline";
1385
+ var require2 = createRequire(import.meta.url);
1386
+ function codexCliEntrypoint() {
1387
+ return join(dirname(require2.resolve("@openai/codex/package.json")), "bin", "codex.js");
1388
+ }
1389
+ var AppServerClient = class _AppServerClient {
1390
+ process;
1391
+ rl;
1392
+ reqId = 1;
1393
+ pending = /* @__PURE__ */ new Map();
1394
+ notificationHandler;
1395
+ stderrHandler;
1396
+ disposed = false;
1397
+ /** Whether this client was disposed or its server process exited. */
1398
+ get closed() {
1399
+ return this.disposed;
1400
+ }
1401
+ /** Create a client by spawning `codex app-server`. */
1402
+ constructor(process2) {
1403
+ this.process = process2;
1404
+ this.rl = createInterface({ input: process2.stdout });
1405
+ this.rl.on("line", (line) => this.handleLine(line));
1406
+ process2.stderr.on("data", (chunk) => {
1407
+ const lines = chunk.toString().split("\n").filter(Boolean);
1408
+ for (const line of lines) {
1409
+ this.stderrHandler?.(line);
1410
+ }
1411
+ });
1412
+ process2.on("exit", () => {
1413
+ this.disposed = true;
1414
+ const err = new Error("codex app-server process exited unexpectedly");
1415
+ for (const { reject } of this.pending.values()) {
1416
+ reject(err);
1417
+ }
1418
+ this.pending.clear();
1419
+ });
1420
+ }
1421
+ /** Spawn the pinned app-server dependency and initialize the client. */
1422
+ static async create() {
1423
+ const proc = spawn(process.execPath, [codexCliEntrypoint(), "app-server"], {
1424
+ stdio: ["pipe", "pipe", "pipe"]
1425
+ });
1426
+ const client = new _AppServerClient(proc);
1427
+ await client.initialize();
1428
+ return client;
1429
+ }
1430
+ /** Set the notification handler for streaming events. */
1431
+ onNotification(handler) {
1432
+ this.notificationHandler = handler;
1433
+ }
1434
+ /** Set the stderr handler for server log lines. */
1435
+ onStderr(handler) {
1436
+ this.stderrHandler = handler;
1437
+ }
1438
+ /** Send the initialize handshake. */
1439
+ async initialize() {
1440
+ const params = {
1441
+ clientInfo: {
1442
+ name: "dsh-loop-engine",
1443
+ title: null,
1444
+ version: "0.1.1-rc.2"
1445
+ },
1446
+ capabilities: { experimentalApi: true, requestAttestation: false }
1447
+ };
1448
+ return this.request("initialize", params);
1449
+ }
1450
+ /** Create a new thread. */
1451
+ async threadStart(params) {
1452
+ return this.request("thread/start", params);
1453
+ }
1454
+ /** Resume an existing thread. */
1455
+ async threadResume(params) {
1456
+ return this.request("thread/resume", params);
1457
+ }
1458
+ /** Start a turn with the given input. */
1459
+ async turnStart(params) {
1460
+ return this.request("turn/start", params);
1461
+ }
1462
+ /** Interrupt an active turn. */
1463
+ async turnInterrupt(params) {
1464
+ return this.request("turn/interrupt", params);
1465
+ }
1466
+ /** Dispose the client and kill the server process. */
1467
+ dispose() {
1468
+ if (this.disposed) return;
1469
+ this.disposed = true;
1470
+ this.rl.close();
1471
+ this.process.stdin?.end();
1472
+ this.process.kill();
1473
+ }
1474
+ /** Send a JSON-RPC request and wait for the response. */
1475
+ request(method, params) {
1476
+ if (this.disposed) {
1477
+ return Promise.reject(new Error("app-server client is disposed"));
1478
+ }
1479
+ const id = this.reqId++;
1480
+ const msg = { jsonrpc: "2.0", id, method, params };
1481
+ return new Promise((resolve4, reject) => {
1482
+ this.pending.set(id, { resolve: resolve4, reject });
1483
+ this.process.stdin.write(JSON.stringify(msg) + "\n");
1484
+ });
1485
+ }
1486
+ /** Handle one line of stdout from the server. */
1487
+ handleLine(line) {
1488
+ if (!line.trim()) return;
1489
+ let obj;
1490
+ try {
1491
+ obj = JSON.parse(line);
1492
+ } catch {
1493
+ return;
1494
+ }
1495
+ if (obj.id !== void 0) {
1496
+ const pending = this.pending.get(obj.id);
1497
+ if (pending) {
1498
+ this.pending.delete(obj.id);
1499
+ if (obj.error) {
1500
+ pending.reject(new Error(obj.error.message));
1501
+ } else {
1502
+ pending.resolve(obj.result);
1503
+ }
1504
+ }
1505
+ }
1506
+ if (obj.method !== void 0) {
1507
+ this.notificationHandler?.(obj.method, obj.params);
1508
+ }
1509
+ }
1510
+ };
1511
+
1512
+ // src/engine-codex/appserver/thread.ts
1513
+ var AppServerThread = class _AppServerThread {
1514
+ constructor(client, threadId) {
1515
+ this.client = client;
1516
+ this.threadId = threadId;
1517
+ }
1518
+ client;
1519
+ threadId;
1520
+ /** Create a new thread on the app-server. */
1521
+ static async create(client, params) {
1522
+ const result = await client.threadStart(params);
1523
+ return new _AppServerThread(client, result.thread.id);
1524
+ }
1525
+ /**
1526
+ * Start a turn and stream its events as an async generator.
1527
+ * The generator ends when the turn completes or an error occurs.
1528
+ */
1529
+ async *turn(input, options) {
1530
+ const { signal, params } = options;
1531
+ const queue = [];
1532
+ const earlyNotifications = [];
1533
+ let resolve4;
1534
+ let done = false;
1535
+ let turnError;
1536
+ let turnId;
1537
+ const notificationHandler = (method, rawParams) => {
1538
+ if (done) return;
1539
+ const params2 = rawParams;
1540
+ if (params2.threadId !== this.threadId) return;
1541
+ if (turnId === void 0) {
1542
+ earlyNotifications.push([method, rawParams]);
1543
+ return;
1544
+ }
1545
+ let event;
1546
+ switch (method) {
1547
+ case "item/started": {
1548
+ const p = params2;
1549
+ if (p.turnId !== turnId) return;
1550
+ event = { kind: "item-started", itemType: p.item.type, itemId: p.item.id };
1551
+ break;
1552
+ }
1553
+ case "item/agentMessage/delta": {
1554
+ const p = params2;
1555
+ if (p.turnId !== turnId) return;
1556
+ event = { kind: "agent-delta", itemId: p.itemId, delta: p.delta };
1557
+ break;
1558
+ }
1559
+ case "item/reasoning/summaryTextDelta": {
1560
+ const p = params2;
1561
+ if (p.turnId !== turnId) return;
1562
+ event = { kind: "reasoning-summary-delta", itemId: p.itemId, delta: p.delta, summaryIndex: p.summaryIndex };
1563
+ break;
1564
+ }
1565
+ case "item/reasoning/textDelta": {
1566
+ const p = params2;
1567
+ if (p.turnId !== turnId) return;
1568
+ event = { kind: "reasoning-text-delta", itemId: p.itemId, delta: p.delta, contentIndex: p.contentIndex };
1569
+ break;
1570
+ }
1571
+ case "item/plan/delta": {
1572
+ const p = params2;
1573
+ if (p.turnId !== turnId) return;
1574
+ event = { kind: "plan-delta", itemId: p.itemId, delta: p.delta };
1575
+ break;
1576
+ }
1577
+ case "item/completed": {
1578
+ const p = params2;
1579
+ if (p.turnId !== turnId) return;
1580
+ event = { kind: "item-completed", item: p.item };
1581
+ break;
1582
+ }
1583
+ case "turn/completed": {
1584
+ const p = params2;
1585
+ if (p.threadId !== this.threadId) return;
1586
+ event = { kind: "turn-completed", turn: p.turn };
1587
+ done = true;
1588
+ break;
1589
+ }
1590
+ case "thread/tokenUsage/updated": {
1591
+ const p = params2;
1592
+ if (p.turnId !== turnId) return;
1593
+ event = { kind: "token-usage", usage: p.tokenUsage };
1594
+ break;
1595
+ }
1596
+ case "error": {
1597
+ const p = params2;
1598
+ if (p.turnId !== turnId) return;
1599
+ event = { kind: "error", error: p.error, willRetry: p.willRetry };
1600
+ done = true;
1601
+ turnError = new Error(p.error.message);
1602
+ break;
1603
+ }
1604
+ }
1605
+ if (event) {
1606
+ queue.push(event);
1607
+ resolve4?.();
1608
+ }
1609
+ };
1610
+ this.client.onNotification(notificationHandler);
1611
+ let turnResult;
1612
+ try {
1613
+ turnResult = await this.client.turnStart({
1614
+ threadId: this.threadId,
1615
+ input,
1616
+ ...params
1617
+ });
1618
+ } catch (error) {
1619
+ this.client.onNotification(noopNotificationHandler);
1620
+ throw error;
1621
+ }
1622
+ turnId = turnResult.turn.id;
1623
+ for (const [method, rawParams] of earlyNotifications) notificationHandler(method, rawParams);
1624
+ earlyNotifications.length = 0;
1625
+ yield { kind: "turn-started", turnId };
1626
+ const abortHandler = () => {
1627
+ if (!done) {
1628
+ done = true;
1629
+ turnError = signal?.reason instanceof Error ? signal.reason : new Error("turn aborted");
1630
+ void this.client.turnInterrupt({ threadId: this.threadId, turnId }).catch(() => {
1631
+ });
1632
+ }
1633
+ resolve4?.();
1634
+ };
1635
+ signal?.addEventListener("abort", abortHandler, { once: true });
1636
+ try {
1637
+ while (true) {
1638
+ if (done && queue.length === 0) break;
1639
+ if (queue.length > 0) {
1640
+ yield queue.shift();
1641
+ } else {
1642
+ await new Promise((r) => {
1643
+ resolve4 = r;
1644
+ });
1645
+ resolve4 = void 0;
1646
+ }
1647
+ }
1648
+ if (turnError) throw turnError;
1649
+ } finally {
1650
+ signal?.removeEventListener("abort", abortHandler);
1651
+ this.client.onNotification(noopNotificationHandler);
1652
+ }
1653
+ }
1654
+ };
1655
+ function noopNotificationHandler() {
1656
+ }
1657
+
1658
+ // src/engine-codex/appserver/mapping.ts
1659
+ import { CallId as CallId2, createToolResultMessage as createToolResultMessage2 } from "@deepseek-ai/dsh-llm";
1660
+ function mapUsage2(usage) {
1661
+ return {
1662
+ inputTokens: usage.inputTokens,
1663
+ outputTokens: usage.outputTokens,
1664
+ ...usage.cachedInputTokens !== void 0 ? { cacheReadTokens: usage.cachedInputTokens } : {},
1665
+ ...usage.reasoningOutputTokens !== void 0 ? { reasoningTokens: usage.reasoningOutputTokens } : {}
1666
+ };
1667
+ }
1668
+ function mapCommandExecution(item) {
1669
+ return {
1670
+ call: {
1671
+ callId: CallId2(item.id),
1672
+ name: "command_execution",
1673
+ arguments: JSON.stringify({ command: item.command ?? "" })
1674
+ },
1675
+ result: createToolResultMessage2({
1676
+ callId: CallId2(item.id),
1677
+ content: [{ type: "text", text: item.aggregatedOutput ?? "" }],
1678
+ isError: (item.exitCode ?? 0) !== 0 || item.status === "failed"
1679
+ })
1680
+ };
1681
+ }
1682
+ function mapFileChange(item) {
1683
+ return {
1684
+ call: {
1685
+ callId: CallId2(item.id),
1686
+ name: "apply_patch",
1687
+ arguments: JSON.stringify(item.changes ?? [])
1688
+ },
1689
+ result: createToolResultMessage2({
1690
+ callId: CallId2(item.id),
1691
+ content: [{ type: "text", text: `patch ${item.status ?? "completed"}` }],
1692
+ isError: item.status === "failed"
1693
+ })
1694
+ };
1695
+ }
1696
+ function mapMcpToolCall(item) {
1697
+ const name2 = item.server !== void 0 && item.tool !== void 0 ? `${item.server}/${item.tool}` : "mcp_tool_call";
1698
+ const isError = item.error !== void 0 && item.error !== null;
1699
+ return {
1700
+ call: {
1701
+ callId: CallId2(item.id),
1702
+ name: name2,
1703
+ arguments: JSON.stringify(item.arguments ?? {})
1704
+ },
1705
+ result: createToolResultMessage2({
1706
+ callId: CallId2(item.id),
1707
+ content: isError ? [{ type: "text", text: item.error?.message ?? "tool call failed" }] : [{ type: "text", text: JSON.stringify(item.result?.content ?? []) }],
1708
+ isError
1709
+ })
1710
+ };
1711
+ }
1712
+
1713
+ // src/engine-codex/agent.ts
1714
+ var PROVIDER2 = "codex";
1715
+ var NATIVE_MODEL_LABEL2 = "codex-native";
1716
+ var CodexAgent = class {
1717
+ constructor(loopCtx, id, options, session, config) {
1718
+ this.loopCtx = loopCtx;
1719
+ this.id = id;
1720
+ this.options = options;
1721
+ this.session = session;
1722
+ this.config = config;
1723
+ this.dispatch = agentEvents2(loopCtx, this);
1724
+ this.inbox = new Inbox2(session, {
1725
+ inserted: (message) => {
1726
+ this.dispatch.emit("agent/inbox/inserted", { message });
1727
+ },
1728
+ discarded: (message) => {
1729
+ this.dispatch.emit("agent/inbox/discarded", { message });
1730
+ },
1731
+ claimed: (message, turn) => {
1732
+ this.dispatch.emit("agent/inbox/claimed", { message, turn });
1733
+ }
1734
+ });
1735
+ const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
1736
+ this.phase = { kind: "idle", lastTurn };
1737
+ this.scope = createScope2(loopCtx, this);
1738
+ this.ctx = this.scope.ctx.extend({ agent: this });
1739
+ this.scope.ctx.effect(() => () => {
1740
+ this.appServer?.dispose();
1741
+ this.appServer = void 0;
1742
+ }, "codex.appServerClient()");
1743
+ }
1744
+ loopCtx;
1745
+ id;
1746
+ options;
1747
+ session;
1748
+ config;
1749
+ inbox;
1750
+ phase;
1751
+ activityDone = Promise.resolve();
1752
+ /** The agent-scoped registration boundary; the lifecycle owner unwinds it after the driver exits. */
1753
+ scope;
1754
+ ctx;
1755
+ /** Fused dispatcher, built once in the constructor so hot-path dispatches never allocate. */
1756
+ dispatch;
1757
+ /** Whether this loop instance has appended its initial/resume request anchor. */
1758
+ requestHeaderLogged = false;
1759
+ /** Lazily created app-server client, reused across steps and released on scope teardown. */
1760
+ appServer;
1761
+ /** Return the cached app-server client, spawning one on first use or after a dead process. */
1762
+ async appServerClient() {
1763
+ if (this.appServer !== void 0 && !this.appServer.closed) return this.appServer;
1764
+ this.appServer = await AppServerClient.create();
1765
+ return this.appServer;
1766
+ }
1767
+ get status() {
1768
+ return this.phase.kind === "idle" || this.phase.kind === "maintenance" ? "idle" : "running";
1769
+ }
1770
+ /** Commit a phase and publish its externally visible status transition. */
1771
+ setPhase(next) {
1772
+ const previousStatus = this.status;
1773
+ this.phase = next;
1774
+ const status = this.status;
1775
+ if (status !== previousStatus) {
1776
+ this.dispatch.emit("agent/status", { status });
1777
+ }
1778
+ }
1779
+ send(message, target, wakeup) {
1780
+ const wakingAfterAbort = wakeup && this.phase.kind !== "idle" && this.phase.abort.signal.aborted;
1781
+ const resolvedTarget = wakingAfterAbort ? "next-turn" : target;
1782
+ this.inbox.splice(resolvedTarget, Infinity, 0, [message]);
1783
+ if (wakeup) this.wakeDriver(wakingAfterAbort);
1784
+ }
1785
+ /**
1786
+ * Queue a message for the next turn and wake the driver.
1787
+ * @param input - the user message to deliver.
1788
+ */
1789
+ followup(input) {
1790
+ this.send(input, "next-turn", true);
1791
+ }
1792
+ /**
1793
+ * Queue a message for the running step and wake the driver.
1794
+ * @param input - the user message to deliver.
1795
+ */
1796
+ steer(input) {
1797
+ this.send(input, "next-step", true);
1798
+ }
1799
+ /**
1800
+ * Queue a message for the running step without waking the driver.
1801
+ * @param input - the user message to deliver.
1802
+ */
1803
+ inject(input) {
1804
+ this.send(input, "next-step", false);
1805
+ }
1806
+ cancel(cause, options = {}) {
1807
+ if (!options.keepInbox) {
1808
+ this.inbox.clear();
1809
+ if (this.phase.kind !== "idle") this.phase.wakeRequested = false;
1810
+ }
1811
+ if (this.phase.kind !== "idle") this.phase.abort.abort(cause);
1812
+ }
1813
+ /**
1814
+ * Run a maintenance job while the agent is idle.
1815
+ * @param job - the maintenance operation, receiving the phase abort signal.
1816
+ * @returns the maintenance result.
1817
+ */
1818
+ runMaintenance(job) {
1819
+ if (this.phase.kind !== "idle") throw new Error(`agent "${this.id}" already has active work`);
1820
+ const done = Promise.withResolvers();
1821
+ const maintenance = {
1822
+ kind: "maintenance",
1823
+ abort: new AbortController(),
1824
+ lastTurn: this.phase.lastTurn,
1825
+ wakeRequested: false
1826
+ };
1827
+ this.setPhase(maintenance);
1828
+ this.activityDone = done.promise;
1829
+ return (async () => {
1830
+ try {
1831
+ return await job(maintenance.abort.signal);
1832
+ } finally {
1833
+ this.setPhase({ kind: "idle", lastTurn: maintenance.lastTurn });
1834
+ if (maintenance.wakeRequested && this.inbox.hasPending) this.wakeDriver();
1835
+ done.resolve();
1836
+ }
1837
+ })();
1838
+ }
1839
+ /**
1840
+ * Start one driver, or latch its wake behind maintenance or an aborted
1841
+ * activity. A wake sent while idle always opens its turn boundary, even
1842
+ * when its message was cleared; only a latched replay is suppressed when
1843
+ * the queue no longer holds the wake.
1844
+ * @param wakeAfterAbort - the {@link send} classification, captured before
1845
+ * the inbox insertion so a reentrant cancel cannot reclassify it.
1846
+ */
1847
+ wakeDriver(wakeAfterAbort = false) {
1848
+ if (this.phase.kind !== "idle") {
1849
+ const reason = this.phase.abort.signal.reason;
1850
+ if (reason?.kind !== "disposed" && (this.phase.kind === "maintenance" || wakeAfterAbort)) {
1851
+ this.phase.wakeRequested = true;
1852
+ }
1853
+ return;
1854
+ }
1855
+ const driver = Promise.withResolvers();
1856
+ this.activityDone = driver.promise;
1857
+ this.setPhase({
1858
+ kind: "running",
1859
+ abort: new AbortController(),
1860
+ turn: this.phase.lastTurn,
1861
+ step: 0,
1862
+ wakeRequested: false
1863
+ });
1864
+ this.loopCtx.agents.withInitiator(this, () => this.kick()).then(driver.resolve, driver.reject);
1865
+ }
1866
+ async whenIdle() {
1867
+ let activity;
1868
+ do {
1869
+ await (activity = this.activityDone);
1870
+ } while (activity !== this.activityDone);
1871
+ }
1872
+ /** Report one failure at its live boundary, then preserve it for driver containment. */
1873
+ throwError(error) {
1874
+ const turn = this.phase.kind === "running" ? this.phase.turn : this.phase.lastTurn;
1875
+ const step = this.phase.kind === "running" ? this.phase.step : 0;
1876
+ this.dispatch.emit("agent/error", { turn, step, error });
1877
+ throw error;
1878
+ }
1879
+ async kick() {
1880
+ try {
1881
+ while (await this.turn()) {
1882
+ }
1883
+ } catch (_error) {
1884
+ } finally {
1885
+ if (this.phase.kind === "running") {
1886
+ const { turn, wakeRequested } = this.phase;
1887
+ this.setPhase({ kind: "idle", lastTurn: turn });
1888
+ if (wakeRequested && this.inbox.hasPending) this.wakeDriver();
1889
+ }
1890
+ }
1891
+ }
1892
+ async preStep(target, position) {
1893
+ if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": pre-step outside running phase`);
1894
+ const signal = this.phase.abort.signal;
1895
+ const claimed = this.inbox.claim(target, position.turn);
1896
+ const decision = await this.dispatch.waterfall(
1897
+ "agent/pre-step",
1898
+ { messages: claimed, ...position, signal },
1899
+ () => Promise.resolve({ kind: "enter", messages: claimed })
1900
+ );
1901
+ signal.throwIfAborted();
1902
+ if (decision.kind === "reject") return decision;
1903
+ const injected = await this.injectSkills(decision.messages, signal);
1904
+ signal.throwIfAborted();
1905
+ return injected !== decision.messages ? { kind: "enter", messages: [...injected] } : { ...decision };
1906
+ }
1907
+ /**
1908
+ * Scan the step's user messages for `/name` skill gestures, load each
1909
+ * matching skill, and inject the rendered skill content into the message
1910
+ * batch. This mirrors what dsh-tool-skill does for the in-process engine.
1911
+ * @param messages - the current step's message batch.
1912
+ * @param signal - cancellation signal (aborted loads are silently dropped).
1913
+ * @returns the original batch when no skill was invoked, or an extended
1914
+ * batch with injected skill-content messages appended.
1915
+ */
1916
+ async injectSkills(messages, signal) {
1917
+ const names = invokedSkillNames(messages);
1918
+ if (names.length === 0) return messages;
1919
+ const skills = this.loopCtx.get("skills");
1920
+ if (skills === void 0) return messages;
1921
+ const cwd = this.session.header.cwd;
1922
+ const injections = [];
1923
+ for (const name2 of names) {
1924
+ if (!isSkillName(name2)) continue;
1925
+ let skill;
1926
+ try {
1927
+ skill = await skills.get(name2, { signal, scope: this, ...cwd === void 0 ? {} : { cwd } });
1928
+ } catch {
1929
+ continue;
1930
+ }
1931
+ if (skill === void 0 || !skill.invocation.userInvocable) continue;
1932
+ if (signal.aborted) return messages;
1933
+ injections.push(createUserMessage2({
1934
+ content: [{ type: "text", text: renderSkillContent(skill) }],
1935
+ source: { kind: "skill-invocation", name: name2, form: "instructions" }
1936
+ }));
1937
+ }
1938
+ return injections.length > 0 ? [...messages, ...injections] : messages;
1939
+ }
1940
+ /**
1941
+ * Resolve the declarative permission stance for one query. Deployment-pinned
1942
+ * fields win per field; anything unpinned follows the session's durable dsh
1943
+ * permission knobs, re-folded per query so mid-session preset switches take
1944
+ * effect on the next step.
1945
+ * @returns the permission fields of the query spec.
1946
+ */
1947
+ queryPermission() {
1948
+ const fold = resolveSessionPermission2(this.session.events);
1949
+ return {
1950
+ sandboxMode: this.config.sandboxMode ?? fold.sandboxMode,
1951
+ approvalPolicy: this.config.approvalPolicy ?? fold.approvalPolicy
1952
+ };
1953
+ }
1954
+ /** Open one turn before claiming its first proposed step. */
1955
+ async turn() {
1956
+ if (this.phase.kind !== "running") {
1957
+ this.throwError(new Error(`agent "${this.id}": turn without driver reservation`));
1958
+ }
1959
+ const phase = this.phase;
1960
+ const { signal } = phase.abort;
1961
+ signal.throwIfAborted();
1962
+ const turn = phase.turn + 1;
1963
+ try {
1964
+ this.session.append("turn/start", { turn });
1965
+ } catch (error) {
1966
+ this.throwError(error);
1967
+ }
1968
+ phase.turn = turn;
1969
+ let turnEnds = null;
1970
+ let target = "next-turn";
1971
+ try {
1972
+ while (true) {
1973
+ signal.throwIfAborted();
1974
+ const step = phase.step + 1;
1975
+ const decision = await this.preStep(target, { turn, step });
1976
+ if (decision.kind === "reject") {
1977
+ turnEnds = { kind: "blocked" };
1978
+ return false;
1979
+ }
1980
+ if (turnEnds && decision.messages.length === 0) break;
1981
+ if (phase.step === 0 && decision.messages.length === 0) {
1982
+ turnEnds = { kind: "completed" };
1983
+ return false;
1984
+ }
1985
+ signal.throwIfAborted();
1986
+ this.session.append("step/start", { turn, step });
1987
+ phase.step = step;
1988
+ try {
1989
+ for (const message of decision.messages) {
1990
+ this.session.append("user/message", message, { surfaceOp: "append" });
1991
+ }
1992
+ const stepEnd = await this.step();
1993
+ if (turnEnds === null) turnEnds = stepEnd;
1994
+ } finally {
1995
+ this.session.append("step/end", { turn, step });
1996
+ }
1997
+ signal.throwIfAborted();
1998
+ if (turnEnds && this.inbox.nextStep.length === 0) {
1999
+ await this.dispatch.serial("agent/turn-stopping", { turn, signal });
2000
+ signal.throwIfAborted();
2001
+ }
2002
+ if (turnEnds && this.inbox.nextStep.length === 0) break;
2003
+ target = "next-step";
2004
+ }
2005
+ } catch (error) {
2006
+ if (signal.aborted) {
2007
+ turnEnds = { kind: "aborted", reason: signal.reason };
2008
+ throw error;
2009
+ }
2010
+ turnEnds = {
2011
+ kind: "error",
2012
+ error: error instanceof LlmError2 ? error.failure : { message: errorChain2(error), code: "UNKNOWN" }
2013
+ };
2014
+ this.throwError(error);
2015
+ } finally {
2016
+ try {
2017
+ this.session.append("turn/end", { turn, reason: turnEnds });
2018
+ } catch (error) {
2019
+ this.throwError(error);
2020
+ }
2021
+ }
2022
+ if (!this.inbox.hasPending) return false;
2023
+ phase.abort = new AbortController();
2024
+ phase.wakeRequested = false;
2025
+ phase.step = 0;
2026
+ return true;
2027
+ }
2028
+ /** Model label recorded in the request header for one lifecycle. */
2029
+ modelLabel() {
2030
+ return this.config.model ?? NATIVE_MODEL_LABEL2;
2031
+ }
2032
+ /** Append the request header snapshot once per loop instance. */
2033
+ assertRequestHeader() {
2034
+ if (this.requestHeaderLogged) return;
2035
+ const header = canonicalHeader2({
2036
+ config: { provider: PROVIDER2, model: this.modelLabel() }
2037
+ });
2038
+ const baseline = this.session.requestHeader();
2039
+ this.session.append("request/header", {
2040
+ header,
2041
+ reason: baseline === void 0 ? "initial" : "resume"
2042
+ });
2043
+ this.requestHeaderLogged = true;
2044
+ }
2045
+ /** Run one Codex thread for the current step and map its transcript into the session log. */
2046
+ async step() {
2047
+ if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
2048
+ const { turn, step, abort: { signal } } = this.phase;
2049
+ signal.throwIfAborted();
2050
+ const cwd = this.session.header.cwd;
2051
+ if (cwd === void 0 || cwd.length === 0) {
2052
+ throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
2053
+ }
2054
+ const history = this.session.deriveMessages();
2055
+ const prompt = serializeHistory(history);
2056
+ if (prompt.length === 0) {
2057
+ throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
2058
+ }
2059
+ this.assertRequestHeader();
2060
+ signal.throwIfAborted();
2061
+ const controller = new AbortController();
2062
+ const cancel = () => {
2063
+ if (!controller.signal.aborted) {
2064
+ controller.abort(signal.reason instanceof Error ? signal.reason : new Error(`agent "${this.id}" query aborted`));
2065
+ }
2066
+ };
2067
+ signal.addEventListener("abort", cancel, { once: true });
2068
+ try {
2069
+ const permission = this.queryPermission();
2070
+ const client = await this.appServerClient();
2071
+ const threadParams = {
2072
+ cwd,
2073
+ sandbox: permission.sandboxMode,
2074
+ approvalPolicy: permission.approvalPolicy,
2075
+ ...this.config.model === void 0 ? {} : { model: this.config.model }
2076
+ };
2077
+ const thread = await AppServerThread.create(client, threadParams);
2078
+ const input = [{ type: "text", text: prompt }];
2079
+ const events = thread.turn(input, {
2080
+ signal: controller.signal,
2081
+ params: {
2082
+ approvalPolicy: permission.approvalPolicy,
2083
+ ...this.config.model === void 0 ? {} : { model: this.config.model }
2084
+ }
2085
+ });
2086
+ let finished = false;
2087
+ const pendingReasoning = [];
2088
+ const pendingReasoningSeqs = [];
2089
+ const textSeqs = [];
2090
+ let held;
2091
+ let reasoningBlockStarted = false;
2092
+ let textBlockStarted = false;
2093
+ let textBlockIndex = 0;
2094
+ const emitChunk = (chunk) => this.session.append("assistant/chunk", { turn, step, chunk }).seq;
2095
+ const flushHeld = (usage) => {
2096
+ if (held === void 0) return;
2097
+ this.session.append("assistant/message", {
2098
+ turn,
2099
+ step,
2100
+ message: createAssistantMessage2({
2101
+ content: held.content,
2102
+ source: { provider: PROVIDER2, model: this.modelLabel() }
2103
+ }),
2104
+ ...usage === void 0 ? {} : { usage }
2105
+ }, {
2106
+ surfaceOp: "append",
2107
+ // Link the durable message to the chunks that streamed it, so replay
2108
+ // can reconstruct the partial exactly as shown.
2109
+ sourceEventSeqs: held.refs
2110
+ });
2111
+ held = void 0;
2112
+ };
2113
+ const flushReasoning = (usage) => {
2114
+ if (pendingReasoning.length === 0) return;
2115
+ flushHeld();
2116
+ held = {
2117
+ content: pendingReasoning.map((text) => ({ type: "reasoning", text })),
2118
+ refs: [...pendingReasoningSeqs]
2119
+ };
2120
+ pendingReasoning.length = 0;
2121
+ pendingReasoningSeqs.length = 0;
2122
+ flushHeld(usage);
2123
+ };
2124
+ signal.throwIfAborted();
2125
+ for await (const event of events) {
2126
+ signal.throwIfAborted();
2127
+ switch (event.kind) {
2128
+ case "turn-started":
2129
+ break;
2130
+ case "item-started": {
2131
+ if (event.itemType === "agentMessage") {
2132
+ textBlockStarted = false;
2133
+ textBlockIndex = pendingReasoning.length;
2134
+ }
2135
+ break;
2136
+ }
2137
+ case "agent-delta": {
2138
+ if (!textBlockStarted) {
2139
+ textBlockStarted = true;
2140
+ textSeqs.push(emitChunk({ type: "block-start", index: textBlockIndex, blockType: "text" }));
2141
+ }
2142
+ textSeqs.push(emitChunk({ type: "text-delta", index: textBlockIndex, text: event.delta }));
2143
+ break;
2144
+ }
2145
+ case "reasoning-summary-delta":
2146
+ case "reasoning-text-delta":
2147
+ case "plan-delta": {
2148
+ const index = pendingReasoning.length;
2149
+ if (!reasoningBlockStarted) {
2150
+ reasoningBlockStarted = true;
2151
+ pendingReasoningSeqs.push(emitChunk({ type: "block-start", index, blockType: "reasoning" }));
2152
+ }
2153
+ pendingReasoningSeqs.push(emitChunk({ type: "reasoning-delta", index, text: event.delta }));
2154
+ break;
2155
+ }
2156
+ case "item-completed": {
2157
+ const item = event.item;
2158
+ if (item.type === "reasoning") {
2159
+ const summary = item.summary;
2160
+ const content = item.content;
2161
+ const text = summary?.join("\n") ?? content?.join("\n") ?? "";
2162
+ pendingReasoning.push(text);
2163
+ reasoningBlockStarted = false;
2164
+ } else if (item.type === "agentMessage") {
2165
+ flushHeld();
2166
+ held = {
2167
+ content: [
2168
+ ...pendingReasoning.map((text) => ({ type: "reasoning", text })),
2169
+ { type: "text", text: item.text ?? "" }
2170
+ ],
2171
+ refs: [...pendingReasoningSeqs, ...textSeqs]
2172
+ };
2173
+ pendingReasoning.length = 0;
2174
+ pendingReasoningSeqs.length = 0;
2175
+ textSeqs.length = 0;
2176
+ reasoningBlockStarted = false;
2177
+ textBlockStarted = false;
2178
+ } else if (item.type === "commandExecution") {
2179
+ flushReasoning();
2180
+ flushHeld();
2181
+ const activity = mapCommandExecution(item);
2182
+ this.session.append("tool/call", {
2183
+ turn,
2184
+ step,
2185
+ callId: activity.call.callId,
2186
+ name: activity.call.name,
2187
+ arguments: activity.call.arguments
2188
+ });
2189
+ this.session.append("tool/result", { turn, step, message: activity.result }, { surfaceOp: "append" });
2190
+ } else if (item.type === "fileChange") {
2191
+ flushReasoning();
2192
+ flushHeld();
2193
+ const activity = mapFileChange(item);
2194
+ this.session.append("tool/call", {
2195
+ turn,
2196
+ step,
2197
+ callId: activity.call.callId,
2198
+ name: activity.call.name,
2199
+ arguments: activity.call.arguments
2200
+ });
2201
+ this.session.append("tool/result", { turn, step, message: activity.result }, { surfaceOp: "append" });
2202
+ } else if (item.type === "mcpToolCall") {
2203
+ flushReasoning();
2204
+ flushHeld();
2205
+ const activity = mapMcpToolCall(item);
2206
+ this.session.append("tool/call", {
2207
+ turn,
2208
+ step,
2209
+ callId: activity.call.callId,
2210
+ name: activity.call.name,
2211
+ arguments: activity.call.arguments
2212
+ });
2213
+ this.session.append("tool/result", { turn, step, message: activity.result }, { surfaceOp: "append" });
2214
+ }
2215
+ break;
2216
+ }
2217
+ case "turn-completed": {
2218
+ const usage = event.turn.usage ? mapUsage2(event.turn.usage) : void 0;
2219
+ if (pendingReasoning.length > 0) flushReasoning(usage);
2220
+ else flushHeld(usage);
2221
+ finished = true;
2222
+ break;
2223
+ }
2224
+ case "error":
2225
+ flushReasoning();
2226
+ flushHeld();
2227
+ throw new LlmError2(event.error.message, "CODEX_ERROR");
2228
+ /* v8 ignore next -- AppServerEvent is a closed union; no unknown kinds */
2229
+ default:
2230
+ break;
2231
+ }
2232
+ }
2233
+ flushReasoning();
2234
+ flushHeld();
2235
+ if (!finished) {
2236
+ throw new LlmError2(
2237
+ `agent "${this.id}": codex query ended without a completed turn`,
2238
+ "CODEX_NO_RESULT"
2239
+ );
2240
+ }
2241
+ return { kind: "completed" };
2242
+ } finally {
2243
+ signal.removeEventListener("abort", cancel);
2244
+ controller.abort();
2245
+ }
2246
+ }
2247
+ };
2248
+
2249
+ // src/engine-codex/loop.ts
2250
+ var CODEX_SANDBOX_MODES = [
2251
+ "read-only",
2252
+ "workspace-write",
2253
+ "danger-full-access"
2254
+ ];
2255
+ var CODEX_APPROVAL_POLICIES = [
2256
+ "never",
2257
+ "on-request",
2258
+ "on-failure",
2259
+ "untrusted"
2260
+ ];
2261
+ var Config2 = z2.object({
2262
+ sandboxMode: z2.union([...CODEX_SANDBOX_MODES]),
2263
+ approvalPolicy: z2.union([...CODEX_APPROVAL_POLICIES]),
2264
+ env: z2.dict(z2.string()).default({}),
2265
+ model: z2.string()
2266
+ });
2267
+ function resolveConfig2(config) {
2268
+ return {
2269
+ sandboxMode: config.sandboxMode,
2270
+ approvalPolicy: config.approvalPolicy,
2271
+ env: config.env ?? {},
2272
+ model: config.model
2273
+ };
2274
+ }
2275
+ var CodexLoop = class extends Service2 {
2276
+ /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
2277
+ static inject = ["agents", "sessions", "systemPrompt"];
2278
+ /** Validated configuration owned by the loop plugin. */
2279
+ config;
2280
+ ownership;
2281
+ /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
2282
+ runtime;
2283
+ constructor(ctx, config) {
2284
+ super(ctx, "agentLoopCodex");
2285
+ this.config = resolveConfig2(config);
2286
+ this.ownership = new FactoryOwnership(ctx.fiber);
2287
+ this.runtime = { ctx };
2288
+ ctx.effect(() => () => this.ownership.dispose(), "agentLoopCodex.transactions()");
2289
+ ctx.effect(() => ctx.agents.setFactory(this), "agentLoopCodex.setFactory()");
2290
+ ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
2291
+ ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
2292
+ ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
2293
+ }
2294
+ /**
2295
+ * Construct the driver, scope, and one memoized reverse teardown for a new
2296
+ * agent. The teardown is registered with the factory and the owner fiber
2297
+ * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
2298
+ * fuses caller cancellation with lifecycle teardown for setup awaits.
2299
+ */
2300
+ /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
2301
+ prepare(ownerCtx, id, options, session, callerSignal) {
2302
+ ownerCtx.fiber.assertActive();
2303
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
2304
+ if (callerSignal?.aborted) {
2305
+ throw callerSignal.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal.reason });
2306
+ }
2307
+ const loopCtx = this.runtime.ctx;
2308
+ const abort = new AbortController();
2309
+ const onCallerAbort = () => {
2310
+ abort.abort(callerSignal?.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal?.reason }));
2311
+ };
2312
+ const onFactoryTeardown = () => {
2313
+ abort.abort(this.ownership.signal.reason);
2314
+ };
2315
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
2316
+ this.ownership.signal.addEventListener("abort", onFactoryTeardown, { once: true });
2317
+ let machine;
2318
+ let detachSession;
2319
+ let detachAgent;
2320
+ let disposing;
2321
+ const machineReady = Promise.withResolvers();
2322
+ const dispose = (ownerTriggered = false) => disposing ??= (async () => {
2323
+ abort.abort(new Error(`agent "${id}" lifecycle disposed`));
2324
+ callerSignal?.removeEventListener("abort", onCallerAbort);
2325
+ this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
2326
+ try {
2327
+ if (machine === void 0) await machineReady.promise;
2328
+ if (machine !== void 0) {
2329
+ machine.cancel({ kind: "disposed" });
2330
+ await machine.whenIdle();
2331
+ await machine.scope.dispose();
2332
+ }
2333
+ } finally {
2334
+ try {
2335
+ detachAgent?.();
2336
+ detachSession?.();
2337
+ } finally {
2338
+ untrack();
2339
+ if (!ownerTriggered) await unfollowOwner();
2340
+ }
2341
+ }
2342
+ })();
2343
+ const untrack = this.ownership.track(dispose);
2344
+ let unfollowOwner;
2345
+ try {
2346
+ unfollowOwner = ownerCtx.effect(() => () => {
2347
+ if (disposing !== void 0) return;
2348
+ abort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
2349
+ return dispose(true);
2350
+ }, `agentLoopCodex.lifecycle(${id})`);
2351
+ } catch (error) {
2352
+ untrack();
2353
+ callerSignal?.removeEventListener("abort", onCallerAbort);
2354
+ this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
2355
+ throw error;
2356
+ }
2357
+ const assertLive = () => {
2358
+ if (!abort.signal.aborted) return;
2359
+ throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
2360
+ };
2361
+ try {
2362
+ const agent = machine = new CodexAgent(loopCtx, id, options, session, this.config);
2363
+ machineReady.resolve();
2364
+ assertLive();
2365
+ return {
2366
+ agent,
2367
+ signal: abort.signal,
2368
+ publish: (source) => {
2369
+ assertLive();
2370
+ detachSession = agent.ctx.sessions.enter(session);
2371
+ detachAgent = loopCtx.agents.enter(agent, ownerCtx.agent);
2372
+ agent.ctx.sessions.announce(session);
2373
+ assertLive();
2374
+ loopCtx.agents.announce(agent);
2375
+ assertLive();
2376
+ emitAgentEvent2(loopCtx, agent, "agent/session-start", { source });
2377
+ assertLive();
2378
+ return { agent, dispose };
2379
+ },
2380
+ dispose
2381
+ };
2382
+ } catch (error) {
2383
+ machineReady.resolve();
2384
+ void dispose();
2385
+ throw error;
2386
+ }
2387
+ }
2388
+ /** Prepare one Agent around an acquired Session, run setup, and publish it. */
2389
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
2390
+ var _stack = [];
2391
+ try {
2392
+ const ownedPreparation = __using(_stack, preparation);
2393
+ const session = ownedPreparation.session;
2394
+ const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
2395
+ try {
2396
+ const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
2397
+ setupCommit?.commit();
2398
+ return prepared.publish(source);
2399
+ } catch (error) {
2400
+ await prepared.dispose();
2401
+ throw error;
2402
+ }
2403
+ } catch (_) {
2404
+ var _error = _, _hasError = true;
2405
+ } finally {
2406
+ __callDispose(_stack, _error, _hasError);
2407
+ }
2408
+ }
2409
+ /**
2410
+ * Create an agent and session under one caller-supplied identity, owned by
2411
+ * the accessing fiber.
2412
+ * @param ownerCtx - caller context that structurally owns the lifecycle.
2413
+ * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
2414
+ * @returns the published handle.
2415
+ */
2416
+ async createAgent(ownerCtx, options) {
2417
+ const preparation = SessionPreparation2.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
2418
+ ...options.seed === void 0 ? {} : { seed: options.seed },
2419
+ ...options.meta === void 0 ? {} : { meta: options.meta }
2420
+ }));
2421
+ const published = this.setupAndPublish(
2422
+ ownerCtx,
2423
+ options.sessionId,
2424
+ preparation,
2425
+ options.agentOptions ?? {},
2426
+ options.setup,
2427
+ options.signal,
2428
+ "startup"
2429
+ );
2430
+ this.ownership.trackWrapper(published);
2431
+ return published;
2432
+ }
2433
+ /**
2434
+ * Resume an owned agent from the configured persistence service.
2435
+ * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
2436
+ * @param options - persisted identity, loop options, setup, and cancellation.
2437
+ * @returns the published handle.
2438
+ */
2439
+ async resume(ownerCtx, options) {
2440
+ const persistence = this.runtime.ctx.get("sessionPersistence");
2441
+ if (persistence === void 0) {
2442
+ throw new Error("cannot resume: session persistence is not configured (load a dsh-session-persistence backend)");
2443
+ }
2444
+ return this.resumeWith(ownerCtx, persistence, options);
2445
+ }
2446
+ /** Resume through an explicit persistence handle. */
2447
+ async resumeWith(ownerCtx, persistence, options) {
2448
+ const id = options.resumeSessionId;
2449
+ let preparation;
2450
+ try {
2451
+ const ownerAbort = new AbortController();
2452
+ const unfollowOwner = ownerCtx.effect(() => () => {
2453
+ ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
2454
+ }, `agentLoopCodex.resume-load(${id})`);
2455
+ const fused = AbortSignal.any([
2456
+ ...options.signal === void 0 ? [] : [options.signal],
2457
+ ownerAbort.signal,
2458
+ this.ownership.signal
2459
+ ]);
2460
+ try {
2461
+ preparation = await raceAbortCall(
2462
+ () => persistence.prepare(id, fused),
2463
+ fused,
2464
+ id,
2465
+ (abandoned) => {
2466
+ abandoned[Symbol.dispose]();
2467
+ }
2468
+ );
2469
+ } finally {
2470
+ await unfollowOwner();
2471
+ }
2472
+ ownerCtx.fiber.assertActive();
2473
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
2474
+ return await this.setupAndPublish(
2475
+ ownerCtx,
2476
+ id,
2477
+ preparation,
2478
+ options.agentOptions ?? {},
2479
+ options.setup,
2480
+ options.signal,
2481
+ "resume"
2482
+ );
2483
+ } finally {
2484
+ preparation?.[Symbol.dispose]();
2485
+ }
2486
+ }
2487
+ };
2488
+
2489
+ // src/engine-pi/loop.ts
2490
+ import { readFileSync } from "node:fs";
2491
+ import { dirname as dirname2, join as join2 } from "node:path";
2492
+ import { fileURLToPath } from "node:url";
2493
+ import { Service as Service3 } from "@deepseek-ai/cordis";
2494
+ import z3 from "@deepseek-ai/schemastery";
2495
+ import { emitAgentEvent as emitAgentEvent3 } from "@deepseek-ai/dsh-agent";
2496
+ import { SessionPreparation as SessionPreparation3 } from "@deepseek-ai/dsh-session";
2497
+
2498
+ // src/engine-pi/agent.ts
2499
+ import { Inbox as Inbox3, agentEvents as agentEvents3 } from "@deepseek-ai/dsh-agent";
2500
+ import { CallId as CallId4, LlmError as LlmError3, createAssistantMessage as createAssistantMessage3, createUserMessage as createUserMessage3, errorChain as errorChain3 } from "@deepseek-ai/dsh-llm";
2501
+ import { createScope as createScope3 } from "@deepseek-ai/dsh-scope";
2502
+ import { canonicalHeader as canonicalHeader3 } from "@deepseek-ai/dsh-session";
2503
+
2504
+ // src/engine-pi/permission.ts
2505
+ var DEFAULT_PI_PERMISSION = {
2506
+ sandboxMode: "read-only",
2507
+ tools: ["read", "grep", "find", "ls"]
2508
+ };
2509
+ var WORKSPACE_WRITE_TOOLS = ["read", "grep", "find", "ls", "write", "edit"];
2510
+ var FULL_ACCESS_TOOLS = [];
2511
+ function toolsForSandbox(mode) {
2512
+ switch (mode) {
2513
+ case "danger-full-access":
2514
+ return FULL_ACCESS_TOOLS;
2515
+ case "workspace-write":
2516
+ return WORKSPACE_WRITE_TOOLS;
2517
+ default:
2518
+ return DEFAULT_PI_PERMISSION.tools;
2519
+ }
2520
+ }
2521
+ function resolveSessionPermission3(events) {
2522
+ if (sessionSandboxMode(events) === "danger-full-access") {
2523
+ return { sandboxMode: "danger-full-access", tools: FULL_ACCESS_TOOLS };
2524
+ }
2525
+ if (sessionApprovalPolicy(events) === "ask") {
2526
+ return { sandboxMode: "read-only", tools: DEFAULT_PI_PERMISSION.tools };
2527
+ }
2528
+ if (sessionSandboxMode(events) === "workspace-write") {
2529
+ return { sandboxMode: "workspace-write", tools: WORKSPACE_WRITE_TOOLS };
2530
+ }
2531
+ return DEFAULT_PI_PERMISSION;
2532
+ }
2533
+
2534
+ // src/engine-pi/rpc/client.ts
2535
+ import { spawn as spawn2 } from "node:child_process";
2536
+ import { StringDecoder } from "node:string_decoder";
2537
+ function defaultSpawn(spec) {
2538
+ const child = spawn2(process.execPath, [...spec.argv], {
2539
+ cwd: spec.cwd,
2540
+ env: spec.env,
2541
+ stdio: ["pipe", "pipe", "pipe"]
2542
+ });
2543
+ return fromChildProcess(child);
2544
+ }
2545
+ function fromChildProcess(child) {
2546
+ return {
2547
+ stdin: child.stdin,
2548
+ stdout: child.stdout,
2549
+ stderr: child.stderr,
2550
+ onExit: (handler) => {
2551
+ child.once("exit", handler);
2552
+ },
2553
+ terminate: () => child.kill()
2554
+ };
2555
+ }
2556
+ var PiRpcClient = class _PiRpcClient {
2557
+ /** Mount a client over an already-spawned Pi RPC process. */
2558
+ constructor(process2) {
2559
+ this.process = process2;
2560
+ this.process.stdout.on("data", (chunk) => this.feed(chunk));
2561
+ this.process.stderr.on("data", this.onStderr);
2562
+ this.process.onExit(() => {
2563
+ this.disposed = true;
2564
+ const err = new Error("pi RPC process exited unexpectedly");
2565
+ for (const { reject } of this.pending.values()) reject(err);
2566
+ this.pending.clear();
2567
+ this.eventWake?.();
2568
+ });
2569
+ }
2570
+ process;
2571
+ reqId = 1;
2572
+ pending = /* @__PURE__ */ new Map();
2573
+ eventBuffer = [];
2574
+ eventWake;
2575
+ eventHandler;
2576
+ disposed = false;
2577
+ decoder = new StringDecoder("utf8");
2578
+ buffer = "";
2579
+ onStderr = (chunk) => {
2580
+ void this.consumeStderr(chunk);
2581
+ };
2582
+ /** Whether this client was disposed or its process exited. */
2583
+ get closed() {
2584
+ return this.disposed;
2585
+ }
2586
+ /**
2587
+ * Create a client, spawning the Pi RPC child through the supplied capability
2588
+ * (or the default node-runtime spawn when none is given).
2589
+ * @param spec - the Pi CLI argv/cwd/env the child should run with.
2590
+ * @param spawn - optional process-spawn capability (the subprocess seam);
2591
+ * absent falls back to the plain node child spawn.
2592
+ * @returns the connected client.
2593
+ */
2594
+ static create(spec, spawn3) {
2595
+ const process2 = spawn3 === void 0 ? defaultSpawn(spec) : spawn3(spec);
2596
+ return new _PiRpcClient(process2);
2597
+ }
2598
+ /** Register the event dispatch handler. */
2599
+ onEvent(handler) {
2600
+ this.eventHandler = handler;
2601
+ }
2602
+ /** Drop any events still buffered from a previous step (stateless per-step sessions). */
2603
+ clearEvents() {
2604
+ this.eventBuffer.length = 0;
2605
+ }
2606
+ /** Start a fresh Pi session. */
2607
+ async newSession() {
2608
+ const command = { type: "new_session" };
2609
+ return this.request(command);
2610
+ }
2611
+ /** Prompt the agent and await the acceptance response. */
2612
+ async prompt(message, options = {}) {
2613
+ const command = { type: "prompt", message, ...options };
2614
+ return this.request(command);
2615
+ }
2616
+ /** Abort the current agent operation. */
2617
+ async abort() {
2618
+ const command = { type: "abort" };
2619
+ return this.request(command);
2620
+ }
2621
+ /** Query session stats. */
2622
+ async getSessionStats() {
2623
+ const command = { type: "get_session_stats" };
2624
+ return this.request(command);
2625
+ }
2626
+ /** Send a command without awaiting its response (fire-and-forget). */
2627
+ send(command) {
2628
+ if (this.disposed) return;
2629
+ this.process.stdin.write(`${JSON.stringify(command)}
2630
+ `);
2631
+ }
2632
+ /**
2633
+ * Send a command and await the correlated response. Assigns a fresh `id`
2634
+ * when the command carries none, so responses always round-trip.
2635
+ */
2636
+ async request(command) {
2637
+ if (this.disposed) throw new Error("pi RPC client is disposed");
2638
+ const id = command.id ?? this.reqId++;
2639
+ const wire = { ...command, id };
2640
+ return new Promise((resolve4, reject) => {
2641
+ this.pending.set(id, { resolve: resolve4, reject });
2642
+ this.process.stdin.write(`${JSON.stringify(wire)}
2643
+ `);
2644
+ });
2645
+ }
2646
+ /**
2647
+ * Consume every buffered event as an async generator, waking as fresh lines
2648
+ * arrive. The caller bounds the iteration by a terminal event; unmatched
2649
+ * lines stay buffered for a later iteration.
2650
+ */
2651
+ async *events() {
2652
+ while (true) {
2653
+ if (this.eventBuffer.length > 0) {
2654
+ yield this.eventBuffer.shift();
2655
+ continue;
2656
+ }
2657
+ if (this.disposed) return;
2658
+ await new Promise((resolve4) => {
2659
+ this.eventWake = resolve4;
2660
+ });
2661
+ this.eventWake = void 0;
2662
+ }
2663
+ }
2664
+ /** Dispose the client and request child termination. */
2665
+ dispose() {
2666
+ if (this.disposed) return;
2667
+ this.disposed = true;
2668
+ this.process.terminate();
2669
+ const err = new Error("pi RPC client is disposed");
2670
+ for (const { reject } of this.pending.values()) reject(err);
2671
+ this.pending.clear();
2672
+ this.eventWake?.();
2673
+ }
2674
+ /** Feed one chunk of stdout into the framing state machine. */
2675
+ feed(chunk) {
2676
+ if (this.disposed) return;
2677
+ const text = typeof chunk === "string" ? this.buffer + chunk : this.buffer + this.decoder.write(chunk);
2678
+ this.buffer = text;
2679
+ while (true) {
2680
+ const newline = this.buffer.indexOf("\n");
2681
+ if (newline === -1) break;
2682
+ let line = this.buffer.slice(0, newline);
2683
+ this.buffer = this.buffer.slice(newline + 1);
2684
+ if (line.endsWith("\r")) line = line.slice(0, -1);
2685
+ this.dispatch(line);
2686
+ }
2687
+ }
2688
+ /** Dispatch one parsed line to the pending map or the event queue. */
2689
+ dispatch(line) {
2690
+ if (line.trim().length === 0) return;
2691
+ let obj;
2692
+ try {
2693
+ obj = JSON.parse(line);
2694
+ } catch {
2695
+ return;
2696
+ }
2697
+ if (obj.type === "response") {
2698
+ if (obj.id !== void 0) {
2699
+ const pending = this.pending.get(obj.id);
2700
+ if (pending !== void 0) {
2701
+ this.pending.delete(obj.id);
2702
+ if (obj.success) pending.resolve(obj);
2703
+ else pending.reject(new Error(obj.error ?? `pi RPC command "${obj.command ?? ""}" failed`));
2704
+ }
2705
+ }
2706
+ return;
2707
+ }
2708
+ const event = obj;
2709
+ this.eventHandler?.(event);
2710
+ this.eventBuffer.push(event);
2711
+ this.eventWake?.();
2712
+ }
2713
+ /** Drain decoded stderr bytes (no-op consumer keeps the pipe flowing). */
2714
+ consumeStderr(_chunk) {
2715
+ }
2716
+ };
2717
+
2718
+ // src/engine-pi/rpc/mapping.ts
2719
+ import {
2720
+ CallId as CallId3,
2721
+ createToolResultMessage as createToolResultMessage3
2722
+ } from "@deepseek-ai/dsh-llm";
2723
+ function mapUsage3(usage) {
2724
+ return {
2725
+ inputTokens: usage.input ?? 0,
2726
+ outputTokens: usage.output ?? 0,
2727
+ ...usage.cacheRead !== void 0 && usage.cacheRead > 0 ? { cacheReadTokens: usage.cacheRead } : {},
2728
+ ...usage.cacheWrite !== void 0 && usage.cacheWrite > 0 ? { cacheWriteTokens: usage.cacheWrite } : {}
2729
+ };
2730
+ }
2731
+ function resultText(content) {
2732
+ if (Array.isArray(content)) {
2733
+ return content.map((block) => {
2734
+ if (typeof block === "object" && block !== null) {
2735
+ const text = block.text;
2736
+ return typeof text === "string" ? text : "";
2737
+ }
2738
+ return "";
2739
+ }).filter((segment) => segment !== "").join("\n\n");
2740
+ }
2741
+ if (typeof content === "object" && content !== null) {
2742
+ const nested = content.content;
2743
+ const text = content.text;
2744
+ if (typeof text === "string") return text;
2745
+ if (nested !== void 0) return resultText(nested);
2746
+ }
2747
+ return "";
2748
+ }
2749
+ function mapToolResult(ev) {
2750
+ return createToolResultMessage3({
2751
+ callId: CallId3(ev.toolCallId),
2752
+ content: [{ type: "text", text: resultText(ev.result) || "(no content)" }],
2753
+ isError: ev.isError
2754
+ });
2755
+ }
2756
+
2757
+ // src/engine-pi/agent.ts
2758
+ var PROVIDER3 = "pi";
2759
+ var NATIVE_MODEL_LABEL3 = "pi-native";
2760
+ var TOOLS_FLAG = "--tools";
2761
+ function specsEqual(a, b) {
2762
+ if (a === void 0) return false;
2763
+ return a.cwd === b.cwd && a.env === b.env && a.argv.length === b.argv.length && a.argv.every((value, index) => value === b.argv[index]);
2764
+ }
2765
+ var PiAgent = class {
2766
+ constructor(loopCtx, id, options, session, config, spawn3, bin) {
2767
+ this.loopCtx = loopCtx;
2768
+ this.id = id;
2769
+ this.options = options;
2770
+ this.session = session;
2771
+ this.config = config;
2772
+ this.spawn = spawn3;
2773
+ this.bin = bin;
2774
+ this.dispatch = agentEvents3(loopCtx, this);
2775
+ this.inbox = new Inbox3(session, {
2776
+ inserted: (message) => {
2777
+ this.dispatch.emit("agent/inbox/inserted", { message });
2778
+ },
2779
+ discarded: (message) => {
2780
+ this.dispatch.emit("agent/inbox/discarded", { message });
2781
+ },
2782
+ claimed: (message, turn) => {
2783
+ this.dispatch.emit("agent/inbox/claimed", { message, turn });
2784
+ }
2785
+ });
2786
+ const lastTurn = session.events.findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
2787
+ this.phase = { kind: "idle", lastTurn };
2788
+ this.scope = createScope3(loopCtx, this);
2789
+ this.ctx = this.scope.ctx.extend({ agent: this });
2790
+ this.scope.ctx.effect(() => () => {
2791
+ this.rpc?.dispose();
2792
+ this.rpc = void 0;
2793
+ }, "pi.rpcClient()");
2794
+ }
2795
+ loopCtx;
2796
+ id;
2797
+ options;
2798
+ session;
2799
+ config;
2800
+ spawn;
2801
+ bin;
2802
+ inbox;
2803
+ phase;
2804
+ activityDone = Promise.resolve();
2805
+ /** The agent-scoped registration boundary; the lifecycle owner unwinds it after the driver exits. */
2806
+ scope;
2807
+ ctx;
2808
+ /** Fused dispatcher, built once in the constructor so hot-path dispatches never allocate. */
2809
+ dispatch;
2810
+ /** Whether this loop instance has appended its initial/resume request anchor. */
2811
+ requestHeaderLogged = false;
2812
+ /** Lazily created RPC client, reused across steps and released on scope teardown. */
2813
+ rpc;
2814
+ /** The spawn spec the cached client was built from; a change forces a respawn. */
2815
+ lastSpec;
2816
+ /** Return the cached RPC client, respawning when the spec or process changed. */
2817
+ async rpcClient(cwd) {
2818
+ const spec = this.spawnSpec(cwd);
2819
+ if (this.rpc !== void 0 && !this.rpc.closed && specsEqual(this.lastSpec, spec)) return this.rpc;
2820
+ this.rpc?.dispose();
2821
+ const client = PiRpcClient.create(spec, this.spawn);
2822
+ this.rpc = client;
2823
+ this.lastSpec = spec;
2824
+ return client;
2825
+ }
2826
+ get status() {
2827
+ return this.phase.kind === "idle" || this.phase.kind === "maintenance" ? "idle" : "running";
2828
+ }
2829
+ /** Commit a phase and publish its externally visible status transition. */
2830
+ setPhase(next) {
2831
+ const previousStatus = this.status;
2832
+ this.phase = next;
2833
+ const status = this.status;
2834
+ if (status !== previousStatus) {
2835
+ this.dispatch.emit("agent/status", { status });
2836
+ }
2837
+ }
2838
+ send(message, target, wakeup) {
2839
+ const wakingAfterAbort = wakeup && this.phase.kind !== "idle" && this.phase.abort.signal.aborted;
2840
+ const resolvedTarget = wakingAfterAbort ? "next-turn" : target;
2841
+ this.inbox.splice(resolvedTarget, Infinity, 0, [message]);
2842
+ if (wakeup) this.wakeDriver(wakingAfterAbort);
2843
+ }
2844
+ /**
2845
+ * Queue a message for the next turn and wake the driver.
2846
+ * @param input - the user message to deliver.
2847
+ */
2848
+ followup(input) {
2849
+ this.send(input, "next-turn", true);
2850
+ }
2851
+ /**
2852
+ * Queue a message for the running step and wake the driver.
2853
+ * @param input - the user message to deliver.
2854
+ */
2855
+ steer(input) {
2856
+ this.send(input, "next-step", true);
2857
+ }
2858
+ /**
2859
+ * Queue a message for the running step without waking the driver.
2860
+ * @param input - the user message to deliver.
2861
+ */
2862
+ inject(input) {
2863
+ this.send(input, "next-step", false);
2864
+ }
2865
+ cancel(cause, options = {}) {
2866
+ if (!options.keepInbox) {
2867
+ this.inbox.clear();
2868
+ if (this.phase.kind !== "idle") this.phase.wakeRequested = false;
2869
+ }
2870
+ if (this.phase.kind !== "idle") this.phase.abort.abort(cause);
2871
+ }
2872
+ /**
2873
+ * Run a maintenance job while the agent is idle.
2874
+ * @param job - the maintenance operation, receiving the phase abort signal.
2875
+ * @returns the maintenance result.
2876
+ */
2877
+ runMaintenance(job) {
2878
+ if (this.phase.kind !== "idle") throw new Error(`agent "${this.id}" already has active work`);
2879
+ const done = Promise.withResolvers();
2880
+ const maintenance = {
2881
+ kind: "maintenance",
2882
+ abort: new AbortController(),
2883
+ lastTurn: this.phase.lastTurn,
2884
+ wakeRequested: false
2885
+ };
2886
+ this.setPhase(maintenance);
2887
+ this.activityDone = done.promise;
2888
+ return (async () => {
2889
+ try {
2890
+ return await job(maintenance.abort.signal);
2891
+ } finally {
2892
+ this.setPhase({ kind: "idle", lastTurn: maintenance.lastTurn });
2893
+ if (maintenance.wakeRequested && this.inbox.hasPending) this.wakeDriver();
2894
+ done.resolve();
2895
+ }
2896
+ })();
2897
+ }
2898
+ /**
2899
+ * Start one driver, or latch its wake behind maintenance or an aborted
2900
+ * activity. A wake sent while idle always opens its turn boundary, even
2901
+ * when its message was cleared; only a latched replay is suppressed when
2902
+ * the queue no longer holds the wake.
2903
+ * @param wakeAfterAbort - the {@link send} classification, captured before
2904
+ * the inbox insertion so a reentrant cancel cannot reclassify it.
2905
+ */
2906
+ wakeDriver(wakeAfterAbort = false) {
2907
+ if (this.phase.kind !== "idle") {
2908
+ const reason = this.phase.abort.signal.reason;
2909
+ if (reason?.kind !== "disposed" && (this.phase.kind === "maintenance" || wakeAfterAbort)) {
2910
+ this.phase.wakeRequested = true;
2911
+ }
2912
+ return;
2913
+ }
2914
+ const driver = Promise.withResolvers();
2915
+ this.activityDone = driver.promise;
2916
+ this.setPhase({
2917
+ kind: "running",
2918
+ abort: new AbortController(),
2919
+ turn: this.phase.lastTurn,
2920
+ step: 0,
2921
+ wakeRequested: false
2922
+ });
2923
+ this.loopCtx.agents.withInitiator(this, () => this.kick()).then(driver.resolve, driver.reject);
2924
+ }
2925
+ async whenIdle() {
2926
+ let activity;
2927
+ do {
2928
+ await (activity = this.activityDone);
2929
+ } while (activity !== this.activityDone);
2930
+ }
2931
+ /** Report one failure at its live boundary, then preserve it for driver containment. */
2932
+ throwError(error) {
2933
+ const turn = this.phase.kind === "running" ? this.phase.turn : this.phase.lastTurn;
2934
+ const step = this.phase.kind === "running" ? this.phase.step : 0;
2935
+ this.dispatch.emit("agent/error", { turn, step, error });
2936
+ throw error;
2937
+ }
2938
+ async kick() {
2939
+ try {
2940
+ while (await this.turn()) {
2941
+ }
2942
+ } catch (_error) {
2943
+ } finally {
2944
+ if (this.phase.kind === "running") {
2945
+ const { turn, wakeRequested } = this.phase;
2946
+ this.setPhase({ kind: "idle", lastTurn: turn });
2947
+ if (wakeRequested && this.inbox.hasPending) this.wakeDriver();
2948
+ }
2949
+ }
2950
+ }
2951
+ async preStep(target, position) {
2952
+ if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": pre-step outside running phase`);
2953
+ const signal = this.phase.abort.signal;
2954
+ const claimed = this.inbox.claim(target, position.turn);
2955
+ const decision = await this.dispatch.waterfall(
2956
+ "agent/pre-step",
2957
+ { messages: claimed, ...position, signal },
2958
+ () => Promise.resolve({ kind: "enter", messages: claimed })
2959
+ );
2960
+ signal.throwIfAborted();
2961
+ if (decision.kind === "reject") return decision;
2962
+ const injected = await this.injectSkills(decision.messages, signal);
2963
+ signal.throwIfAborted();
2964
+ return injected !== decision.messages ? { kind: "enter", messages: [...injected] } : { ...decision };
2965
+ }
2966
+ /**
2967
+ * Scan the step's user messages for `/name` skill gestures, load each
2968
+ * matching skill, and inject the rendered skill content into the message
2969
+ * batch. This mirrors what dsh-tool-skill does for the in-process engine.
2970
+ * @param messages - the current step's message batch.
2971
+ * @param signal - cancellation signal (aborted loads are silently dropped).
2972
+ * @returns the original batch when no skill was invoked, or an extended
2973
+ * batch with injected skill-content messages appended.
2974
+ */
2975
+ async injectSkills(messages, signal) {
2976
+ const names = invokedSkillNames(messages);
2977
+ if (names.length === 0) return messages;
2978
+ const skills = this.loopCtx.get("skills");
2979
+ if (skills === void 0) return messages;
2980
+ const cwd = this.session.header.cwd;
2981
+ const injections = [];
2982
+ for (const name2 of names) {
2983
+ if (!isSkillName(name2)) continue;
2984
+ let skill;
2985
+ try {
2986
+ skill = await skills.get(name2, { signal, scope: this, ...cwd === void 0 ? {} : { cwd } });
2987
+ } catch {
2988
+ continue;
2989
+ }
2990
+ if (skill === void 0 || !skill.invocation.userInvocable) continue;
2991
+ if (signal.aborted) return messages;
2992
+ injections.push(createUserMessage3({
2993
+ content: [{ type: "text", text: renderSkillContent(skill) }],
2994
+ source: { kind: "skill-invocation", name: name2, form: "instructions" }
2995
+ }));
2996
+ }
2997
+ return injections.length > 0 ? [...messages, ...injections] : messages;
2998
+ }
2999
+ /**
3000
+ * Resolve the runtime permission stance for one query. Deployment-pinned
3001
+ * fields win; anything unpinned follows the session's durable dsh permission
3002
+ * knobs, re-folded per query so mid-session preset switches take effect on the
3003
+ * next step.
3004
+ * @returns the permission fields of the query spec.
3005
+ */
3006
+ queryPermission() {
3007
+ const fold = resolveSessionPermission3(this.session.events);
3008
+ const sandboxMode = this.config.sandboxMode ?? fold.sandboxMode;
3009
+ return {
3010
+ sandboxMode,
3011
+ tools: this.config.sandboxMode === void 0 ? fold.tools : toolsForSandbox(sandboxMode)
3012
+ };
3013
+ }
3014
+ /** Open one turn before claiming its first proposed step. */
3015
+ async turn() {
3016
+ if (this.phase.kind !== "running") {
3017
+ this.throwError(new Error(`agent "${this.id}": turn without driver reservation`));
3018
+ }
3019
+ const phase = this.phase;
3020
+ const { signal } = phase.abort;
3021
+ signal.throwIfAborted();
3022
+ const turn = phase.turn + 1;
3023
+ try {
3024
+ this.session.append("turn/start", { turn });
3025
+ } catch (error) {
3026
+ this.throwError(error);
3027
+ }
3028
+ phase.turn = turn;
3029
+ let turnEnds = null;
3030
+ let target = "next-turn";
3031
+ try {
3032
+ while (true) {
3033
+ signal.throwIfAborted();
3034
+ const step = phase.step + 1;
3035
+ const decision = await this.preStep(target, { turn, step });
3036
+ if (decision.kind === "reject") {
3037
+ turnEnds = { kind: "blocked" };
3038
+ return false;
3039
+ }
3040
+ if (turnEnds && decision.messages.length === 0) break;
3041
+ if (phase.step === 0 && decision.messages.length === 0) {
3042
+ turnEnds = { kind: "completed" };
3043
+ return false;
3044
+ }
3045
+ signal.throwIfAborted();
3046
+ this.session.append("step/start", { turn, step });
3047
+ phase.step = step;
3048
+ try {
3049
+ for (const message of decision.messages) {
3050
+ this.session.append("user/message", message, { surfaceOp: "append" });
3051
+ }
3052
+ const stepEnd = await this.step();
3053
+ if (turnEnds === null) turnEnds = stepEnd;
3054
+ } finally {
3055
+ this.session.append("step/end", { turn, step });
3056
+ }
3057
+ signal.throwIfAborted();
3058
+ if (turnEnds && this.inbox.nextStep.length === 0) {
3059
+ await this.dispatch.serial("agent/turn-stopping", { turn, signal });
3060
+ signal.throwIfAborted();
3061
+ }
3062
+ if (turnEnds && this.inbox.nextStep.length === 0) break;
3063
+ target = "next-step";
3064
+ }
3065
+ } catch (error) {
3066
+ if (signal.aborted) {
3067
+ turnEnds = { kind: "aborted", reason: signal.reason };
3068
+ throw error;
3069
+ }
3070
+ turnEnds = {
3071
+ kind: "error",
3072
+ error: error instanceof LlmError3 ? error.failure : { message: errorChain3(error), code: "UNKNOWN" }
3073
+ };
3074
+ this.throwError(error);
3075
+ } finally {
3076
+ try {
3077
+ this.session.append("turn/end", { turn, reason: turnEnds });
3078
+ } catch (error) {
3079
+ this.throwError(error);
3080
+ }
3081
+ }
3082
+ if (!this.inbox.hasPending) return false;
3083
+ phase.abort = new AbortController();
3084
+ phase.wakeRequested = false;
3085
+ phase.step = 0;
3086
+ return true;
3087
+ }
3088
+ /** Model label recorded in the request header for one lifecycle. */
3089
+ modelLabel() {
3090
+ return this.config.model ?? NATIVE_MODEL_LABEL3;
3091
+ }
3092
+ /** Append the request header snapshot once per loop instance. */
3093
+ assertRequestHeader() {
3094
+ if (this.requestHeaderLogged) return;
3095
+ const header = canonicalHeader3({
3096
+ config: { provider: PROVIDER3, model: this.modelLabel() }
3097
+ });
3098
+ const baseline = this.session.requestHeader();
3099
+ this.session.append("request/header", {
3100
+ header,
3101
+ reason: baseline === void 0 ? "initial" : "resume"
3102
+ });
3103
+ this.requestHeaderLogged = true;
3104
+ }
3105
+ /** Build the `pi --mode rpc` argv/cwd/env for one step's child process. */
3106
+ spawnSpec(cwd) {
3107
+ const argv = [];
3108
+ if (this.config.provider !== void 0) argv.push("--provider", this.config.provider);
3109
+ if (this.config.model !== void 0 && this.config.thinkingLevel !== void 0) {
3110
+ argv.push("--model", `${this.config.model}:${this.config.thinkingLevel}`);
3111
+ } else if (this.config.model !== void 0) {
3112
+ argv.push("--model", this.config.model);
3113
+ } else if (this.config.thinkingLevel !== void 0) {
3114
+ argv.push("--model", `:${this.config.thinkingLevel}`);
3115
+ }
3116
+ const permission = this.queryPermission();
3117
+ if (permission.tools.length > 0) argv.push(TOOLS_FLAG, permission.tools.join(","));
3118
+ return {
3119
+ argv: [
3120
+ this.bin,
3121
+ "--mode",
3122
+ "rpc",
3123
+ "--no-session",
3124
+ ...argv
3125
+ ],
3126
+ cwd,
3127
+ env: this.config.env
3128
+ };
3129
+ }
3130
+ /**
3131
+ * Run one Pi RPC query for the current step and map its event stream into the
3132
+ * session log. The step opens a fresh Pi session (`new_session`) and sends the
3133
+ * serialized session history as one prompt, then consumes events until the
3134
+ * agent settles. Like the Codex/Claude drivers, Pi owns its own system prompt
3135
+ * natively, so the dsh system-prompt assembly (which pulls dsh tool schemas
3136
+ * and `agent.ctx.tools`) is deliberately not run — the durable session log is
3137
+ * the sole source of model context.
3138
+ */
3139
+ async step() {
3140
+ if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
3141
+ const { turn, step, abort: { signal } } = this.phase;
3142
+ signal.throwIfAborted();
3143
+ const cwd = this.session.header.cwd;
3144
+ if (cwd === void 0 || cwd.length === 0) {
3145
+ throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
3146
+ }
3147
+ const history = this.session.deriveMessages();
3148
+ const prompt = serializeHistory(history);
3149
+ if (prompt.length === 0) {
3150
+ throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
3151
+ }
3152
+ this.assertRequestHeader();
3153
+ signal.throwIfAborted();
3154
+ const controller = new AbortController();
3155
+ const cancel = () => {
3156
+ if (!controller.signal.aborted) {
3157
+ controller.abort(signal.reason instanceof Error ? signal.reason : new Error(`agent "${this.id}" query aborted`));
3158
+ void this.rpc?.abort().catch(() => void 0);
3159
+ }
3160
+ };
3161
+ signal.addEventListener("abort", cancel, { once: true });
3162
+ try {
3163
+ const client = await this.rpcClient(cwd);
3164
+ signal.throwIfAborted();
3165
+ await client.newSession();
3166
+ client.clearEvents();
3167
+ await client.prompt(prompt);
3168
+ let finished = false;
3169
+ let settled = false;
3170
+ const chunkSeqs = [];
3171
+ let held;
3172
+ const startedText = /* @__PURE__ */ new Set();
3173
+ const startedReasoning = /* @__PURE__ */ new Set();
3174
+ const thinkingByIndex = /* @__PURE__ */ new Map();
3175
+ const emittedToolCalls = /* @__PURE__ */ new Set();
3176
+ let lastUsage;
3177
+ let assistantFlushed = false;
3178
+ const emitChunk = (chunk) => {
3179
+ const seq = this.session.append("assistant/chunk", { turn, step, chunk }).seq;
3180
+ chunkSeqs.push(seq);
3181
+ return seq;
3182
+ };
3183
+ const flushHeld = (usage) => {
3184
+ if (held === void 0) return;
3185
+ this.session.append("assistant/message", {
3186
+ turn,
3187
+ step,
3188
+ message: createAssistantMessage3({
3189
+ content: held.content,
3190
+ source: { provider: PROVIDER3, model: this.modelLabel() }
3191
+ }),
3192
+ ...usage === void 0 ? {} : { usage }
3193
+ }, {
3194
+ surfaceOp: "append",
3195
+ // Link the durable message to the chunks that streamed it, so replay
3196
+ // can reconstruct the partial exactly as shown.
3197
+ sourceEventSeqs: held.refs
3198
+ });
3199
+ held = void 0;
3200
+ };
3201
+ const ensureTextBlock = (index) => {
3202
+ if (startedText.has(index)) return;
3203
+ startedText.add(index);
3204
+ emitChunk({ type: "block-start", index, blockType: "text" });
3205
+ };
3206
+ const ensureReasoningBlock = (index) => {
3207
+ if (startedReasoning.has(index)) return;
3208
+ startedReasoning.add(index);
3209
+ emitChunk({ type: "block-start", index, blockType: "reasoning" });
3210
+ };
3211
+ const emitToolCall = (callId, name2, rawArguments) => {
3212
+ if (emittedToolCalls.has(callId)) return;
3213
+ emittedToolCalls.add(callId);
3214
+ const argumentsValue = typeof rawArguments === "string" ? rawArguments : JSON.stringify(rawArguments ?? {});
3215
+ this.session.append("tool/call", {
3216
+ turn,
3217
+ step,
3218
+ callId: CallId4(callId),
3219
+ name: name2,
3220
+ arguments: argumentsValue
3221
+ });
3222
+ };
3223
+ const contentOf = (message) => {
3224
+ let blocks = [];
3225
+ const content = Array.isArray(message.content) ? message.content : typeof message.content === "string" ? [{ type: "text", text: message.content }] : [];
3226
+ for (const block of content) {
3227
+ if (block.type === "text") {
3228
+ blocks.push({ type: "text", text: block.text });
3229
+ } else if (block.type === "thinking") {
3230
+ blocks.push({ type: "reasoning", text: block.thinking });
3231
+ }
3232
+ }
3233
+ if (!blocks.some((block) => block.type === "reasoning") && thinkingByIndex.size > 0) {
3234
+ const folded = [...thinkingByIndex.entries()].sort((a, b) => a[0] - b[0]).map(([, text]) => ({ type: "reasoning", text }));
3235
+ blocks = [...folded, ...blocks];
3236
+ }
3237
+ return blocks;
3238
+ };
3239
+ signal.throwIfAborted();
3240
+ for await (const event of client.events()) {
3241
+ signal.throwIfAborted();
3242
+ switch (event.type) {
3243
+ case "agent_start":
3244
+ case "compaction_start":
3245
+ case "compaction_end":
3246
+ case "auto_retry_start":
3247
+ case "auto_retry_end":
3248
+ case "queue_update":
3249
+ case "bash_execution_update":
3250
+ case "extension_ui_request":
3251
+ break;
3252
+ case "message_start":
3253
+ if (event.message.role === "assistant") {
3254
+ chunkSeqs.length = 0;
3255
+ startedText.clear();
3256
+ startedReasoning.clear();
3257
+ thinkingByIndex.clear();
3258
+ }
3259
+ break;
3260
+ case "message_update": {
3261
+ if (event.usage !== void 0) lastUsage = mapUsage3(event.usage);
3262
+ const delta = event.assistantMessageEvent;
3263
+ switch (delta.type) {
3264
+ case "text_start":
3265
+ case "thinking_start":
3266
+ break;
3267
+ case "text_delta":
3268
+ ensureTextBlock(delta.contentIndex);
3269
+ emitChunk({ type: "text-delta", index: delta.contentIndex, text: delta.delta });
3270
+ break;
3271
+ case "thinking_delta":
3272
+ ensureReasoningBlock(delta.contentIndex);
3273
+ emitChunk({ type: "reasoning-delta", index: delta.contentIndex, text: delta.delta });
3274
+ thinkingByIndex.set(delta.contentIndex, (thinkingByIndex.get(delta.contentIndex) ?? "") + delta.delta);
3275
+ break;
3276
+ case "toolcall_start":
3277
+ break;
3278
+ case "toolcall_delta":
3279
+ break;
3280
+ case "toolcall_end":
3281
+ emitToolCall(delta.toolCall.id, delta.toolCall.name, delta.toolCall.arguments);
3282
+ break;
3283
+ case "text_end":
3284
+ case "thinking_end":
3285
+ break;
3286
+ }
3287
+ break;
3288
+ }
3289
+ case "message_end": {
3290
+ if (event.message.role === "assistant") {
3291
+ if (event.message.usage !== void 0) lastUsage = mapUsage3(event.message.usage);
3292
+ flushHeld();
3293
+ held = { content: contentOf(event.message), refs: [...chunkSeqs] };
3294
+ chunkSeqs.length = 0;
3295
+ flushHeld(lastUsage);
3296
+ assistantFlushed = true;
3297
+ }
3298
+ break;
3299
+ }
3300
+ case "tool_execution_start":
3301
+ emitToolCall(event.toolCallId, event.toolName, event.args);
3302
+ break;
3303
+ case "tool_execution_update":
3304
+ break;
3305
+ case "tool_execution_end":
3306
+ emitToolCall(event.toolCallId, event.toolName, void 0);
3307
+ this.session.append("tool/result", {
3308
+ turn,
3309
+ step,
3310
+ message: mapToolResult({ toolCallId: event.toolCallId, result: event.result, isError: event.isError })
3311
+ }, { surfaceOp: "append" });
3312
+ break;
3313
+ case "turn_end": {
3314
+ if (!assistantFlushed && event.message !== void 0) {
3315
+ if (event.message.usage !== void 0) lastUsage = mapUsage3(event.message.usage);
3316
+ held = { content: contentOf(event.message), refs: [...chunkSeqs] };
3317
+ chunkSeqs.length = 0;
3318
+ }
3319
+ for (const toolResult of event.toolResults ?? []) {
3320
+ this.appendToolResult(turn, step, toolResult);
3321
+ }
3322
+ flushHeld(lastUsage);
3323
+ finished = true;
3324
+ break;
3325
+ }
3326
+ case "agent_end":
3327
+ flushHeld(lastUsage);
3328
+ finished = true;
3329
+ if (!event.willRetry) settled = true;
3330
+ break;
3331
+ case "agent_settled":
3332
+ flushHeld(lastUsage);
3333
+ finished = true;
3334
+ settled = true;
3335
+ break;
3336
+ }
3337
+ if (settled) break;
3338
+ }
3339
+ flushHeld(lastUsage);
3340
+ if (!finished) {
3341
+ throw new LlmError3(
3342
+ `agent "${this.id}": pi query ended without an agent settle`,
3343
+ "PI_NO_RESULT"
3344
+ );
3345
+ }
3346
+ return { kind: "completed" };
3347
+ } finally {
3348
+ signal.removeEventListener("abort", cancel);
3349
+ controller.abort();
3350
+ }
3351
+ }
3352
+ /** Append one Pi tool result to the durable log as a `tool/result` message. */
3353
+ appendToolResult(turn, step, toolResult) {
3354
+ const text = typeof toolResult.content === "string" ? toolResult.content : toolResult.content.map((block) => block.type === "text" ? block.text : "").filter((segment) => segment !== "").join("\n\n");
3355
+ this.session.append("tool/result", {
3356
+ turn,
3357
+ step,
3358
+ message: mapToolResult({
3359
+ toolCallId: toolResult.toolCallId,
3360
+ result: { content: [{ type: "text", text }] },
3361
+ isError: toolResult.isError === true
3362
+ })
3363
+ }, { surfaceOp: "append" });
3364
+ }
3365
+ };
3366
+
3367
+ // src/engine-pi/loop.ts
3368
+ var PI_SANDBOX_MODES = [
3369
+ "read-only",
3370
+ "workspace-write",
3371
+ "danger-full-access"
3372
+ ];
3373
+ var PI_DISPOSE_GRACE_MS = 3e3;
3374
+ var Config3 = z3.object({
3375
+ sandboxMode: z3.union([...PI_SANDBOX_MODES]),
3376
+ provider: z3.string(),
3377
+ model: z3.string(),
3378
+ thinkingLevel: z3.string(),
3379
+ env: z3.dict(z3.string()).default({})
3380
+ });
3381
+ function resolveConfig3(config) {
3382
+ return {
3383
+ sandboxMode: config.sandboxMode,
3384
+ provider: config.provider,
3385
+ model: config.model,
3386
+ thinkingLevel: config.thinkingLevel,
3387
+ env: config.env ?? {}
3388
+ };
3389
+ }
3390
+ function piCliEntrypoint() {
3391
+ const mainUrl = import.meta.resolve("@earendil-works/pi-coding-agent");
3392
+ const root = dirname2(dirname2(fileURLToPath(mainUrl)));
3393
+ const pkg = JSON.parse(readFileSync(join2(root, "package.json"), "utf8"));
3394
+ const bin = pkg.bin;
3395
+ const rel = typeof bin === "string" ? bin : bin?.["pi"] ?? Object.values(bin ?? {})[0] ?? "bin/pi.js";
3396
+ return join2(root, rel);
3397
+ }
3398
+ function piSubprocessSpec(spec, graceMs) {
3399
+ return {
3400
+ // `spec.argv[0]` is the Pi CLI entrypoint; run it under the current node.
3401
+ argv: [process.execPath, ...spec.argv],
3402
+ cwd: spec.cwd,
3403
+ stdio: { stdin: "pipe", stdout: "pipe", stderr: "pipe" },
3404
+ graceMs,
3405
+ env: spec.env
3406
+ };
3407
+ }
3408
+ function fromSubprocess(handle) {
3409
+ const { stdin, stdout, stderr } = handle;
3410
+ if (stdin === void 0 || stdout === void 0 || stderr === void 0) {
3411
+ throw new Error("agent-loop-pi: spawned child must pipe stdin/stdout/stderr");
3412
+ }
3413
+ return {
3414
+ stdin,
3415
+ stdout,
3416
+ stderr,
3417
+ onExit: (handler) => {
3418
+ void handle.done.then(handler, handler);
3419
+ },
3420
+ terminate: () => handle.terminate()
3421
+ };
3422
+ }
3423
+ var PiLoop = class extends Service3 {
3424
+ /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
3425
+ static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
3426
+ /** Validated configuration owned by the loop plugin. */
3427
+ config;
3428
+ ownership;
3429
+ /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
3430
+ runtime;
3431
+ /** Process-tree spawn capability handed to every agent, sandboxed by the subprocess seam. */
3432
+ spawn;
3433
+ /** Resolved Pi CLI entrypoint; `argv[0]` of every Pi RPC child. */
3434
+ bin;
3435
+ constructor(ctx, config) {
3436
+ super(ctx, "agentLoopPi");
3437
+ this.config = resolveConfig3(config);
3438
+ this.ownership = new FactoryOwnership(ctx.fiber);
3439
+ this.runtime = { ctx };
3440
+ this.bin = piCliEntrypoint();
3441
+ this.spawn = (spec) => fromSubprocess(this.runtime.ctx.subprocess.spawn(piSubprocessSpec(spec, PI_DISPOSE_GRACE_MS)));
3442
+ ctx.effect(() => () => this.ownership.dispose(), "agentLoopPi.transactions()");
3443
+ ctx.effect(() => ctx.agents.setFactory(this), "agentLoopPi.setFactory()");
3444
+ ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
3445
+ ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
3446
+ ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
3447
+ }
3448
+ /**
3449
+ * Construct the driver, scope, and one memoized reverse teardown for a new
3450
+ * agent. The teardown is registered with the factory and the owner fiber
3451
+ * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
3452
+ * fuses caller cancellation with lifecycle teardown for setup awaits.
3453
+ */
3454
+ /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
3455
+ prepare(ownerCtx, id, options, session, callerSignal) {
3456
+ ownerCtx.fiber.assertActive();
3457
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
3458
+ if (callerSignal?.aborted) {
3459
+ throw callerSignal.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal.reason });
3460
+ }
3461
+ const loopCtx = this.runtime.ctx;
3462
+ const abort = new AbortController();
3463
+ const onCallerAbort = () => {
3464
+ abort.abort(callerSignal?.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal?.reason }));
3465
+ };
3466
+ const onFactoryTeardown = () => {
3467
+ abort.abort(this.ownership.signal.reason);
3468
+ };
3469
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
3470
+ this.ownership.signal.addEventListener("abort", onFactoryTeardown, { once: true });
3471
+ let machine;
3472
+ let detachSession;
3473
+ let detachAgent;
3474
+ let disposing;
3475
+ const machineReady = Promise.withResolvers();
3476
+ const dispose = (ownerTriggered = false) => disposing ??= (async () => {
3477
+ abort.abort(new Error(`agent "${id}" lifecycle disposed`));
3478
+ callerSignal?.removeEventListener("abort", onCallerAbort);
3479
+ this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
3480
+ try {
3481
+ if (machine === void 0) await machineReady.promise;
3482
+ if (machine !== void 0) {
3483
+ machine.cancel({ kind: "disposed" });
3484
+ await machine.whenIdle();
3485
+ await machine.scope.dispose();
3486
+ }
3487
+ } finally {
3488
+ try {
3489
+ detachAgent?.();
3490
+ detachSession?.();
3491
+ } finally {
3492
+ untrack();
3493
+ if (!ownerTriggered) await unfollowOwner();
3494
+ }
3495
+ }
3496
+ })();
3497
+ const untrack = this.ownership.track(dispose);
3498
+ let unfollowOwner;
3499
+ try {
3500
+ unfollowOwner = ownerCtx.effect(() => () => {
3501
+ if (disposing !== void 0) return;
3502
+ abort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
3503
+ return dispose(true);
3504
+ }, `agentLoopPi.lifecycle(${id})`);
3505
+ } catch (error) {
3506
+ untrack();
3507
+ callerSignal?.removeEventListener("abort", onCallerAbort);
3508
+ this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
3509
+ throw error;
3510
+ }
3511
+ const assertLive = () => {
3512
+ if (!abort.signal.aborted) return;
3513
+ throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
3514
+ };
3515
+ try {
3516
+ const agent = machine = new PiAgent(loopCtx, id, options, session, this.config, this.spawn, this.bin);
3517
+ machineReady.resolve();
3518
+ assertLive();
3519
+ return {
3520
+ agent,
3521
+ signal: abort.signal,
3522
+ publish: (source) => {
3523
+ assertLive();
3524
+ detachSession = agent.ctx.sessions.enter(session);
3525
+ detachAgent = loopCtx.agents.enter(agent, ownerCtx.agent);
3526
+ agent.ctx.sessions.announce(session);
3527
+ assertLive();
3528
+ loopCtx.agents.announce(agent);
3529
+ assertLive();
3530
+ emitAgentEvent3(loopCtx, agent, "agent/session-start", { source });
3531
+ assertLive();
3532
+ return { agent, dispose };
3533
+ },
3534
+ dispose
3535
+ };
3536
+ } catch (error) {
3537
+ machineReady.resolve();
3538
+ void dispose();
3539
+ throw error;
3540
+ }
3541
+ }
3542
+ /** Prepare one Agent around an acquired Session, run setup, and publish it. */
3543
+ async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source) {
3544
+ var _stack = [];
3545
+ try {
3546
+ const ownedPreparation = __using(_stack, preparation);
3547
+ const session = ownedPreparation.session;
3548
+ const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal);
3549
+ try {
3550
+ const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id);
3551
+ setupCommit?.commit();
3552
+ return prepared.publish(source);
3553
+ } catch (error) {
3554
+ await prepared.dispose();
3555
+ throw error;
3556
+ }
3557
+ } catch (_) {
3558
+ var _error = _, _hasError = true;
3559
+ } finally {
3560
+ __callDispose(_stack, _error, _hasError);
3561
+ }
3562
+ }
3563
+ /**
3564
+ * Create an agent and session under one caller-supplied identity, owned by
3565
+ * the accessing fiber.
3566
+ * @param ownerCtx - caller context that structurally owns the lifecycle.
3567
+ * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
3568
+ * @returns the published handle.
3569
+ */
3570
+ async createAgent(ownerCtx, options) {
3571
+ const preparation = SessionPreparation3.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
3572
+ ...options.seed === void 0 ? {} : { seed: options.seed },
3573
+ ...options.meta === void 0 ? {} : { meta: options.meta }
3574
+ }));
3575
+ const published = this.setupAndPublish(
3576
+ ownerCtx,
3577
+ options.sessionId,
3578
+ preparation,
3579
+ options.agentOptions ?? {},
3580
+ options.setup,
3581
+ options.signal,
3582
+ "startup"
3583
+ );
3584
+ this.ownership.trackWrapper(published);
3585
+ return published;
3586
+ }
3587
+ /**
3588
+ * Resume an owned agent from the configured persistence service.
3589
+ * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
3590
+ * @param options - persisted identity, loop options, setup, and cancellation.
3591
+ * @returns the published handle.
3592
+ */
3593
+ async resume(ownerCtx, options) {
3594
+ const persistence = this.runtime.ctx.get("sessionPersistence");
3595
+ if (persistence === void 0) {
3596
+ throw new Error("cannot resume: session persistence is not configured (load a dsh-session-persistence backend)");
3597
+ }
3598
+ return this.resumeWith(ownerCtx, persistence, options);
3599
+ }
3600
+ /** Resume through an explicit persistence handle. */
3601
+ async resumeWith(ownerCtx, persistence, options) {
3602
+ const id = options.resumeSessionId;
3603
+ let preparation;
3604
+ try {
3605
+ const ownerAbort = new AbortController();
3606
+ const unfollowOwner = ownerCtx.effect(() => () => {
3607
+ ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
3608
+ }, `agentLoopPi.resume-load(${id})`);
3609
+ const fused = AbortSignal.any([
3610
+ ...options.signal === void 0 ? [] : [options.signal],
3611
+ ownerAbort.signal,
3612
+ this.ownership.signal
3613
+ ]);
3614
+ try {
3615
+ preparation = await raceAbortCall(
3616
+ () => persistence.prepare(id, fused),
3617
+ fused,
3618
+ id,
3619
+ (abandoned) => {
3620
+ abandoned[Symbol.dispose]();
3621
+ }
3622
+ );
3623
+ } finally {
3624
+ await unfollowOwner();
3625
+ }
3626
+ ownerCtx.fiber.assertActive();
3627
+ if (!this.ownership.isActive()) throw new Error("agent loop is not active");
3628
+ return await this.setupAndPublish(
3629
+ ownerCtx,
3630
+ id,
3631
+ preparation,
3632
+ options.agentOptions ?? {},
3633
+ options.setup,
3634
+ options.signal,
3635
+ "resume"
3636
+ );
3637
+ } finally {
3638
+ preparation?.[Symbol.dispose]();
3639
+ }
3640
+ }
3641
+ };
3642
+
3643
+ // src/settings.ts
3644
+ import z4 from "@deepseek-ai/schemastery";
3645
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
3646
+
3647
+ // src/namespace.ts
3648
+ var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
3649
+
3650
+ // src/settings.ts
3651
+ var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi"];
3652
+ var LOOP_ENGINE_SETTINGS_SCHEMA = z4.object({
3653
+ engine: z4.union([z4.const("in-process"), z4.const("claude-code"), z4.const("codex"), z4.const("pi")]).default("in-process")
3654
+ });
3655
+ function loopEngineSettingsNamespace() {
3656
+ return settingsNamespace(LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL);
3657
+ }
3658
+
3659
+ // src/patch-manager.ts
3660
+ var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block: ";
3661
+ var MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --";
3662
+ var END_MARKER_LINE = `${MANAGED_BLOCK_END}
3663
+ `;
3664
+ function renderManagedBlock(engine) {
3665
+ if (engine === "in-process") return "";
3666
+ return [
3667
+ `${MANAGED_BLOCK_BEGIN}${engine} --`,
3668
+ "- id: agent-loop",
3669
+ " disabled: true",
3670
+ END_MARKER_LINE
3671
+ ].join("\n");
3672
+ }
3673
+ var BEGIN_MARKER_RE = /^# -- dsh-loop-engine managed block: (\S+) --$/m;
3674
+ function currentEngineOf(text) {
3675
+ const engine = BEGIN_MARKER_RE.exec(text)?.[1];
3676
+ return LOOP_ENGINE_IDS.includes(engine ?? "") ? engine : "in-process";
3677
+ }
3678
+ function managedSpan(text) {
3679
+ const begin = text.indexOf(MANAGED_BLOCK_BEGIN);
3680
+ if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
3681
+ const afterBegin = begin + MANAGED_BLOCK_BEGIN.length;
3682
+ const endAt = text.indexOf(MANAGED_BLOCK_END, afterBegin);
3683
+ const spanEnd = endAt === -1 ? text.length : endAt + END_MARKER_LINE.length;
3684
+ const before = text.slice(0, begin);
3685
+ const blankBefore = before.endsWith("\n\n");
3686
+ return {
3687
+ head: blankBefore ? before.slice(0, -1) : before,
3688
+ tail: text.slice(spanEnd),
3689
+ present: true,
3690
+ blankBefore
3691
+ };
3692
+ }
3693
+ function ensureTrailingNewline(text) {
3694
+ return text.endsWith("\n") ? text : `${text}
3695
+ `;
3696
+ }
3697
+ function applyManagedBlock(text, engine) {
3698
+ const block = renderManagedBlock(engine);
3699
+ const span = managedSpan(text);
3700
+ if (!span.present) {
3701
+ if (block === "") return text;
3702
+ const base = ensureTrailingNewline(text);
3703
+ return `${base}
3704
+ ${block}`;
3705
+ }
3706
+ if (block === "") {
3707
+ return span.tail.startsWith("\n") ? `${span.head}${span.tail.slice(1)}` : `${span.head}${span.tail}`;
3708
+ }
3709
+ return `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
3710
+ }
3711
+
3712
+ // src/commands.ts
3713
+ var CLAUDE_CODE_COMMANDS = [
3714
+ {
3715
+ name: "help",
3716
+ description: "Show help about Claude Code commands",
3717
+ handler: async () => ({ kind: "success" })
3718
+ },
3719
+ {
3720
+ name: "compact",
3721
+ description: "Compact the conversation to reduce context usage",
3722
+ handler: async () => ({ kind: "success" })
3723
+ },
3724
+ {
3725
+ name: "clear",
3726
+ description: "Clear the conversation and start fresh",
3727
+ handler: async () => ({ kind: "success" })
3728
+ },
3729
+ {
3730
+ name: "review",
3731
+ description: "Review recent changes (git diff)",
3732
+ handler: async () => ({ kind: "success" })
3733
+ },
3734
+ {
3735
+ name: "explain",
3736
+ description: "Explain the selected code",
3737
+ handler: async () => ({ kind: "success" })
3738
+ },
3739
+ {
3740
+ name: "fix",
3741
+ description: "Fix issues in the code",
3742
+ handler: async () => ({ kind: "success" })
3743
+ },
3744
+ {
3745
+ name: "tests",
3746
+ description: "Add tests for the selected code",
3747
+ handler: async () => ({ kind: "success" })
3748
+ }
3749
+ ];
3750
+
3751
+ // src/skills.ts
3752
+ import { readFile, readdir, stat } from "node:fs/promises";
3753
+ import { homedir } from "node:os";
3754
+ import { join as join3, resolve } from "node:path";
3755
+ var SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3756
+ var PROVIDER_NAME = "claude-code";
3757
+ var CLAUDE_CODE_RANK = 150;
3758
+ var CLAUDE_CODE_USER_RANK = 160;
3759
+ function parseFrontmatter(raw) {
3760
+ const firstLineEnd = raw.indexOf("\n");
3761
+ if (firstLineEnd < 0) return void 0;
3762
+ const firstLine = raw.slice(0, firstLineEnd).replace(/\r$/, "");
3763
+ if (firstLine !== "---") return void 0;
3764
+ const start = firstLineEnd + 1;
3765
+ const closing = findClosingFrontmatter(raw, start);
3766
+ if (closing === void 0) return void 0;
3767
+ const yaml = raw.slice(start, closing.start);
3768
+ const data = {};
3769
+ const lines = yaml.split("\n");
3770
+ for (let index = 0; index < lines.length; index += 1) {
3771
+ const line = lines[index] ?? "";
3772
+ const trimmed = line.trim();
3773
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
3774
+ const colon = trimmed.indexOf(":");
3775
+ if (colon < 0) continue;
3776
+ const key = trimmed.slice(0, colon).trim();
3777
+ const value = trimmed.slice(colon + 1).trim();
3778
+ if (key.length === 0) continue;
3779
+ if (value === ">" || value === ">-" || value === "|" || value === "|-") {
3780
+ const block = [];
3781
+ while (index + 1 < lines.length) {
3782
+ const next = lines[index + 1] ?? "";
3783
+ if (next.trim().length > 0 && !/^[\s]/.test(next)) break;
3784
+ index += 1;
3785
+ if (next.trim().length > 0) block.push(next.trim());
3786
+ }
3787
+ data[key] = value.startsWith("|") ? block.join("\n") : block.join(" ");
3788
+ continue;
3789
+ }
3790
+ data[key] = unquote(value);
3791
+ }
3792
+ return { data, body: raw.slice(closing.bodyStart) };
3793
+ }
3794
+ function unquote(value) {
3795
+ if (value.length >= 2) {
3796
+ const first = value[0];
3797
+ const last = value[value.length - 1];
3798
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
3799
+ return value.slice(1, -1);
3800
+ }
3801
+ }
3802
+ return value;
3803
+ }
3804
+ function findClosingFrontmatter(raw, start) {
3805
+ let lineStart = start;
3806
+ while (lineStart <= raw.length) {
3807
+ const nextNewline = raw.indexOf("\n", lineStart);
3808
+ const lineEnd = nextNewline < 0 ? raw.length : nextNewline;
3809
+ const line = raw.slice(lineStart, lineEnd).replace(/\r$/, "");
3810
+ if (line === "---") {
3811
+ return { start: lineStart, bodyStart: nextNewline < 0 ? raw.length : nextNewline + 1 };
3812
+ }
3813
+ if (nextNewline < 0) return void 0;
3814
+ lineStart = nextNewline + 1;
3815
+ }
3816
+ return void 0;
3817
+ }
3818
+ function parseSkillFile(raw) {
3819
+ const parsed = parseFrontmatter(raw);
3820
+ if (parsed === void 0) return void 0;
3821
+ const name2 = stringField(parsed.data, "name");
3822
+ const description = stringField(parsed.data, "description");
3823
+ if (name2 === void 0 || description === void 0 || !SKILL_NAME.test(name2)) return void 0;
3824
+ const disableModelInvocation = booleanField(parsed.data, "disable-model-invocation");
3825
+ const userInvocable = booleanField(parsed.data, "user-invocable");
3826
+ const whenToUse = optionalString(parsed.data, "whenToUse");
3827
+ return {
3828
+ name: name2,
3829
+ description,
3830
+ ...whenToUse !== void 0 ? { whenToUse } : {},
3831
+ invocation: {
3832
+ modelInvocable: disableModelInvocation !== true,
3833
+ userInvocable: userInvocable !== false
3834
+ },
3835
+ content: parsed.body.trim()
3836
+ };
3837
+ }
3838
+ function stringField(data, key) {
3839
+ const value = data[key];
3840
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3841
+ }
3842
+ function optionalString(data, key) {
3843
+ const value = data[key];
3844
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3845
+ }
3846
+ function booleanField(data, key) {
3847
+ const value = data[key];
3848
+ if (typeof value !== "string") return void 0;
3849
+ if (value === "true" || value === "yes") return true;
3850
+ if (value === "false" || value === "no") return false;
3851
+ return void 0;
3852
+ }
3853
+ var ClaudeCodeSkillProvider = class {
3854
+ constructor(control) {
3855
+ this.control = control;
3856
+ }
3857
+ control;
3858
+ name = PROVIDER_NAME;
3859
+ async list(options) {
3860
+ const candidates = [];
3861
+ const cwd = options.cwd;
3862
+ if (cwd !== void 0) {
3863
+ const projectRoot = await findProjectRoot(resolve(cwd));
3864
+ await collectSkillsDir(join3(projectRoot, ".claude", "skills"), CLAUDE_CODE_RANK, candidates);
3865
+ await collectClaudeMd(projectRoot, candidates);
3866
+ }
3867
+ await collectSkillsDir(join3(homedir(), ".claude", "skills"), CLAUDE_CODE_USER_RANK, candidates);
3868
+ if (this.control.signal.aborted) return [];
3869
+ return candidates;
3870
+ }
3871
+ async get(candidate, _options) {
3872
+ const locator = candidate.locator;
3873
+ try {
3874
+ const raw = await readFile(locator.path, { encoding: "utf8" });
3875
+ const parsed = parseSkillFile(raw);
3876
+ if (parsed === void 0) return void 0;
3877
+ return {
3878
+ name: parsed.name,
3879
+ description: parsed.description,
3880
+ ...parsed.whenToUse !== void 0 ? { whenToUse: parsed.whenToUse } : {},
3881
+ invocation: parsed.invocation,
3882
+ source: "custom",
3883
+ provider: this.name,
3884
+ content: parsed.content,
3885
+ path: locator.path,
3886
+ ...candidate.resourceBase !== void 0 ? { resourceBase: candidate.resourceBase } : {}
3887
+ };
3888
+ } catch {
3889
+ return void 0;
3890
+ }
3891
+ }
3892
+ };
3893
+ async function collectSkillsDir(skillsDir, rank, candidates) {
3894
+ let entries;
3895
+ try {
3896
+ entries = await readdir(skillsDir, { withFileTypes: true, encoding: "utf8" });
3897
+ } catch {
3898
+ return;
3899
+ }
3900
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3901
+ const entryPath = join3(skillsDir, entry.name);
3902
+ const info = await stat(entryPath).catch(() => void 0);
3903
+ if (info === void 0) continue;
3904
+ if (info.isDirectory()) {
3905
+ const path = join3(entryPath, "SKILL.md");
3906
+ const skill2 = await tryParseSkill(path);
3907
+ if (skill2 === void 0) continue;
3908
+ candidates.push(toCandidate(skill2, path, rank, entryPath));
3909
+ continue;
3910
+ }
3911
+ if (!entry.name.endsWith(".md")) continue;
3912
+ const skill = await tryParseSkill(entryPath);
3913
+ if (skill === void 0) continue;
3914
+ candidates.push(toCandidate(skill, entryPath, rank, skillsDir));
3915
+ }
3916
+ }
3917
+ async function collectClaudeMd(projectRoot, candidates) {
3918
+ const claudeMd = join3(projectRoot, "CLAUDE.md");
3919
+ try {
3920
+ const info = await stat(claudeMd);
3921
+ if (!info.isFile()) return;
3922
+ const skill = await tryParseSkill(claudeMd);
3923
+ if (skill !== void 0) {
3924
+ candidates.push(toCandidate(skill, claudeMd, CLAUDE_CODE_RANK, projectRoot));
3925
+ }
3926
+ } catch {
3927
+ }
3928
+ }
3929
+ function toCandidate(skill, path, rank, resourceBaseDir) {
3930
+ return {
3931
+ name: skill.name,
3932
+ description: skill.description,
3933
+ ...skill.whenToUse !== void 0 ? { whenToUse: skill.whenToUse } : {},
3934
+ invocation: skill.invocation,
3935
+ source: "custom",
3936
+ provider: PROVIDER_NAME,
3937
+ rank,
3938
+ locator: { kind: "file", path },
3939
+ path,
3940
+ resourceBase: { kind: "directory", path: resourceBaseDir }
3941
+ };
3942
+ }
3943
+ async function tryParseSkill(path) {
3944
+ try {
3945
+ const raw = await readFile(path, { encoding: "utf8" });
3946
+ return parseSkillFile(raw);
3947
+ } catch {
3948
+ return void 0;
3949
+ }
3950
+ }
3951
+ async function findProjectRoot(cwd) {
3952
+ let current = cwd;
3953
+ while (true) {
3954
+ try {
3955
+ await stat(join3(current, ".git"));
3956
+ return current;
3957
+ } catch {
3958
+ }
3959
+ const parent = resolve(current, "..");
3960
+ if (parent === current) return cwd;
3961
+ current = parent;
3962
+ }
3963
+ }
3964
+
3965
+ // src/engine-codex/skills.ts
3966
+ import { readFile as readFile2 } from "node:fs/promises";
3967
+ import { homedir as homedir2 } from "node:os";
3968
+ import { join as join4, resolve as resolve2 } from "node:path";
3969
+ var PROVIDER_NAME2 = "codex";
3970
+ var CODEX_PROJECT_RANK = 140;
3971
+ var CODEX_USER_RANK = 160;
3972
+ var CodexSkillProvider = class {
3973
+ constructor(control) {
3974
+ this.control = control;
3975
+ }
3976
+ control;
3977
+ name = PROVIDER_NAME2;
3978
+ async list(options) {
3979
+ const candidates = [];
3980
+ const cwd = options.cwd;
3981
+ if (cwd !== void 0) {
3982
+ const projectRoot = await findProjectRoot(resolve2(cwd));
3983
+ await this.collectAgentsMd(join4(projectRoot, "AGENTS.md"), CODEX_PROJECT_RANK, candidates);
3984
+ }
3985
+ await this.collectAgentsMd(join4(homedir2(), ".codex", "AGENTS.md"), CODEX_USER_RANK, candidates);
3986
+ if (this.control.signal.aborted) return [];
3987
+ return candidates;
3988
+ }
3989
+ async get(candidate, _options) {
3990
+ const locator = candidate.locator;
3991
+ try {
3992
+ const content = await readFile2(locator.path, { encoding: "utf8" });
3993
+ return {
3994
+ name: candidate.name,
3995
+ description: candidate.description,
3996
+ invocation: candidate.invocation,
3997
+ source: candidate.source,
3998
+ provider: this.name,
3999
+ content,
4000
+ path: locator.path,
4001
+ /* v8 ignore next -- every candidate from collectAgentsMd carries a resourceBase */
4002
+ ...candidate.resourceBase !== void 0 ? { resourceBase: candidate.resourceBase } : {}
4003
+ };
4004
+ } catch {
4005
+ return void 0;
4006
+ }
4007
+ }
4008
+ /** Read one AGENTS.md file and push a candidate when it exists. */
4009
+ async collectAgentsMd(path, rank, candidates) {
4010
+ try {
4011
+ const content = await readFile2(path, { encoding: "utf8" });
4012
+ if (content.trim().length === 0) return;
4013
+ candidates.push({
4014
+ name: "agents-md",
4015
+ description: "Codex project/user instructions (AGENTS.md)",
4016
+ invocation: { modelInvocable: true, userInvocable: true },
4017
+ source: "custom",
4018
+ provider: this.name,
4019
+ rank,
4020
+ locator: { kind: "agents-md", path },
4021
+ path,
4022
+ resourceBase: { kind: "file", path }
4023
+ });
4024
+ } catch {
4025
+ }
4026
+ }
4027
+ };
4028
+
4029
+ // src/engine-pi/skills.ts
4030
+ import { readFile as readFile3 } from "node:fs/promises";
4031
+ import { homedir as homedir3 } from "node:os";
4032
+ import { join as join5, resolve as resolve3 } from "node:path";
4033
+ var PROVIDER_NAME3 = "pi";
4034
+ var PI_PROJECT_RANK = 140;
4035
+ var PI_USER_RANK = 160;
4036
+ var PiSkillProvider = class {
4037
+ constructor(control) {
4038
+ this.control = control;
4039
+ }
4040
+ control;
4041
+ name = PROVIDER_NAME3;
4042
+ async list(options) {
4043
+ const candidates = [];
4044
+ const cwd = options.cwd;
4045
+ if (cwd !== void 0) {
4046
+ const projectRoot = await findProjectRoot(resolve3(cwd));
4047
+ await this.collectAgentsMd(join5(projectRoot, "AGENTS.md"), PI_PROJECT_RANK, candidates);
4048
+ }
4049
+ await this.collectAgentsMd(join5(homedir3(), ".pi", "AGENTS.md"), PI_USER_RANK, candidates);
4050
+ if (this.control.signal.aborted) return [];
4051
+ return candidates;
4052
+ }
4053
+ async get(candidate, _options) {
4054
+ const locator = candidate.locator;
4055
+ try {
4056
+ const content = await readFile3(locator.path, { encoding: "utf8" });
4057
+ return {
4058
+ name: candidate.name,
4059
+ description: candidate.description,
4060
+ invocation: candidate.invocation,
4061
+ source: candidate.source,
4062
+ provider: this.name,
4063
+ content,
4064
+ path: locator.path,
4065
+ /* v8 ignore next -- every candidate from collectAgentsMd carries a resourceBase */
4066
+ ...candidate.resourceBase !== void 0 ? { resourceBase: candidate.resourceBase } : {}
4067
+ };
4068
+ } catch {
4069
+ return void 0;
4070
+ }
4071
+ }
4072
+ /** Read one AGENTS.md file and push a candidate when it exists. */
4073
+ async collectAgentsMd(path, rank, candidates) {
4074
+ try {
4075
+ const content = await readFile3(path, { encoding: "utf8" });
4076
+ if (content.trim().length === 0) return;
4077
+ candidates.push({
4078
+ name: "agents-md",
4079
+ description: "Pi project/user instructions (AGENTS.md)",
4080
+ invocation: { modelInvocable: true, userInvocable: true },
4081
+ source: "custom",
4082
+ provider: this.name,
4083
+ rank,
4084
+ locator: { kind: "agents-md", path },
4085
+ path,
4086
+ resourceBase: { kind: "file", path }
4087
+ });
4088
+ } catch {
4089
+ }
4090
+ }
4091
+ };
4092
+
4093
+ // src/index.ts
4094
+ var name = "loop-engine";
4095
+ var inject = [];
4096
+ var MAX_MOUNT_ATTEMPTS = 40;
4097
+ var MOUNT_RETRY_MS = 50;
4098
+ var Config4 = z5.object({
4099
+ profile: z5.string(),
4100
+ patchFilename: z5.string(),
4101
+ patchPath: z5.string(),
4102
+ permissionMode: z5.union(CLAUDE_CODE_PERMISSION_MODES.map((mode) => z5.const(mode))),
4103
+ env: z5.dict(z5.string()),
4104
+ model: z5.string(),
4105
+ disposeGraceMs: z5.number(),
4106
+ maxTurns: z5.number(),
4107
+ sandboxMode: z5.union(CODEX_SANDBOX_MODES.map((mode) => z5.const(mode))),
4108
+ approvalPolicy: z5.union(CODEX_APPROVAL_POLICIES.map((policy) => z5.const(policy))),
4109
+ piProvider: z5.string(),
4110
+ piThinking: z5.string()
4111
+ });
4112
+ function resolvePatchPath(config) {
4113
+ if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
4114
+ return join6(
4115
+ resolveDshHome(),
4116
+ "profiles",
4117
+ config.profile ?? "web",
4118
+ config.patchFilename ?? "cordis.patch.yml"
4119
+ );
4120
+ }
4121
+ function isMissing(error) {
4122
+ return error?.code === "ENOENT";
4123
+ }
4124
+ async function readPatchOrUndefined(path) {
4125
+ try {
4126
+ return await readFile4(path, "utf8");
4127
+ } catch (error) {
4128
+ if (isMissing(error)) return void 0;
4129
+ throw error;
4130
+ }
4131
+ }
4132
+ async function writePatchFile(path, text) {
4133
+ await mkdir(dirname3(path), { recursive: true });
4134
+ const tmp = `${path}.tmp-${randomUUID()}`;
4135
+ await writeFile(tmp, text, "utf8");
4136
+ await rename(tmp, path);
4137
+ }
4138
+ function writePatchFileSync(path, text) {
4139
+ mkdirSync(dirname3(path), { recursive: true });
4140
+ const tmp = `${path}.tmp-${randomUUID()}`;
4141
+ writeFileSync(tmp, text, "utf8");
4142
+ renameSync(tmp, path);
4143
+ }
4144
+ async function syncManagedBlock(path, engine) {
4145
+ const current = await readPatchOrUndefined(path);
4146
+ if (current !== void 0 && currentEngineOf(current) === engine) return false;
4147
+ const next = applyManagedBlock(current ?? "", engine);
4148
+ await writePatchFile(path, next);
4149
+ return true;
4150
+ }
4151
+ function readPatchFileSync(path) {
4152
+ try {
4153
+ return readFileSync2(path, "utf8");
4154
+ } catch (error) {
4155
+ if (isMissing(error)) return "";
4156
+ throw error;
4157
+ }
4158
+ }
4159
+ function claudeCodeConfig(config) {
4160
+ return {
4161
+ ...config.permissionMode === void 0 ? {} : { permissionMode: config.permissionMode },
4162
+ ...config.env === void 0 ? {} : { env: config.env },
4163
+ ...config.model === void 0 ? {} : { model: config.model },
4164
+ ...config.disposeGraceMs === void 0 ? {} : { disposeGraceMs: config.disposeGraceMs },
4165
+ ...config.maxTurns === void 0 ? {} : { maxTurns: config.maxTurns }
4166
+ };
4167
+ }
4168
+ function codexConfig(config) {
4169
+ return {
4170
+ ...config.sandboxMode === void 0 ? {} : { sandboxMode: config.sandboxMode },
4171
+ ...config.approvalPolicy === void 0 ? {} : { approvalPolicy: config.approvalPolicy },
4172
+ ...config.env === void 0 ? {} : { env: config.env },
4173
+ ...config.model === void 0 ? {} : { model: config.model }
4174
+ };
4175
+ }
4176
+ function piConfig(config) {
4177
+ return {
4178
+ ...config.piProvider === void 0 ? {} : { provider: config.piProvider },
4179
+ ...config.model === void 0 ? {} : { model: config.model },
4180
+ ...config.piThinking === void 0 ? {} : { thinkingLevel: config.piThinking },
4181
+ ...config.env === void 0 ? {} : { env: config.env },
4182
+ ...config.sandboxMode === void 0 ? {} : { sandboxMode: config.sandboxMode }
4183
+ };
4184
+ }
4185
+ function apply(ctx, config) {
4186
+ const patchPath = resolvePatchPath(config);
4187
+ let fileEngine = currentEngineOf(readPatchFileSync(patchPath));
4188
+ let engineFiber;
4189
+ let mountedEngine;
4190
+ let commandDisposers;
4191
+ let skillDisposer;
4192
+ let mountAttempts = 0;
4193
+ let mountRetry;
4194
+ const CLEAR_RETRY = () => {
4195
+ if (mountRetry !== void 0) {
4196
+ clearTimeout(mountRetry);
4197
+ mountRetry = void 0;
4198
+ }
4199
+ };
4200
+ const cleanupEngineRegistrations = () => {
4201
+ if (commandDisposers !== void 0) {
4202
+ for (const dispose of commandDisposers) dispose();
4203
+ commandDisposers = void 0;
4204
+ }
4205
+ if (skillDisposer !== void 0) {
4206
+ skillDisposer();
4207
+ skillDisposer = void 0;
4208
+ }
4209
+ };
4210
+ const hostFactory = (engine, mount) => {
4211
+ if (engineFiber !== void 0) return;
4212
+ const fiber = mount();
4213
+ engineFiber = fiber;
4214
+ mountedEngine = engine;
4215
+ void fiber.then(() => void 0, (error) => {
4216
+ cleanupEngineRegistrations();
4217
+ engineFiber = void 0;
4218
+ mountedEngine = void 0;
4219
+ if (error instanceof Error && error.message.includes("an agent factory is already registered") && mountAttempts < MAX_MOUNT_ATTEMPTS) {
4220
+ mountAttempts += 1;
4221
+ mountRetry = setTimeout(() => {
4222
+ mountEngine(engine);
4223
+ }, MOUNT_RETRY_MS);
4224
+ return;
4225
+ }
4226
+ ctx.logger.error(`loop-engine: ${engine} factory failed to start: ${String(error)}`);
4227
+ });
4228
+ };
4229
+ const mountClaude = () => {
4230
+ if (engineFiber !== void 0) return;
4231
+ const commands = ctx.get("commands");
4232
+ if (commands !== void 0) {
4233
+ const disposers = [];
4234
+ for (const cmd of CLAUDE_CODE_COMMANDS) {
4235
+ disposers.push(commands.register(cmd));
4236
+ }
4237
+ commandDisposers = disposers;
4238
+ }
4239
+ const skills = ctx.get("skills");
4240
+ if (skills !== void 0) {
4241
+ skillDisposer = skills.registerProvider((control) => new ClaudeCodeSkillProvider(control));
4242
+ }
4243
+ hostFactory("claude-code", () => ctx.plugin(ClaudeCodeLoop, claudeCodeConfig(config)));
4244
+ };
4245
+ const mountCodex = () => {
4246
+ const skills = ctx.get("skills");
4247
+ if (skills !== void 0) {
4248
+ skillDisposer = skills.registerProvider((control) => new CodexSkillProvider(control));
4249
+ }
4250
+ hostFactory("codex", () => ctx.plugin(CodexLoop, codexConfig(config)));
4251
+ };
4252
+ const mountPi = () => {
4253
+ const skills = ctx.get("skills");
4254
+ if (skills !== void 0) {
4255
+ skillDisposer = skills.registerProvider((control) => new PiSkillProvider(control));
4256
+ }
4257
+ hostFactory("pi", () => ctx.plugin(PiLoop, piConfig(config)));
4258
+ };
4259
+ const mountEngine = (engine) => {
4260
+ if (engine === "claude-code") mountClaude();
4261
+ else if (engine === "codex") mountCodex();
4262
+ else if (engine === "pi") mountPi();
4263
+ };
4264
+ const unmountEngine = () => {
4265
+ const fiber = engineFiber;
4266
+ mountAttempts = 0;
4267
+ CLEAR_RETRY();
4268
+ cleanupEngineRegistrations();
4269
+ mountedEngine = void 0;
4270
+ if (fiber === void 0) return;
4271
+ engineFiber = void 0;
4272
+ void fiber.then((resolved) => {
4273
+ void resolved.dispose();
4274
+ }, () => void 0);
4275
+ };
4276
+ mountEngine(fileEngine);
4277
+ ctx.effect(() => () => CLEAR_RETRY(), "loop-engine: mount retry cleanup");
4278
+ let source;
4279
+ installSettingsSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine }, {
4280
+ setSource: (current) => {
4281
+ source = current;
4282
+ },
4283
+ onChange: () => {
4284
+ const next = source().engine;
4285
+ if (next === fileEngine) return;
4286
+ if (mountedEngine !== next) {
4287
+ unmountEngine();
4288
+ mountEngine(next);
4289
+ }
4290
+ try {
4291
+ const updated = applyManagedBlock(readPatchFileSync(patchPath), next);
4292
+ writePatchFileSync(patchPath, updated);
4293
+ fileEngine = next;
4294
+ } catch (error) {
4295
+ ctx.logger.error(`loop-engine: managed block write failed: ${String(error)}`);
4296
+ }
4297
+ }
4298
+ });
4299
+ }
4300
+ export {
4301
+ Config4 as Config,
4302
+ apply,
4303
+ inject,
4304
+ name,
4305
+ resolvePatchPath,
4306
+ syncManagedBlock,
4307
+ writePatchFile,
4308
+ writePatchFileSync
4309
+ };
4310
+ //# sourceMappingURL=index.js.map