dsh-loop-engine 0.1.5-rc1 → 0.1.5-rc3

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 (35) hide show
  1. package/README.md +112 -19
  2. package/README.zh.md +85 -5
  3. package/lib/client.js +158 -0
  4. package/lib/index.js +1020 -1486
  5. package/lib/invariant.js +6 -3
  6. package/lib/types/client/turn-status.d.ts +43 -0
  7. package/lib/types/driver-core/agents-md-skill-provider.d.ts +72 -0
  8. package/lib/types/driver-core/hosted-loop-factory.d.ts +119 -0
  9. package/lib/types/driver-core/ownership.d.ts +6 -6
  10. package/lib/types/driver-core/permission-knobs.d.ts +1 -1
  11. package/lib/types/driver-core/prompt.d.ts +1 -1
  12. package/lib/types/driver-core/skill-inject.d.ts +2 -2
  13. package/lib/types/engine-claude/agent.d.ts +22 -0
  14. package/lib/types/engine-claude/loop.d.ts +7 -56
  15. package/lib/types/engine-claude/mapping.d.ts +28 -3
  16. package/lib/types/engine-codex/agent.d.ts +46 -3
  17. package/lib/types/engine-codex/appserver/client.d.ts +16 -0
  18. package/lib/types/engine-codex/loop.d.ts +7 -56
  19. package/lib/types/engine-codex/permission.d.ts +100 -6
  20. package/lib/types/engine-codex/skills.d.ts +7 -8
  21. package/lib/types/engine-kimi/acp/client.d.ts +33 -3
  22. package/lib/types/engine-kimi/acp/mapping.d.ts +36 -7
  23. package/lib/types/engine-kimi/acp/types.d.ts +41 -2
  24. package/lib/types/engine-kimi/agent.d.ts +65 -8
  25. package/lib/types/engine-kimi/loop.d.ts +7 -56
  26. package/lib/types/engine-kimi/skills.d.ts +9 -14
  27. package/lib/types/engine-pi/agent.d.ts +23 -4
  28. package/lib/types/engine-pi/loop.d.ts +7 -56
  29. package/lib/types/engine-pi/permission.d.ts +16 -12
  30. package/lib/types/engine-pi/skills.d.ts +6 -14
  31. package/lib/types/patch-manager.d.ts +7 -0
  32. package/lib/types/provider-route.d.ts +16 -7
  33. package/lib/types/settings.d.ts +4 -1
  34. package/lib/types/skills.d.ts +6 -14
  35. package/package.json +22 -22
package/lib/index.js CHANGED
@@ -46,22 +46,25 @@ var __callDispose = (stack, error, hasError) => {
46
46
 
47
47
  // src/index.ts
48
48
  import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
49
- import { mkdir as mkdir2, readFile as readFile6, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
49
+ import { mkdir as mkdir2, readFile as readFile5, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
50
50
  import { randomUUID as randomUUID2 } from "node:crypto";
51
- import { dirname as dirname6, join as join11 } from "node:path";
51
+ import { dirname as dirname5, join as join12 } from "node:path";
52
52
  import z6 from "@deepseek-ai/schemastery";
53
53
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
54
54
 
55
55
  // src/engine-claude/loop.ts
56
- import { Service } from "@deepseek-ai/cordis";
57
56
  import z from "@deepseek-ai/schemastery";
58
- import { emitAgentEvent } from "@deepseek-ai/dsh-agent";
59
57
  import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
60
- import { interruptedTurnClosers, SessionLogOffset, SessionPreparation } from "@deepseek-ai/dsh-session";
61
58
 
62
59
  // src/engine-claude/agent.ts
63
60
  import { agentEvents } from "@deepseek-ai/dsh-agent";
64
- import { LlmError, createAssistantMessage, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
61
+ import {
62
+ AssistantStreamAccumulator as AssistantStreamAccumulator2,
63
+ LlmError,
64
+ createAssistantMessage,
65
+ createUserMessage,
66
+ errorChain
67
+ } from "@deepseek-ai/dsh-llm";
65
68
  import { createScope } from "@deepseek-ai/dsh-scope";
66
69
  import { canonicalHeader } from "@deepseek-ai/dsh-session";
67
70
  import { query as officialQuery } from "@anthropic-ai/claude-agent-sdk";
@@ -148,12 +151,16 @@ function toolResultContent(content) {
148
151
  }
149
152
  function mapUsage(usage) {
150
153
  return {
151
- inputTokens: usage.input_tokens,
152
- outputTokens: usage.output_tokens,
154
+ inputTokens: usage.input_tokens ?? 0,
155
+ outputTokens: usage.output_tokens ?? 0,
153
156
  ...usage.cache_read_input_tokens == null ? {} : { cacheReadTokens: usage.cache_read_input_tokens },
154
157
  ...usage.cache_creation_input_tokens == null ? {} : { cacheWriteTokens: usage.cache_creation_input_tokens }
155
158
  };
156
159
  }
160
+ function meaningfulUsage(usage) {
161
+ if (usage === void 0) return void 0;
162
+ return usage.inputTokens !== 0 || usage.outputTokens !== 0 || (usage.cacheReadTokens ?? 0) !== 0 || (usage.cacheWriteTokens ?? 0) !== 0 ? usage : void 0;
163
+ }
157
164
  function mapStreamEvent(event, toolCalls) {
158
165
  switch (event.type) {
159
166
  case "content_block_start": {
@@ -845,6 +852,17 @@ var ClaudeCodeAgent = class {
845
852
  requestHeaderLogged = false;
846
853
  /** Agent-lifecycle-local counter naming each streamed attempt. */
847
854
  streamAttempts = 0;
855
+ /**
856
+ * Tool results logged into the currently open step. A result means the
857
+ * segment that requested the call is finished, so the next assistant content
858
+ * opens the next step (see {@link beginSegment}).
859
+ */
860
+ stepSettledTools = 0;
861
+ /**
862
+ * Whether the current query has rotated into a second step. The query-total
863
+ * usage record is only meaningful while one step holds the whole query.
864
+ */
865
+ rotated = false;
848
866
  get status() {
849
867
  return this.phase.kind === "idle" || this.phase.kind === "maintenance" ? "idle" : "running";
850
868
  }
@@ -1091,7 +1109,7 @@ var ClaudeCodeAgent = class {
1091
1109
  const stepEnd = await this.step();
1092
1110
  if (turnEnds === null) turnEnds = stepEnd;
1093
1111
  } finally {
1094
- this.session.append("step/end", { turn, step });
1112
+ this.session.append("step/end", { turn, step: phase.step });
1095
1113
  }
1096
1114
  signal.throwIfAborted();
1097
1115
  if (turnEnds && this.inbox.nextStep.length === 0) {
@@ -1124,6 +1142,24 @@ var ClaudeCodeAgent = class {
1124
1142
  phase.step = 0;
1125
1143
  return true;
1126
1144
  }
1145
+ /**
1146
+ * Rotate to the next step when the segment that ran a tool has finished, so
1147
+ * each assistant segment lands in its own step.
1148
+ *
1149
+ * Called as new assistant content begins. A step holding a settled tool
1150
+ * result means the previous segment is complete, and the content about to be
1151
+ * written belongs to the next one. Rotating here (rather than when a call is
1152
+ * announced) keeps calls announced together — one model turn — in one step.
1153
+ * @param phase - the running phase carrying the open step's position.
1154
+ */
1155
+ beginSegment(phase) {
1156
+ if (this.stepSettledTools === 0) return;
1157
+ this.session.append("step/end", { turn: phase.turn, step: phase.step });
1158
+ phase.step += 1;
1159
+ this.session.append("step/start", { turn: phase.turn, step: phase.step });
1160
+ this.stepSettledTools = 0;
1161
+ this.rotated = true;
1162
+ }
1127
1163
  /** Model label recorded in the request header for one lifecycle. */
1128
1164
  modelLabel() {
1129
1165
  return this.config.model ?? NATIVE_MODEL_LABEL;
@@ -1144,8 +1180,11 @@ var ClaudeCodeAgent = class {
1144
1180
  /** Run one Claude Code query for the current step and map its transcript into the session log. */
1145
1181
  async step() {
1146
1182
  if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
1147
- const { turn, step, abort: { signal } } = this.phase;
1183
+ const phase = this.phase;
1184
+ const { abort: { signal } } = phase;
1148
1185
  signal.throwIfAborted();
1186
+ this.stepSettledTools = 0;
1187
+ this.rotated = false;
1149
1188
  const cwd = this.session.header.cwd;
1150
1189
  if (cwd === void 0 || cwd.length === 0) {
1151
1190
  throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
@@ -1186,8 +1225,8 @@ var ClaudeCodeAgent = class {
1186
1225
  live = new DriverAssistantStream(
1187
1226
  this.id,
1188
1227
  ++this.streamAttempts,
1189
- turn,
1190
- step,
1228
+ phase.turn,
1229
+ phase.step,
1191
1230
  (frame2) => this.dispatch.emit("agent/assistant-stream", { frame: frame2 })
1192
1231
  );
1193
1232
  live.start();
@@ -1197,11 +1236,17 @@ var ClaudeCodeAgent = class {
1197
1236
  const toolCalls = /* @__PURE__ */ new Map();
1198
1237
  const reasoningByIndex = /* @__PURE__ */ new Map();
1199
1238
  let pendingUsage;
1239
+ let requestUsage;
1200
1240
  signal.throwIfAborted();
1201
1241
  for await (const message of query) {
1202
1242
  signal.throwIfAborted();
1203
1243
  switch (message.type) {
1204
1244
  case "stream_event": {
1245
+ const event = message.event;
1246
+ if (event.type === "message_start") requestUsage = void 0;
1247
+ else if (event.type === "message_delta" && event.usage !== void 0) {
1248
+ requestUsage = meaningfulUsage(mapUsage(event.usage));
1249
+ }
1205
1250
  for (const chunk of mapStreamEvent(message.event, toolCalls)) {
1206
1251
  currentStream().push(chunk);
1207
1252
  if (chunk.type === "reasoning-delta") {
@@ -1219,7 +1264,7 @@ var ClaudeCodeAgent = class {
1219
1264
  reasoning.forEach((block, index) => {
1220
1265
  reasoningByIndex.set(index, block.text);
1221
1266
  });
1222
- pendingUsage = mapped.usage;
1267
+ pendingUsage = meaningfulUsage(mapped.usage) ?? requestUsage;
1223
1268
  break;
1224
1269
  }
1225
1270
  let content = mapped.content;
@@ -1228,13 +1273,14 @@ var ClaudeCodeAgent = class {
1228
1273
  content = [...synthesized, ...content];
1229
1274
  }
1230
1275
  if (content.length > 0) {
1276
+ this.beginSegment(phase);
1231
1277
  reasoningByIndex.clear();
1232
- const usage = mapped.usage ?? pendingUsage;
1278
+ const usage = meaningfulUsage(mapped.usage) ?? requestUsage ?? pendingUsage;
1233
1279
  pendingUsage = void 0;
1234
1280
  const attempt = live;
1235
1281
  const data = {
1236
- turn,
1237
- step,
1282
+ turn: phase.turn,
1283
+ step: phase.step,
1238
1284
  message: createAssistantMessage({
1239
1285
  content,
1240
1286
  source: { provider: PROVIDER, model: mapped.model }
@@ -1252,8 +1298,8 @@ var ClaudeCodeAgent = class {
1252
1298
  }
1253
1299
  for (const call of mapped.toolCalls) {
1254
1300
  this.session.append("tool/call", {
1255
- turn,
1256
- step,
1301
+ turn: phase.turn,
1302
+ step: phase.step,
1257
1303
  callId: call.callId,
1258
1304
  name: call.name,
1259
1305
  arguments: call.arguments
@@ -1263,7 +1309,8 @@ var ClaudeCodeAgent = class {
1263
1309
  }
1264
1310
  case "user": {
1265
1311
  for (const result of mapToolResults(message.message)) {
1266
- this.session.append("tool/result", { turn, step, message: result }, { surfaceOp: "append" });
1312
+ this.session.append("tool/result", { turn: phase.turn, step: phase.step, message: result }, { surfaceOp: "append" });
1313
+ this.stepSettledTools += 1;
1267
1314
  }
1268
1315
  break;
1269
1316
  }
@@ -1271,10 +1318,11 @@ var ClaudeCodeAgent = class {
1271
1318
  if (reasoningByIndex.size > 0) {
1272
1319
  const trailing = [...reasoningByIndex.entries()].sort((a, b) => a[0] - b[0]).map(([, text]) => ({ type: "reasoning", text }));
1273
1320
  reasoningByIndex.clear();
1321
+ this.beginSegment(phase);
1274
1322
  const attempt = live;
1275
1323
  const data = {
1276
- turn,
1277
- step,
1324
+ turn: phase.turn,
1325
+ step: phase.step,
1278
1326
  message: createAssistantMessage({
1279
1327
  content: trailing,
1280
1328
  source: { provider: PROVIDER, model: NATIVE_MODEL_LABEL }
@@ -1290,6 +1338,16 @@ var ClaudeCodeAgent = class {
1290
1338
  }
1291
1339
  pendingUsage = void 0;
1292
1340
  }
1341
+ const stepUsage = meaningfulUsage(mapUsage(message.usage));
1342
+ if (stepUsage !== void 0 && !this.rotated) {
1343
+ const accumulator = new AssistantStreamAccumulator2();
1344
+ accumulator.push({ time: Date.now(), chunk: { type: "usage", usage: stepUsage } });
1345
+ this.session.append("assistant/attempt", {
1346
+ turn: phase.turn,
1347
+ step: phase.step,
1348
+ stream: [...accumulator.snapshot()]
1349
+ });
1350
+ }
1293
1351
  if (message.subtype === "success") {
1294
1352
  finished = true;
1295
1353
  } else {
@@ -1318,6 +1376,11 @@ var ClaudeCodeAgent = class {
1318
1376
  }
1319
1377
  };
1320
1378
 
1379
+ // src/driver-core/hosted-loop-factory.ts
1380
+ import { Service } from "@deepseek-ai/cordis";
1381
+ import { emitAgentEvent } from "@deepseek-ai/dsh-agent";
1382
+ import { interruptedTurnClosers, SessionLogOffset, SessionPreparation } from "@deepseek-ai/dsh-session";
1383
+
1321
1384
  // src/driver-core/ownership.ts
1322
1385
  var INACTIVE_FIBER_VALUES = {
1323
1386
  FAILED: 3,
@@ -1406,54 +1469,23 @@ async function raceAbortCall(operation, signal, id, releaseAbandoned) {
1406
1469
  }
1407
1470
  }
1408
1471
 
1409
- // src/engine-claude/loop.ts
1410
- var CLAUDE_CODE_PERMISSION_MODES = [
1411
- "dontAsk",
1412
- "acceptEdits",
1413
- "auto",
1414
- "plan",
1415
- "bypassPermissions"
1416
- ];
1417
- var Config = z.object({
1418
- permissionMode: z.union([...CLAUDE_CODE_PERMISSION_MODES]),
1419
- env: z.dict(z.string()).default({}),
1420
- model: z.string(),
1421
- disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
1422
- maxTurns: z.number().step(1).min(1)
1423
- });
1424
- function resolveConfig(config) {
1425
- const disposeGraceMs = config.disposeGraceMs ?? DEFAULT_DISPOSE_GRACE_MS;
1426
- if (!Number.isFinite(disposeGraceMs) || disposeGraceMs <= 0) {
1427
- throw new Error("agent-loop-claude-code: disposeGraceMs must be a positive finite number");
1428
- }
1429
- if (disposeGraceMs > MAX_TIMER_DELAY_MS) {
1430
- throw new Error(
1431
- `agent-loop-claude-code: disposeGraceMs must be no greater than ${MAX_TIMER_DELAY_MS}`
1432
- );
1433
- }
1434
- return {
1435
- permissionMode: config.permissionMode,
1436
- env: config.env ?? {},
1437
- model: config.model,
1438
- disposeGraceMs,
1439
- maxTurns: config.maxTurns
1440
- };
1441
- }
1442
- var ClaudeCodeLoop = class extends Service {
1443
- /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
1444
- static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
1472
+ // src/driver-core/hosted-loop-factory.ts
1473
+ var HostedLoopFactory = class extends Service {
1445
1474
  /** Validated configuration owned by the loop plugin. */
1446
1475
  config;
1447
1476
  ownership;
1448
1477
  /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
1449
1478
  runtime;
1450
- constructor(ctx, config) {
1451
- super(ctx, "agentLoopClaudeCode");
1452
- this.config = resolveConfig(config);
1479
+ /** Cordis service name, also the prefix of every effect label. */
1480
+ label;
1481
+ constructor(ctx, label, config) {
1482
+ super(ctx, label);
1483
+ this.label = label;
1484
+ this.config = config;
1453
1485
  this.ownership = new FactoryOwnership(ctx.fiber);
1454
1486
  this.runtime = { ctx };
1455
- ctx.effect(() => () => this.ownership.dispose(), "agentLoopClaudeCode.transactions()");
1456
- ctx.effect(() => ctx.agents.setFactory(this), "agentLoopClaudeCode.setFactory()");
1487
+ ctx.effect(() => () => this.ownership.dispose(), `${label}.transactions()`);
1488
+ ctx.effect(() => ctx.agents.setFactory(this), `${label}.setFactory()`);
1457
1489
  ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
1458
1490
  ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
1459
1491
  ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
@@ -1518,7 +1550,7 @@ var ClaudeCodeLoop = class extends Service {
1518
1550
  if (disposing !== void 0) return;
1519
1551
  abort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
1520
1552
  return dispose(true);
1521
- }, `agentLoopClaudeCode.lifecycle(${id})`);
1553
+ }, `${this.label}.lifecycle(${id})`);
1522
1554
  } catch (error) {
1523
1555
  untrack();
1524
1556
  callerSignal?.removeEventListener("abort", onCallerAbort);
@@ -1530,7 +1562,7 @@ var ClaudeCodeLoop = class extends Service {
1530
1562
  throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
1531
1563
  };
1532
1564
  try {
1533
- const agent = machine = new ClaudeCodeAgent(loopCtx, id, options, session, this.config);
1565
+ const agent = machine = this.buildAgent(loopCtx, id, options, session);
1534
1566
  machineReady.resolve();
1535
1567
  assertLive();
1536
1568
  return {
@@ -1685,7 +1717,7 @@ var ClaudeCodeLoop = class extends Service {
1685
1717
  const ownerAbort = new AbortController();
1686
1718
  const unfollowOwner = ownerCtx.effect(() => () => {
1687
1719
  ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
1688
- }, `agentLoopClaudeCode.resume-load(${id})`);
1720
+ }, `${this.label}.resume-load(${id})`);
1689
1721
  const fused = AbortSignal.any([
1690
1722
  ...options.signal === void 0 ? [] : [options.signal],
1691
1723
  ownerAbort.signal,
@@ -1746,11 +1778,53 @@ var ClaudeCodeLoop = class extends Service {
1746
1778
  }
1747
1779
  };
1748
1780
 
1781
+ // src/engine-claude/loop.ts
1782
+ var CLAUDE_CODE_PERMISSION_MODES = [
1783
+ "dontAsk",
1784
+ "acceptEdits",
1785
+ "auto",
1786
+ "plan",
1787
+ "bypassPermissions"
1788
+ ];
1789
+ var Config = z.object({
1790
+ permissionMode: z.union([...CLAUDE_CODE_PERMISSION_MODES]),
1791
+ env: z.dict(z.string()).default({}),
1792
+ model: z.string(),
1793
+ disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
1794
+ maxTurns: z.number().step(1).min(1)
1795
+ });
1796
+ function resolveConfig(config) {
1797
+ const disposeGraceMs = config.disposeGraceMs ?? DEFAULT_DISPOSE_GRACE_MS;
1798
+ if (!Number.isFinite(disposeGraceMs) || disposeGraceMs <= 0) {
1799
+ throw new Error("agent-loop-claude-code: disposeGraceMs must be a positive finite number");
1800
+ }
1801
+ if (disposeGraceMs > MAX_TIMER_DELAY_MS) {
1802
+ throw new Error(
1803
+ `agent-loop-claude-code: disposeGraceMs must be no greater than ${MAX_TIMER_DELAY_MS}`
1804
+ );
1805
+ }
1806
+ return {
1807
+ permissionMode: config.permissionMode,
1808
+ env: config.env ?? {},
1809
+ model: config.model,
1810
+ disposeGraceMs,
1811
+ maxTurns: config.maxTurns
1812
+ };
1813
+ }
1814
+ var ClaudeCodeLoop = class extends HostedLoopFactory {
1815
+ /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
1816
+ static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
1817
+ constructor(ctx, config) {
1818
+ super(ctx, "agentLoopClaudeCode", resolveConfig(config));
1819
+ }
1820
+ /** Construct the Claude Code driver for one prepared session. */
1821
+ buildAgent(loopCtx, id, options, session) {
1822
+ return new ClaudeCodeAgent(loopCtx, id, options, session, this.config);
1823
+ }
1824
+ };
1825
+
1749
1826
  // src/engine-codex/loop.ts
1750
- import { Service as Service2 } from "@deepseek-ai/cordis";
1751
1827
  import z2 from "@deepseek-ai/schemastery";
1752
- import { emitAgentEvent as emitAgentEvent2 } from "@deepseek-ai/dsh-agent";
1753
- import { interruptedTurnClosers as interruptedTurnClosers2, SessionLogOffset as SessionLogOffset2, SessionPreparation as SessionPreparation2 } from "@deepseek-ai/dsh-session";
1754
1828
 
1755
1829
  // src/engine-codex/agent.ts
1756
1830
  import { agentEvents as agentEvents2 } from "@deepseek-ai/dsh-agent";
@@ -1763,6 +1837,42 @@ var DEFAULT_CODEX_PERMISSION = {
1763
1837
  sandboxMode: "read-only",
1764
1838
  approvalPolicy: "never"
1765
1839
  };
1840
+ function userInputQuestions(params) {
1841
+ if (typeof params !== "object" || params === null) return [];
1842
+ const raw = params.questions;
1843
+ if (!Array.isArray(raw)) return [];
1844
+ const questions = [];
1845
+ for (const entry of raw) {
1846
+ if (typeof entry !== "object" || entry === null) continue;
1847
+ const question = entry;
1848
+ if (typeof question.id !== "string" || question.id === "") continue;
1849
+ if (typeof question.question !== "string" || question.question === "") continue;
1850
+ const options = Array.isArray(question.options) ? question.options.flatMap((option) => {
1851
+ if (typeof option !== "object" || option === null) return [];
1852
+ const { label, description } = option;
1853
+ if (typeof label !== "string") return [];
1854
+ return [{ label, ...typeof description === "string" ? { description } : {} }];
1855
+ }) : [];
1856
+ questions.push({
1857
+ id: question.id,
1858
+ question: question.question,
1859
+ ...typeof question.header === "string" && question.header !== "" ? { header: question.header } : {},
1860
+ ...options.length > 0 ? { options } : {}
1861
+ });
1862
+ }
1863
+ return questions;
1864
+ }
1865
+ function userInputResponse(answer) {
1866
+ if (answer === void 0) return { answers: {} };
1867
+ const answers = {};
1868
+ for (const item of answer.answers) {
1869
+ answers[item.id] = { answers: [...item.selected, ...item.custom === void 0 || item.custom === "" ? [] : [item.custom]] };
1870
+ }
1871
+ return { answers };
1872
+ }
1873
+ function elicitationResponse() {
1874
+ return { action: "decline" };
1875
+ }
1766
1876
  function resolveSessionPermission2(events) {
1767
1877
  if (sessionSandboxMode(events) === "danger-full-access") {
1768
1878
  return { sandboxMode: "danger-full-access", approvalPolicy: "never" };
@@ -1772,6 +1882,68 @@ function resolveSessionPermission2(events) {
1772
1882
  }
1773
1883
  return DEFAULT_CODEX_PERMISSION;
1774
1884
  }
1885
+ var REASON_INPUT_CAP2 = 200;
1886
+ function approvalToolName(method) {
1887
+ switch (method) {
1888
+ case "item/commandExecution/requestApproval":
1889
+ return "command";
1890
+ case "item/fileChange/requestApproval":
1891
+ return "file-change";
1892
+ case "item/permissions/requestApproval":
1893
+ return "permissions";
1894
+ default:
1895
+ return method;
1896
+ }
1897
+ }
1898
+ function approvalKind(method) {
1899
+ switch (method) {
1900
+ case "item/commandExecution/requestApproval":
1901
+ return "command execution";
1902
+ case "item/fileChange/requestApproval":
1903
+ return "file change";
1904
+ case "item/permissions/requestApproval":
1905
+ return "permission change";
1906
+ default:
1907
+ return method;
1908
+ }
1909
+ }
1910
+ function approvalDetail(method, params) {
1911
+ if (typeof params !== "object" || params === null) return String(params);
1912
+ const record = params;
1913
+ if (method === "item/commandExecution/requestApproval" && typeof record.command === "string") {
1914
+ return record.command;
1915
+ }
1916
+ if (typeof record.reason === "string") return record.reason;
1917
+ if (method === "item/fileChange/requestApproval" && typeof record.grantRoot === "string") {
1918
+ return record.grantRoot;
1919
+ }
1920
+ return JSON.stringify(params);
1921
+ }
1922
+ function approvalReason2(method, params) {
1923
+ const detail = approvalDetail(method, params);
1924
+ const bounded = detail.length > REASON_INPUT_CAP2 ? `${detail.slice(0, REASON_INPUT_CAP2 - 3)}...` : detail;
1925
+ return `Codex requests permission for ${approvalKind(method)}: ${bounded}`;
1926
+ }
1927
+ function approvalDecision(outcome) {
1928
+ return outcome === "allowed-once" ? "accept" : "decline";
1929
+ }
1930
+ function permissionsGrant(outcome, requested) {
1931
+ return outcome === "allowed-once" ? { permissions: requested, scope: "turn" } : { permissions: {}, scope: "turn" };
1932
+ }
1933
+ function resolveApprovalRequest(method, params, outcome) {
1934
+ switch (method) {
1935
+ case "item/commandExecution/requestApproval":
1936
+ return { result: { decision: approvalDecision(outcome) } };
1937
+ case "item/fileChange/requestApproval":
1938
+ return { result: { decision: approvalDecision(outcome) } };
1939
+ case "item/permissions/requestApproval": {
1940
+ const requested = typeof params === "object" && params !== null ? params.permissions ?? {} : {};
1941
+ return { result: permissionsGrant(outcome, requested) };
1942
+ }
1943
+ default:
1944
+ return { error: { code: -32601, message: "Method not found" } };
1945
+ }
1946
+ }
1775
1947
 
1776
1948
  // src/engine-codex/appserver/client.ts
1777
1949
  import { spawn } from "node:child_process";
@@ -1788,6 +1960,7 @@ var AppServerClient = class _AppServerClient {
1788
1960
  reqId = 1;
1789
1961
  pending = /* @__PURE__ */ new Map();
1790
1962
  notificationHandler;
1963
+ requestHandler;
1791
1964
  stderrHandler;
1792
1965
  disposed = false;
1793
1966
  /** Whether this client was disposed or its server process exited. */
@@ -1827,6 +2000,10 @@ var AppServerClient = class _AppServerClient {
1827
2000
  onNotification(handler) {
1828
2001
  this.notificationHandler = handler;
1829
2002
  }
2003
+ /** Set the handler for server-initiated requests (e.g. approvals). */
2004
+ onRequest(handler) {
2005
+ this.requestHandler = handler;
2006
+ }
1830
2007
  /** Set the stderr handler for server log lines. */
1831
2008
  onStderr(handler) {
1832
2009
  this.stderrHandler = handler;
@@ -1888,7 +2065,12 @@ var AppServerClient = class _AppServerClient {
1888
2065
  } catch {
1889
2066
  return;
1890
2067
  }
2068
+ if (obj.id !== void 0 && obj.method !== void 0) {
2069
+ void this.answerRequest(obj.method, obj.params, obj.id);
2070
+ return;
2071
+ }
1891
2072
  if (obj.id !== void 0) {
2073
+ if (typeof obj.id !== "number") return;
1892
2074
  const pending = this.pending.get(obj.id);
1893
2075
  if (pending) {
1894
2076
  this.pending.delete(obj.id);
@@ -1898,11 +2080,24 @@ var AppServerClient = class _AppServerClient {
1898
2080
  pending.resolve(obj.result);
1899
2081
  }
1900
2082
  }
2083
+ return;
1901
2084
  }
1902
2085
  if (obj.method !== void 0) {
1903
2086
  this.notificationHandler?.(obj.method, obj.params);
1904
2087
  }
1905
2088
  }
2089
+ /** Resolve one inbound server request and write the JSON-RPC reply to stdin. */
2090
+ async answerRequest(method, params, id) {
2091
+ let outcome;
2092
+ try {
2093
+ outcome = this.requestHandler !== void 0 ? await this.requestHandler(method, params, id) : { error: { code: -32601, message: "Method not found" } };
2094
+ } catch (error) {
2095
+ outcome = { error: { code: -32603, message: error instanceof Error ? error.message : "internal error" } };
2096
+ }
2097
+ if (this.disposed) return;
2098
+ const reply = "result" in outcome ? { id, result: outcome.result } : { id, error: outcome.error };
2099
+ this.process.stdin?.write(JSON.stringify(reply) + "\n");
2100
+ }
1906
2101
  };
1907
2102
 
1908
2103
  // src/engine-codex/appserver/thread.ts
@@ -2109,6 +2304,9 @@ function mapMcpToolCall(item) {
2109
2304
  // src/engine-codex/agent.ts
2110
2305
  var PROVIDER2 = "codex";
2111
2306
  var NATIVE_MODEL_LABEL2 = "codex-native";
2307
+ function nonEmptyText(parts) {
2308
+ return parts !== void 0 && parts.some((part) => part.length > 0) ? parts.join("\n") : void 0;
2309
+ }
2112
2310
  var CodexAgent = class {
2113
2311
  constructor(loopCtx, id, options, session, config) {
2114
2312
  this.loopCtx = loopCtx;
@@ -2154,14 +2352,103 @@ var CodexAgent = class {
2154
2352
  requestHeaderLogged = false;
2155
2353
  /** Agent-lifecycle-local counter naming each streamed attempt. */
2156
2354
  streamAttempts = 0;
2355
+ /**
2356
+ * Tool results logged into the currently open step. A result means the
2357
+ * segment that requested the call is finished, so the next assistant content
2358
+ * opens the next step (see {@link beginSegment}).
2359
+ */
2360
+ stepSettledTools = 0;
2361
+ /**
2362
+ * Rotate to the next step when the segment that ran a tool has finished, so
2363
+ * each assistant segment lands in its own step.
2364
+ *
2365
+ * Called as new assistant content begins. A step holding a settled tool
2366
+ * result means the previous segment is complete, and the content about to be
2367
+ * written belongs to the next one. Rotating here (rather than when a call is
2368
+ * announced) keeps calls announced together — one model turn — in one step.
2369
+ * @param phase - the running phase carrying the open step's position.
2370
+ */
2371
+ beginSegment(phase) {
2372
+ if (this.stepSettledTools === 0) return;
2373
+ this.session.append("step/end", { turn: phase.turn, step: phase.step });
2374
+ phase.step += 1;
2375
+ this.session.append("step/start", { turn: phase.turn, step: phase.step });
2376
+ this.stepSettledTools = 0;
2377
+ }
2157
2378
  /** Lazily created app-server client, reused across steps and released on scope teardown. */
2158
2379
  appServer;
2159
2380
  /** Return the cached app-server client, spawning one on first use or after a dead process. */
2160
2381
  async appServerClient() {
2161
2382
  if (this.appServer !== void 0 && !this.appServer.closed) return this.appServer;
2162
2383
  this.appServer = await AppServerClient.create();
2384
+ this.appServer.onRequest((method, params) => this.answerRequest(method, params));
2163
2385
  return this.appServer;
2164
2386
  }
2387
+ /**
2388
+ * Answer one server-initiated interaction. Approvals go through the dsh
2389
+ * approval seam; a `request_user_input` question goes to the user-questions
2390
+ * seam (both fail closed when their seam is absent), and an MCP elicitation is
2391
+ * declined outright. Anything else is a protocol error.
2392
+ * @param method - the server request method.
2393
+ * @param params - the server request params.
2394
+ * @returns the JSON-RPC outcome to send back.
2395
+ */
2396
+ async answerRequest(method, params) {
2397
+ if (method === "item/tool/requestUserInput") return this.answerUserInput(params);
2398
+ if (method === "mcpServer/elicitation/request") return { result: elicitationResponse() };
2399
+ return this.answerApproval(method, params);
2400
+ }
2401
+ /** Resolve one native Codex approval request through the dsh approval seam. */
2402
+ async answerApproval(method, params) {
2403
+ const outcome = await this.requestApproval(method, params);
2404
+ return resolveApprovalRequest(method, params, outcome);
2405
+ }
2406
+ /**
2407
+ * Put one `request_user_input` question to the human through the dsh
2408
+ * user-questions seam. The seam itself fails closed (`NO_PROVIDER`) when no
2409
+ * answerer is composed, so a refusal degrades to "no answers given" and the
2410
+ * turn continues; asking is never silently skipped, and the degradation is
2411
+ * logged rather than swallowed.
2412
+ * @param params - the request params carrying the questions.
2413
+ * @returns the response payload (or the empty answer when nobody answered).
2414
+ */
2415
+ async answerUserInput(params) {
2416
+ const questions = userInputQuestions(params);
2417
+ const service = this.loopCtx.get("userQuestions");
2418
+ if (questions.length === 0) {
2419
+ return { result: userInputResponse(void 0) };
2420
+ }
2421
+ if (service === void 0) {
2422
+ this.loopCtx.logger.warn("loop-engine: codex asked for user input, but the user-questions service is not composed; answering with no answers");
2423
+ return { result: userInputResponse(void 0) };
2424
+ }
2425
+ const phase = this.phase;
2426
+ const signal = phase.kind === "running" ? phase.abort.signal : void 0;
2427
+ try {
2428
+ const answer = await service.ask({
2429
+ questions,
2430
+ agent: this,
2431
+ ...signal === void 0 ? {} : { signal }
2432
+ });
2433
+ return { result: userInputResponse(answer) };
2434
+ } catch (error) {
2435
+ this.loopCtx.logger.warn(`loop-engine: codex user-input request went unanswered: ${String(error)}`);
2436
+ return { result: userInputResponse(void 0) };
2437
+ }
2438
+ }
2439
+ /** Ask the dsh approval seam; fail closed to a denial when it is absent. */
2440
+ async requestApproval(method, params) {
2441
+ const approval = this.loopCtx.get("approval");
2442
+ if (approval === void 0) return "unavailable";
2443
+ const phase = this.phase;
2444
+ const signal = phase.kind === "running" ? phase.abort.signal : void 0;
2445
+ return approval.request({
2446
+ agent: this,
2447
+ toolName: approvalToolName(method),
2448
+ reason: approvalReason2(method, params),
2449
+ ...signal === void 0 ? {} : { signal }
2450
+ });
2451
+ }
2165
2452
  get status() {
2166
2453
  return this.phase.kind === "idle" || this.phase.kind === "maintenance" ? "idle" : "running";
2167
2454
  }
@@ -2390,7 +2677,7 @@ var CodexAgent = class {
2390
2677
  const stepEnd = await this.step();
2391
2678
  if (turnEnds === null) turnEnds = stepEnd;
2392
2679
  } finally {
2393
- this.session.append("step/end", { turn, step });
2680
+ this.session.append("step/end", { turn, step: phase.step });
2394
2681
  }
2395
2682
  signal.throwIfAborted();
2396
2683
  if (turnEnds && this.inbox.nextStep.length === 0) {
@@ -2443,8 +2730,10 @@ var CodexAgent = class {
2443
2730
  /** Run one Codex thread for the current step and map its transcript into the session log. */
2444
2731
  async step() {
2445
2732
  if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
2446
- const { turn, step, abort: { signal } } = this.phase;
2733
+ const phase = this.phase;
2734
+ const { abort: { signal } } = phase;
2447
2735
  signal.throwIfAborted();
2736
+ this.stepSettledTools = 0;
2448
2737
  const cwd = this.session.header.cwd;
2449
2738
  if (cwd === void 0 || cwd.length === 0) {
2450
2739
  throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
@@ -2485,6 +2774,7 @@ var CodexAgent = class {
2485
2774
  let finished = false;
2486
2775
  const pendingReasoning = [];
2487
2776
  const pendingReasoningStream = [];
2777
+ const streamedReasoning = /* @__PURE__ */ new Map();
2488
2778
  let held;
2489
2779
  let reasoningBlockStarted = false;
2490
2780
  let textBlockStarted = false;
@@ -2494,20 +2784,29 @@ var CodexAgent = class {
2494
2784
  live = new DriverAssistantStream(
2495
2785
  this.id,
2496
2786
  ++this.streamAttempts,
2497
- turn,
2498
- step,
2787
+ phase.turn,
2788
+ phase.step,
2499
2789
  (frame2) => this.dispatch.emit("agent/assistant-stream", { frame: frame2 })
2500
2790
  );
2501
2791
  live.start();
2502
2792
  }
2503
2793
  return live;
2504
2794
  };
2795
+ const foldReasoning = () => {
2796
+ if (pendingReasoning.length === 0) return;
2797
+ const reasoningBlocks = pendingReasoning.map((text) => ({ type: "reasoning", text }));
2798
+ held = held === void 0 ? { content: reasoningBlocks, stream: [...pendingReasoningStream] } : { ...held, content: [...held.content, ...reasoningBlocks], stream: [...held.stream, ...pendingReasoningStream] };
2799
+ pendingReasoning.length = 0;
2800
+ pendingReasoningStream.length = 0;
2801
+ reasoningBlockStarted = false;
2802
+ };
2505
2803
  const flushHeld = (usage) => {
2804
+ foldReasoning();
2506
2805
  if (held === void 0) return;
2507
2806
  const attempt = live;
2508
2807
  const data = {
2509
- turn,
2510
- step,
2808
+ turn: phase.turn,
2809
+ step: phase.step,
2511
2810
  message: createAssistantMessage2({
2512
2811
  content: held.content,
2513
2812
  source: { provider: PROVIDER2, model: this.modelLabel() }
@@ -2524,16 +2823,10 @@ var CodexAgent = class {
2524
2823
  }
2525
2824
  held = void 0;
2526
2825
  };
2527
- const flushReasoning = (usage) => {
2528
- if (pendingReasoning.length === 0) return;
2529
- flushHeld();
2530
- held = {
2531
- content: pendingReasoning.map((text) => ({ type: "reasoning", text })),
2532
- stream: [...pendingReasoningStream]
2533
- };
2534
- pendingReasoning.length = 0;
2535
- pendingReasoningStream.length = 0;
2536
- flushHeld(usage);
2826
+ const foldToolCall = (call) => {
2827
+ foldReasoning();
2828
+ const block = { type: "tool-call", id: call.callId, name: call.name, arguments: call.arguments };
2829
+ held = held === void 0 ? { content: [block], stream: [] } : { ...held, content: [...held.content, block] };
2537
2830
  };
2538
2831
  signal.throwIfAborted();
2539
2832
  for await (const event of events) {
@@ -2544,7 +2837,7 @@ var CodexAgent = class {
2544
2837
  case "item-started": {
2545
2838
  if (event.itemType === "agentMessage") {
2546
2839
  textBlockStarted = false;
2547
- textBlockIndex = pendingReasoning.length;
2840
+ textBlockIndex = (held?.content.length ?? 0) + pendingReasoning.length;
2548
2841
  }
2549
2842
  break;
2550
2843
  }
@@ -2559,85 +2852,100 @@ var CodexAgent = class {
2559
2852
  case "reasoning-summary-delta":
2560
2853
  case "reasoning-text-delta":
2561
2854
  case "plan-delta": {
2562
- const index = pendingReasoning.length;
2855
+ this.beginSegment(phase);
2856
+ const index = (held?.content.length ?? 0) + pendingReasoning.length;
2563
2857
  if (!reasoningBlockStarted) {
2564
2858
  reasoningBlockStarted = true;
2565
2859
  currentStream().push({ type: "block-start", index, blockType: "reasoning" });
2566
2860
  }
2567
2861
  currentStream().push({ type: "reasoning-delta", index, text: event.delta });
2862
+ if (event.kind !== "plan-delta") {
2863
+ streamedReasoning.set(event.itemId, (streamedReasoning.get(event.itemId) ?? "") + event.delta);
2864
+ }
2568
2865
  break;
2569
2866
  }
2570
2867
  case "item-completed": {
2571
2868
  const item = event.item;
2572
2869
  if (item.type === "reasoning") {
2573
- const summary = item.summary;
2574
- const content = item.content;
2575
- const text = summary?.join("\n") ?? content?.join("\n") ?? "";
2870
+ const terminal = item;
2871
+ const text = nonEmptyText(terminal.summary) ?? nonEmptyText(terminal.content) ?? streamedReasoning.get(terminal.id) ?? "";
2872
+ streamedReasoning.delete(terminal.id);
2576
2873
  pendingReasoning.push(text);
2577
2874
  pendingReasoningStream.push(...live?.takeStream() ?? []);
2578
2875
  reasoningBlockStarted = false;
2876
+ } else if (item.type === "plan") {
2877
+ live?.takeStream();
2878
+ reasoningBlockStarted = false;
2579
2879
  } else if (item.type === "agentMessage") {
2880
+ this.beginSegment(phase);
2580
2881
  const textStream = live?.takeStream() ?? [];
2581
- flushHeld();
2582
- held = {
2583
- content: [
2584
- ...pendingReasoning.map((text) => ({ type: "reasoning", text })),
2585
- { type: "text", text: item.text ?? "" }
2586
- ],
2587
- stream: [...pendingReasoningStream, ...textStream]
2588
- };
2882
+ const reasoningBlocks = pendingReasoning.map((text) => ({ type: "reasoning", text }));
2883
+ const textBlock = { type: "text", text: item.text ?? "" };
2884
+ if (held === void 0) {
2885
+ held = { content: [...reasoningBlocks, textBlock], stream: [...pendingReasoningStream, ...textStream] };
2886
+ } else {
2887
+ held = {
2888
+ ...held,
2889
+ content: [...held.content, ...reasoningBlocks, textBlock],
2890
+ stream: [...held.stream, ...pendingReasoningStream, ...textStream]
2891
+ };
2892
+ }
2589
2893
  pendingReasoning.length = 0;
2590
2894
  pendingReasoningStream.length = 0;
2591
2895
  reasoningBlockStarted = false;
2592
2896
  textBlockStarted = false;
2593
2897
  } else if (item.type === "commandExecution") {
2594
- flushReasoning();
2595
- flushHeld();
2898
+ this.beginSegment(phase);
2596
2899
  const activity = mapCommandExecution(item);
2900
+ foldToolCall(activity.call);
2901
+ flushHeld();
2597
2902
  this.session.append("tool/call", {
2598
- turn,
2599
- step,
2903
+ turn: phase.turn,
2904
+ step: phase.step,
2600
2905
  callId: activity.call.callId,
2601
2906
  name: activity.call.name,
2602
2907
  arguments: activity.call.arguments
2603
2908
  });
2604
- this.session.append("tool/result", { turn, step, message: activity.result }, { surfaceOp: "append" });
2909
+ this.session.append("tool/result", { turn: phase.turn, step: phase.step, message: activity.result }, { surfaceOp: "append" });
2910
+ this.stepSettledTools += 1;
2605
2911
  } else if (item.type === "fileChange") {
2606
- flushReasoning();
2607
- flushHeld();
2912
+ this.beginSegment(phase);
2608
2913
  const activity = mapFileChange(item);
2914
+ foldToolCall(activity.call);
2915
+ flushHeld();
2609
2916
  this.session.append("tool/call", {
2610
- turn,
2611
- step,
2917
+ turn: phase.turn,
2918
+ step: phase.step,
2612
2919
  callId: activity.call.callId,
2613
2920
  name: activity.call.name,
2614
2921
  arguments: activity.call.arguments
2615
2922
  });
2616
- this.session.append("tool/result", { turn, step, message: activity.result }, { surfaceOp: "append" });
2923
+ this.session.append("tool/result", { turn: phase.turn, step: phase.step, message: activity.result }, { surfaceOp: "append" });
2924
+ this.stepSettledTools += 1;
2617
2925
  } else if (item.type === "mcpToolCall") {
2618
- flushReasoning();
2619
- flushHeld();
2926
+ this.beginSegment(phase);
2620
2927
  const activity = mapMcpToolCall(item);
2928
+ foldToolCall(activity.call);
2929
+ flushHeld();
2621
2930
  this.session.append("tool/call", {
2622
- turn,
2623
- step,
2931
+ turn: phase.turn,
2932
+ step: phase.step,
2624
2933
  callId: activity.call.callId,
2625
2934
  name: activity.call.name,
2626
2935
  arguments: activity.call.arguments
2627
2936
  });
2628
- this.session.append("tool/result", { turn, step, message: activity.result }, { surfaceOp: "append" });
2937
+ this.session.append("tool/result", { turn: phase.turn, step: phase.step, message: activity.result }, { surfaceOp: "append" });
2938
+ this.stepSettledTools += 1;
2629
2939
  }
2630
2940
  break;
2631
2941
  }
2632
2942
  case "turn-completed": {
2633
2943
  const usage = event.turn.usage ? mapUsage2(event.turn.usage) : void 0;
2634
- if (pendingReasoning.length > 0) flushReasoning(usage);
2635
- else flushHeld(usage);
2944
+ flushHeld(usage);
2636
2945
  finished = true;
2637
2946
  break;
2638
2947
  }
2639
2948
  case "error":
2640
- flushReasoning();
2641
2949
  flushHeld();
2642
2950
  throw new LlmError2(event.error.message, "CODEX_ERROR");
2643
2951
  /* v8 ignore next -- AppServerEvent is a closed union; no unknown kinds */
@@ -2645,7 +2953,6 @@ var CodexAgent = class {
2645
2953
  break;
2646
2954
  }
2647
2955
  }
2648
- flushReasoning();
2649
2956
  flushHeld();
2650
2957
  if (!finished) {
2651
2958
  throw new LlmError2(
@@ -2688,310 +2995,15 @@ function resolveConfig2(config) {
2688
2995
  model: config.model
2689
2996
  };
2690
2997
  }
2691
- var CodexLoop = class extends Service2 {
2998
+ var CodexLoop = class extends HostedLoopFactory {
2692
2999
  /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
2693
3000
  static inject = ["agents", "sessions", "systemPrompt"];
2694
- /** Validated configuration owned by the loop plugin. */
2695
- config;
2696
- ownership;
2697
- /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
2698
- runtime;
2699
3001
  constructor(ctx, config) {
2700
- super(ctx, "agentLoopCodex");
2701
- this.config = resolveConfig2(config);
2702
- this.ownership = new FactoryOwnership(ctx.fiber);
2703
- this.runtime = { ctx };
2704
- ctx.effect(() => () => this.ownership.dispose(), "agentLoopCodex.transactions()");
2705
- ctx.effect(() => ctx.agents.setFactory(this), "agentLoopCodex.setFactory()");
2706
- ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
2707
- ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
2708
- ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
3002
+ super(ctx, "agentLoopCodex", resolveConfig2(config));
2709
3003
  }
2710
- /**
2711
- * Construct the driver, scope, and one memoized reverse teardown for a new
2712
- * agent. The teardown is registered with the factory and the owner fiber
2713
- * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
2714
- * fuses caller cancellation with lifecycle teardown for setup awaits.
2715
- */
2716
- /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
2717
- prepare(ownerCtx, id, options, session, callerSignal, handle, parentAgent) {
2718
- ownerCtx.fiber.assertActive();
2719
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
2720
- if (callerSignal?.aborted) {
2721
- throw callerSignal.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal.reason });
2722
- }
2723
- const loopCtx = this.runtime.ctx;
2724
- const abort = new AbortController();
2725
- const onCallerAbort = () => {
2726
- abort.abort(callerSignal?.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal?.reason }));
2727
- };
2728
- const onFactoryTeardown = () => {
2729
- abort.abort(this.ownership.signal.reason);
2730
- };
2731
- callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
2732
- this.ownership.signal.addEventListener("abort", onFactoryTeardown, { once: true });
2733
- let machine;
2734
- let detachSession;
2735
- let detachAgent;
2736
- let disposing;
2737
- const machineReady = Promise.withResolvers();
2738
- const dispose = (ownerTriggered = false) => disposing ??= (async () => {
2739
- abort.abort(new Error(`agent "${id}" lifecycle disposed`));
2740
- callerSignal?.removeEventListener("abort", onCallerAbort);
2741
- this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
2742
- try {
2743
- if (machine === void 0) await machineReady.promise;
2744
- if (machine !== void 0) {
2745
- machine.cancel({ kind: "disposed" });
2746
- await machine.whenIdle();
2747
- await machine.scope.dispose();
2748
- }
2749
- } finally {
2750
- try {
2751
- await handle?.close();
2752
- } finally {
2753
- try {
2754
- detachAgent?.();
2755
- detachSession?.();
2756
- } finally {
2757
- untrack();
2758
- if (!ownerTriggered) await unfollowOwner();
2759
- }
2760
- }
2761
- }
2762
- })();
2763
- const untrack = this.ownership.track(dispose);
2764
- let unfollowOwner;
2765
- try {
2766
- unfollowOwner = ownerCtx.effect(() => () => {
2767
- if (disposing !== void 0) return;
2768
- abort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
2769
- return dispose(true);
2770
- }, `agentLoopCodex.lifecycle(${id})`);
2771
- } catch (error) {
2772
- untrack();
2773
- callerSignal?.removeEventListener("abort", onCallerAbort);
2774
- this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
2775
- throw error;
2776
- }
2777
- const assertLive = () => {
2778
- if (!abort.signal.aborted) return;
2779
- throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
2780
- };
2781
- try {
2782
- const agent = machine = new CodexAgent(loopCtx, id, options, session, this.config);
2783
- machineReady.resolve();
2784
- assertLive();
2785
- return {
2786
- agent,
2787
- signal: abort.signal,
2788
- publish: (source) => {
2789
- assertLive();
2790
- detachSession = agent.ctx.sessions.enter(session);
2791
- detachAgent = loopCtx.agents.enter(agent, parentAgent);
2792
- agent.ctx.sessions.announce(session);
2793
- assertLive();
2794
- loopCtx.agents.announce(agent);
2795
- assertLive();
2796
- emitAgentEvent2(loopCtx, agent, "agent/session-start", { source });
2797
- assertLive();
2798
- return { agent, dispose };
2799
- },
2800
- dispose
2801
- };
2802
- } catch (error) {
2803
- machineReady.resolve();
2804
- void dispose();
2805
- throw error;
2806
- }
2807
- }
2808
- /** Prepare one Agent around an acquired Session, run setup, and publish it. */
2809
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored, parentAgent) {
2810
- var _stack = [];
2811
- try {
2812
- const ownedPreparation = __using(_stack, preparation);
2813
- const session = ownedPreparation.session;
2814
- let prepared;
2815
- try {
2816
- prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle, parentAgent);
2817
- } catch (error) {
2818
- await stored?.handle.close().catch(() => {
2819
- });
2820
- throw error;
2821
- }
2822
- try {
2823
- const setupCommit = await raceAbort(setup?.(prepared.agent.ctx, prepared.agent), prepared.signal, id);
2824
- setupCommit?.commit();
2825
- await this.appendUnstoredSuffix(stored, session);
2826
- return prepared.publish(source);
2827
- } catch (error) {
2828
- await prepared.dispose().catch(() => {
2829
- });
2830
- throw error;
2831
- }
2832
- } catch (_) {
2833
- var _error = _, _hasError = true;
2834
- } finally {
2835
- __callDispose(_stack, _error, _hasError);
2836
- }
2837
- }
2838
- /**
2839
- * Create an agent and session under one caller-supplied identity, owned by
2840
- * the accessing fiber. When a persistence backend is mounted, the session's
2841
- * durable identity is stored before publication.
2842
- * @param ownerCtx - caller context that structurally owns the lifecycle.
2843
- * @param options - identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.
2844
- * @returns the published handle.
2845
- */
2846
- async createAgent(ownerCtx, options) {
2847
- const preparation = SessionPreparation2.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
2848
- ...options.seed === void 0 ? {} : { seed: options.seed },
2849
- ...options.meta === void 0 ? {} : { meta: options.meta },
2850
- ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
2851
- }));
2852
- const published = (async () => {
2853
- let stored;
2854
- try {
2855
- stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
2856
- () => this.createStoredSession(preparation.session, options.signal),
2857
- options.signal,
2858
- options.sessionId,
2859
- (abandoned) => {
2860
- void abandoned?.handle.close().catch(() => {
2861
- });
2862
- }
2863
- );
2864
- } catch (error) {
2865
- preparation[Symbol.dispose]();
2866
- throw error;
2867
- }
2868
- return this.setupAndPublish(
2869
- ownerCtx,
2870
- options.sessionId,
2871
- preparation,
2872
- options.agentOptions ?? {},
2873
- options.setup,
2874
- options.signal,
2875
- "startup",
2876
- stored,
2877
- options.parentAgent
2878
- );
2879
- })();
2880
- this.ownership.trackWrapper(published);
2881
- return published;
2882
- }
2883
- /**
2884
- * Take a fresh session's write ownership when persistence is mounted.
2885
- * Nothing is appended here: the constructor seed (which never re-emits
2886
- * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
2887
- * publication commit point, so a failed or cancelled setup closes an
2888
- * unmaterialized handle and leaves no stored residue — the same id can be
2889
- * created again.
2890
- * @param session - the unpublished session to store.
2891
- * @param signal - optional cancellation forwarded to the backend create.
2892
- * @returns the owned handle and stored cursor, or `undefined` without a backend.
2893
- */
2894
- async createStoredSession(session, signal) {
2895
- const persistence = this.runtime.ctx.get("sessionPersistence");
2896
- if (persistence === void 0) return void 0;
2897
- const handle = await persistence.create(session.header, {
2898
- inheritedEventCount: session.inheritedEventCount,
2899
- ...signal === void 0 ? {} : { signal }
2900
- });
2901
- return { handle, storedCount: 0 };
2902
- }
2903
- /**
2904
- * Durably store the session events appended since the last stored cursor.
2905
- * Pre-publication appends (constructor seed markers, setup-window events)
2906
- * never re-emit through `session/event`, so publication must flush them
2907
- * through the handle before live events start routing into it.
2908
- * @param stored - the session's owned handle and stored cursor, if any.
2909
- * @param session - the unpublished session whose suffix is stored.
2910
- */
2911
- async appendUnstoredSuffix(stored, session) {
2912
- if (stored === void 0) return;
2913
- const suffix = session.snapshotEvents(SessionLogOffset2(stored.storedCount));
2914
- if (suffix.length > 0) await stored.handle.append(suffix);
2915
- stored.storedCount += suffix.length;
2916
- }
2917
- /**
2918
- * Resume an owned agent from the configured persistence service.
2919
- * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
2920
- * @param options - persisted identity, optional live parent, loop options, setup, and cancellation.
2921
- * @returns the published handle.
2922
- */
2923
- async resume(ownerCtx, options) {
2924
- const persistence = this.runtime.ctx.get("sessionPersistence");
2925
- if (persistence === void 0) {
2926
- throw new Error("cannot resume: session persistence is not configured (load a dsh-session-persistence backend)");
2927
- }
2928
- return this.resumeWith(ownerCtx, persistence, options);
2929
- }
2930
- /** Resume through an explicit persistence service. */
2931
- resumeWith(ownerCtx, persistence, options) {
2932
- const id = options.resumeSessionId;
2933
- const published = (async () => {
2934
- const ownerAbort = new AbortController();
2935
- const unfollowOwner = ownerCtx.effect(() => () => {
2936
- ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
2937
- }, `agentLoopCodex.resume-load(${id})`);
2938
- const fused = AbortSignal.any([
2939
- ...options.signal === void 0 ? [] : [options.signal],
2940
- ownerAbort.signal,
2941
- this.ownership.signal
2942
- ]);
2943
- let handle;
2944
- let stored;
2945
- let preparation;
2946
- try {
2947
- try {
2948
- handle = await raceAbortCall(
2949
- () => persistence.open(id, "write", { signal: fused }),
2950
- fused,
2951
- id,
2952
- (abandoned) => {
2953
- void abandoned.close();
2954
- }
2955
- );
2956
- const coldRead = await handle.read(0, void 0, { signal: fused });
2957
- fused.throwIfAborted();
2958
- const persisted = coldRead.events;
2959
- const closers = interruptedTurnClosers2(persisted);
2960
- if (closers.length > 0) await handle.append(closers);
2961
- preparation = SessionPreparation2.create(this.runtime.ctx.sessions.prepare(id, {
2962
- seed: [...persisted, ...closers],
2963
- meta: structuredClone(handle.header),
2964
- inheritedEventCount: handle.inheritedEventCount,
2965
- eventState: coldRead.eventState
2966
- }));
2967
- stored = { handle, storedCount: persisted.length + closers.length };
2968
- await this.appendUnstoredSuffix(stored, preparation.session);
2969
- } finally {
2970
- await unfollowOwner();
2971
- }
2972
- ownerCtx.fiber.assertActive();
2973
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
2974
- const owned = stored;
2975
- handle = void 0;
2976
- return await this.setupAndPublish(
2977
- ownerCtx,
2978
- id,
2979
- preparation,
2980
- options.agentOptions ?? {},
2981
- options.setup,
2982
- options.signal,
2983
- "resume",
2984
- owned,
2985
- options.parentAgent
2986
- );
2987
- } finally {
2988
- preparation?.[Symbol.dispose]();
2989
- await handle?.close().catch(() => {
2990
- });
2991
- }
2992
- })();
2993
- this.ownership.trackWrapper(published);
2994
- return published;
3004
+ /** Construct the Codex driver for one prepared session. */
3005
+ buildAgent(loopCtx, id, options, session) {
3006
+ return new CodexAgent(loopCtx, id, options, session, this.config);
2995
3007
  }
2996
3008
  };
2997
3009
 
@@ -2999,23 +3011,20 @@ var CodexLoop = class extends Service2 {
2999
3011
  import { readFileSync } from "node:fs";
3000
3012
  import { dirname as dirname2, join as join2 } from "node:path";
3001
3013
  import { fileURLToPath } from "node:url";
3002
- import { Service as Service3 } from "@deepseek-ai/cordis";
3003
3014
  import z3 from "@deepseek-ai/schemastery";
3004
- import { emitAgentEvent as emitAgentEvent3 } from "@deepseek-ai/dsh-agent";
3005
- import { interruptedTurnClosers as interruptedTurnClosers3, SessionLogOffset as SessionLogOffset3, SessionPreparation as SessionPreparation3 } from "@deepseek-ai/dsh-session";
3006
3015
 
3007
3016
  // src/engine-pi/agent.ts
3008
3017
  import { agentEvents as agentEvents3 } from "@deepseek-ai/dsh-agent";
3009
- import { ToolCallId as ToolCallId4, LlmError as LlmError3, createAssistantMessage as createAssistantMessage3, createUserMessage as createUserMessage3, errorChain as errorChain3 } from "@deepseek-ai/dsh-llm";
3018
+ import { ToolCallId as ToolCallId5, LlmError as LlmError3, createAssistantMessage as createAssistantMessage3, createUserMessage as createUserMessage3, errorChain as errorChain3 } from "@deepseek-ai/dsh-llm";
3010
3019
  import { createScope as createScope3 } from "@deepseek-ai/dsh-scope";
3011
3020
  import { canonicalHeader as canonicalHeader3 } from "@deepseek-ai/dsh-session";
3012
3021
 
3013
3022
  // src/engine-pi/permission.ts
3014
3023
  var DEFAULT_PI_PERMISSION = {
3015
3024
  sandboxMode: "read-only",
3016
- tools: ["read", "grep", "find", "ls"]
3025
+ tools: ["read"]
3017
3026
  };
3018
- var WORKSPACE_WRITE_TOOLS = ["read", "grep", "find", "ls", "write", "edit"];
3027
+ var WORKSPACE_WRITE_TOOLS = ["read", "write", "edit"];
3019
3028
  var FULL_ACCESS_TOOLS = [];
3020
3029
  function toolsForSandbox(mode) {
3021
3030
  switch (mode) {
@@ -3226,7 +3235,7 @@ var PiRpcClient = class _PiRpcClient {
3226
3235
 
3227
3236
  // src/engine-pi/rpc/mapping.ts
3228
3237
  import {
3229
- ToolCallId as ToolCallId3,
3238
+ ToolCallId as ToolCallId4,
3230
3239
  createToolResultMessage as createToolResultMessage3
3231
3240
  } from "@deepseek-ai/dsh-llm";
3232
3241
  function mapUsage3(usage) {
@@ -3257,7 +3266,7 @@ function resultText(content) {
3257
3266
  }
3258
3267
  function mapToolResult(ev) {
3259
3268
  return createToolResultMessage3({
3260
- callId: ToolCallId3(ev.toolCallId),
3269
+ callId: ToolCallId4(ev.toolCallId),
3261
3270
  content: [{ type: "text", text: resultText(ev.result) || "(no content)" }],
3262
3271
  isError: ev.isError
3263
3272
  });
@@ -3267,10 +3276,6 @@ function mapToolResult(ev) {
3267
3276
  var PROVIDER3 = "pi";
3268
3277
  var NATIVE_MODEL_LABEL3 = "pi-native";
3269
3278
  var TOOLS_FLAG = "--tools";
3270
- function specsEqual(a, b) {
3271
- if (a === void 0) return false;
3272
- return a.cwd === b.cwd && a.env === b.env && a.argv.length === b.argv.length && a.argv.every((value, index) => value === b.argv[index]);
3273
- }
3274
3279
  var PiAgent = class {
3275
3280
  constructor(loopCtx, id, options, session, config, spawn4, bin, catalog) {
3276
3281
  this.loopCtx = loopCtx;
@@ -3322,18 +3327,39 @@ var PiAgent = class {
3322
3327
  requestHeaderLogged = false;
3323
3328
  /** Agent-lifecycle-local counter naming each streamed attempt. */
3324
3329
  streamAttempts = 0;
3325
- /** Lazily created RPC client, reused across steps and released on scope teardown. */
3330
+ /** This step's RPC child; released by the step teardown and the scope teardown. */
3326
3331
  rpc;
3327
- /** The spawn spec the cached client was built from; a change forces a respawn. */
3328
- lastSpec;
3329
- /** Return the cached RPC client, respawning when the spec or process changed. */
3330
- async rpcClient(cwd) {
3331
- const spec = this.spawnSpec(cwd);
3332
- if (this.rpc !== void 0 && !this.rpc.closed && specsEqual(this.lastSpec, spec)) return this.rpc;
3333
- this.rpc?.dispose();
3334
- const client = PiRpcClient.create(spec, this.spawn);
3332
+ /**
3333
+ * Tool results logged into the currently open step. A result means the
3334
+ * segment that requested the call is finished, so the next assistant content
3335
+ * opens the next step (see {@link beginSegment}).
3336
+ */
3337
+ stepSettledTools = 0;
3338
+ /**
3339
+ * Rotate to the next step when the segment that ran a tool has finished, so
3340
+ * each assistant segment lands in its own step.
3341
+ *
3342
+ * Called as new assistant content begins. A step holding a settled tool
3343
+ * result means the previous segment is complete, and the content about to be
3344
+ * written belongs to the next one. Rotating at a message boundary keeps calls
3345
+ * announced together — one model turn — in one step.
3346
+ * @param phase - the running phase carrying the open step's position.
3347
+ */
3348
+ beginSegment(phase) {
3349
+ if (this.stepSettledTools === 0) return;
3350
+ this.session.append("step/end", { turn: phase.turn, step: phase.step });
3351
+ phase.step += 1;
3352
+ this.session.append("step/start", { turn: phase.turn, step: phase.step });
3353
+ this.stepSettledTools = 0;
3354
+ }
3355
+ /**
3356
+ * Open this step's RPC child. The Pi RPC process is single-session, so a step
3357
+ * never reuses one: the step teardown disposes it and the next step respawns
3358
+ * a fresh child.
3359
+ */
3360
+ rpcClient(cwd) {
3361
+ const client = PiRpcClient.create(this.spawnSpec(cwd), this.spawn);
3335
3362
  this.rpc = client;
3336
- this.lastSpec = spec;
3337
3363
  return client;
3338
3364
  }
3339
3365
  get status() {
@@ -3565,7 +3591,7 @@ var PiAgent = class {
3565
3591
  const stepEnd = await this.step();
3566
3592
  if (turnEnds === null) turnEnds = stepEnd;
3567
3593
  } finally {
3568
- this.session.append("step/end", { turn, step });
3594
+ this.session.append("step/end", { turn, step: phase.step });
3569
3595
  }
3570
3596
  signal.throwIfAborted();
3571
3597
  if (turnEnds && this.inbox.nextStep.length === 0) {
@@ -3688,8 +3714,10 @@ var PiAgent = class {
3688
3714
  */
3689
3715
  async step() {
3690
3716
  if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
3691
- const { turn, step, abort: { signal } } = this.phase;
3717
+ const phase = this.phase;
3718
+ const { abort: { signal } } = phase;
3692
3719
  signal.throwIfAborted();
3720
+ this.stepSettledTools = 0;
3693
3721
  const cwd = this.session.header.cwd;
3694
3722
  if (cwd === void 0 || cwd.length === 0) {
3695
3723
  throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
@@ -3716,15 +3744,15 @@ var PiAgent = class {
3716
3744
  live = new DriverAssistantStream(
3717
3745
  this.id,
3718
3746
  ++this.streamAttempts,
3719
- turn,
3720
- step,
3747
+ phase.turn,
3748
+ phase.step,
3721
3749
  (frame2) => this.dispatch.emit("agent/assistant-stream", { frame: frame2 })
3722
3750
  );
3723
3751
  live.start();
3724
3752
  }
3725
3753
  return live;
3726
3754
  };
3727
- const client = await this.rpcClient(cwd);
3755
+ const client = this.rpcClient(cwd);
3728
3756
  signal.throwIfAborted();
3729
3757
  await client.newSession();
3730
3758
  client.clearEvents();
@@ -3736,14 +3764,17 @@ var PiAgent = class {
3736
3764
  const startedReasoning = /* @__PURE__ */ new Set();
3737
3765
  const thinkingByIndex = /* @__PURE__ */ new Map();
3738
3766
  const emittedToolCalls = /* @__PURE__ */ new Set();
3767
+ let pendingToolCalls = [];
3768
+ let pendingCallLog = [];
3769
+ let callsToLog = [];
3739
3770
  let lastUsage;
3740
3771
  let assistantFlushed = false;
3741
3772
  const flushHeld = (usage) => {
3742
3773
  if (held === void 0) return;
3743
3774
  const attempt = live;
3744
3775
  const data = {
3745
- turn,
3746
- step,
3776
+ turn: phase.turn,
3777
+ step: phase.step,
3747
3778
  message: createAssistantMessage3({
3748
3779
  content: held.content,
3749
3780
  source: { provider: PROVIDER3, model: this.modelLabel() }
@@ -3759,6 +3790,16 @@ var PiAgent = class {
3759
3790
  live = void 0;
3760
3791
  }
3761
3792
  held = void 0;
3793
+ for (const call of callsToLog) {
3794
+ this.session.append("tool/call", {
3795
+ turn: phase.turn,
3796
+ step: phase.step,
3797
+ callId: ToolCallId5(call.callId),
3798
+ name: call.name,
3799
+ arguments: call.arguments
3800
+ });
3801
+ }
3802
+ callsToLog = [];
3762
3803
  };
3763
3804
  const ensureTextBlock = (index) => {
3764
3805
  if (startedText.has(index)) return;
@@ -3774,13 +3815,17 @@ var PiAgent = class {
3774
3815
  if (emittedToolCalls.has(callId)) return;
3775
3816
  emittedToolCalls.add(callId);
3776
3817
  const argumentsValue = typeof rawArguments === "string" ? rawArguments : JSON.stringify(rawArguments ?? {});
3777
- this.session.append("tool/call", {
3778
- turn,
3779
- step,
3780
- callId: ToolCallId4(callId),
3781
- name: name2,
3782
- arguments: argumentsValue
3783
- });
3818
+ pendingToolCalls.push({ type: "tool-call", id: ToolCallId5(callId), name: name2, arguments: argumentsValue });
3819
+ pendingCallLog.push({ callId, name: name2, arguments: argumentsValue });
3820
+ };
3821
+ const ensureToolCallOwner = () => {
3822
+ if (pendingToolCalls.length === 0) return;
3823
+ flushHeld();
3824
+ held = { content: [...pendingToolCalls] };
3825
+ pendingToolCalls = [];
3826
+ callsToLog = pendingCallLog;
3827
+ pendingCallLog = [];
3828
+ flushHeld();
3784
3829
  };
3785
3830
  const contentOf = (message) => {
3786
3831
  let blocks = [];
@@ -3796,6 +3841,12 @@ var PiAgent = class {
3796
3841
  const folded = [...thinkingByIndex.entries()].sort((a, b) => a[0] - b[0]).map(([, text]) => ({ type: "reasoning", text }));
3797
3842
  blocks = [...folded, ...blocks];
3798
3843
  }
3844
+ if (pendingToolCalls.length > 0) {
3845
+ blocks = [...blocks, ...pendingToolCalls];
3846
+ pendingToolCalls = [];
3847
+ callsToLog = pendingCallLog;
3848
+ pendingCallLog = [];
3849
+ }
3799
3850
  return blocks;
3800
3851
  };
3801
3852
  signal.throwIfAborted();
@@ -3813,6 +3864,7 @@ var PiAgent = class {
3813
3864
  break;
3814
3865
  case "message_start":
3815
3866
  if (event.message.role === "assistant") {
3867
+ this.beginSegment(phase);
3816
3868
  startedText.clear();
3817
3869
  startedReasoning.clear();
3818
3870
  thinkingByIndex.clear();
@@ -3864,21 +3916,26 @@ var PiAgent = class {
3864
3916
  break;
3865
3917
  case "tool_execution_end":
3866
3918
  emitToolCall(event.toolCallId, event.toolName, void 0);
3919
+ ensureToolCallOwner();
3867
3920
  this.session.append("tool/result", {
3868
- turn,
3869
- step,
3921
+ turn: phase.turn,
3922
+ step: phase.step,
3870
3923
  message: mapToolResult({ toolCallId: event.toolCallId, result: event.result, isError: event.isError })
3871
3924
  }, { surfaceOp: "append" });
3925
+ this.stepSettledTools += 1;
3872
3926
  break;
3873
3927
  case "turn_end": {
3874
3928
  if (!assistantFlushed && event.message !== void 0) {
3875
3929
  if (event.message.usage !== void 0) lastUsage = mapUsage3(event.message.usage);
3876
3930
  held = { content: contentOf(event.message) };
3877
3931
  }
3932
+ flushHeld(lastUsage);
3933
+ assistantFlushed = true;
3878
3934
  for (const toolResult2 of event.toolResults ?? []) {
3879
- this.appendToolResult(turn, step, toolResult2);
3935
+ ensureToolCallOwner();
3936
+ this.appendToolResult(phase.turn, phase.step, toolResult2);
3937
+ this.stepSettledTools += 1;
3880
3938
  }
3881
- flushHeld(lastUsage);
3882
3939
  finished = true;
3883
3940
  break;
3884
3941
  }
@@ -4051,14 +4108,9 @@ function fromSubprocess(handle) {
4051
4108
  terminate: () => handle.terminate()
4052
4109
  };
4053
4110
  }
4054
- var PiLoop = class extends Service3 {
4111
+ var PiLoop = class extends HostedLoopFactory {
4055
4112
  /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
4056
4113
  static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
4057
- /** Validated configuration owned by the loop plugin. */
4058
- config;
4059
- ownership;
4060
- /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
4061
- runtime;
4062
4114
  /** Process-tree spawn capability handed to every agent, sandboxed by the subprocess seam. */
4063
4115
  spawn;
4064
4116
  /** Resolved Pi CLI entrypoint; `argv[0]` of every Pi RPC child. */
@@ -4066,10 +4118,7 @@ var PiLoop = class extends Service3 {
4066
4118
  /** Discovered Pi model catalog, forwarded to each agent so it can validate the session-selected model against what pi can actually serve. */
4067
4119
  catalog;
4068
4120
  constructor(ctx, config) {
4069
- super(ctx, "agentLoopPi");
4070
- this.config = resolveConfig3(config);
4071
- this.ownership = new FactoryOwnership(ctx.fiber);
4072
- this.runtime = { ctx };
4121
+ super(ctx, "agentLoopPi", resolveConfig3(config));
4073
4122
  this.bin = piCliEntrypoint();
4074
4123
  this.catalog = config.piCatalogHolder ?? { entries: [] };
4075
4124
  this.spawn = (spec) => fromSubprocess(this.runtime.ctx.subprocess.spawn(piSubprocessSpec(spec, PI_DISPOSE_GRACE_MS)));
@@ -4081,318 +4130,28 @@ var PiLoop = class extends Service3 {
4081
4130
  holder.entries = [];
4082
4131
  });
4083
4132
  }
4084
- ctx.effect(() => () => this.ownership.dispose(), "agentLoopPi.transactions()");
4085
- ctx.effect(() => ctx.agents.setFactory(this), "agentLoopPi.setFactory()");
4086
- ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
4087
- ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
4088
- ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
4089
4133
  }
4090
- /**
4091
- * Construct the driver, scope, and one memoized reverse teardown for a new
4092
- * agent. The teardown is registered with the factory and the owner fiber
4093
- * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
4094
- * fuses caller cancellation with lifecycle teardown for setup awaits.
4095
- */
4096
- /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
4097
- prepare(ownerCtx, id, options, session, callerSignal, handle, parentAgent) {
4098
- ownerCtx.fiber.assertActive();
4099
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
4100
- if (callerSignal?.aborted) {
4101
- throw callerSignal.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal.reason });
4102
- }
4103
- const loopCtx = this.runtime.ctx;
4104
- const abort = new AbortController();
4105
- const onCallerAbort = () => {
4106
- abort.abort(callerSignal?.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal?.reason }));
4107
- };
4108
- const onFactoryTeardown = () => {
4109
- abort.abort(this.ownership.signal.reason);
4110
- };
4111
- callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
4112
- this.ownership.signal.addEventListener("abort", onFactoryTeardown, { once: true });
4113
- let machine;
4114
- let detachSession;
4115
- let detachAgent;
4116
- let disposing;
4117
- const machineReady = Promise.withResolvers();
4118
- const dispose = (ownerTriggered = false) => disposing ??= (async () => {
4119
- abort.abort(new Error(`agent "${id}" lifecycle disposed`));
4120
- callerSignal?.removeEventListener("abort", onCallerAbort);
4121
- this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
4122
- try {
4123
- if (machine === void 0) await machineReady.promise;
4124
- if (machine !== void 0) {
4125
- machine.cancel({ kind: "disposed" });
4126
- await machine.whenIdle();
4127
- await machine.scope.dispose();
4128
- }
4129
- } finally {
4130
- try {
4131
- await handle?.close();
4132
- } finally {
4133
- try {
4134
- detachAgent?.();
4135
- detachSession?.();
4136
- } finally {
4137
- untrack();
4138
- if (!ownerTriggered) await unfollowOwner();
4139
- }
4140
- }
4141
- }
4142
- })();
4143
- const untrack = this.ownership.track(dispose);
4144
- let unfollowOwner;
4145
- try {
4146
- unfollowOwner = ownerCtx.effect(() => () => {
4147
- if (disposing !== void 0) return;
4148
- abort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
4149
- return dispose(true);
4150
- }, `agentLoopPi.lifecycle(${id})`);
4151
- } catch (error) {
4152
- untrack();
4153
- callerSignal?.removeEventListener("abort", onCallerAbort);
4154
- this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
4155
- throw error;
4156
- }
4157
- const assertLive = () => {
4158
- if (!abort.signal.aborted) return;
4159
- throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
4160
- };
4161
- try {
4162
- const agent = machine = new PiAgent(
4163
- loopCtx,
4164
- id,
4165
- options,
4166
- session,
4167
- this.config,
4168
- this.spawn,
4169
- this.bin,
4170
- this.catalog
4171
- );
4172
- machineReady.resolve();
4173
- assertLive();
4174
- return {
4175
- agent,
4176
- signal: abort.signal,
4177
- publish: (source) => {
4178
- assertLive();
4179
- detachSession = agent.ctx.sessions.enter(session);
4180
- detachAgent = loopCtx.agents.enter(agent, parentAgent);
4181
- agent.ctx.sessions.announce(session);
4182
- assertLive();
4183
- loopCtx.agents.announce(agent);
4184
- assertLive();
4185
- emitAgentEvent3(loopCtx, agent, "agent/session-start", { source });
4186
- assertLive();
4187
- return { agent, dispose };
4188
- },
4189
- dispose
4190
- };
4191
- } catch (error) {
4192
- machineReady.resolve();
4193
- void dispose();
4194
- throw error;
4195
- }
4196
- }
4197
- /** Prepare one Agent around an acquired Session, run setup, and publish it. */
4198
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored, parentAgent) {
4199
- var _stack = [];
4200
- try {
4201
- const ownedPreparation = __using(_stack, preparation);
4202
- const session = ownedPreparation.session;
4203
- let prepared;
4204
- try {
4205
- prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle, parentAgent);
4206
- } catch (error) {
4207
- await stored?.handle.close().catch(() => {
4208
- });
4209
- throw error;
4210
- }
4211
- try {
4212
- const setupCommit = await raceAbort(setup?.(prepared.agent.ctx, prepared.agent), prepared.signal, id);
4213
- setupCommit?.commit();
4214
- await this.appendUnstoredSuffix(stored, session);
4215
- return prepared.publish(source);
4216
- } catch (error) {
4217
- await prepared.dispose().catch(() => {
4218
- });
4219
- throw error;
4220
- }
4221
- } catch (_) {
4222
- var _error = _, _hasError = true;
4223
- } finally {
4224
- __callDispose(_stack, _error, _hasError);
4225
- }
4226
- }
4227
- /**
4228
- * Create an agent and session under one caller-supplied identity, owned by
4229
- * the accessing fiber. When a persistence backend is mounted, the session's
4230
- * durable identity is stored before publication.
4231
- * @param ownerCtx - caller context that structurally owns the lifecycle.
4232
- * @param options - identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.
4233
- * @returns the published handle.
4234
- */
4235
- async createAgent(ownerCtx, options) {
4236
- const preparation = SessionPreparation3.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
4237
- ...options.seed === void 0 ? {} : { seed: options.seed },
4238
- ...options.meta === void 0 ? {} : { meta: options.meta },
4239
- ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
4240
- }));
4241
- const published = (async () => {
4242
- let stored;
4243
- try {
4244
- stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
4245
- () => this.createStoredSession(preparation.session, options.signal),
4246
- options.signal,
4247
- options.sessionId,
4248
- (abandoned) => {
4249
- void abandoned?.handle.close().catch(() => {
4250
- });
4251
- }
4252
- );
4253
- } catch (error) {
4254
- preparation[Symbol.dispose]();
4255
- throw error;
4256
- }
4257
- return this.setupAndPublish(
4258
- ownerCtx,
4259
- options.sessionId,
4260
- preparation,
4261
- options.agentOptions ?? {},
4262
- options.setup,
4263
- options.signal,
4264
- "startup",
4265
- stored,
4266
- options.parentAgent
4267
- );
4268
- })();
4269
- this.ownership.trackWrapper(published);
4270
- return published;
4271
- }
4272
- /**
4273
- * Take a fresh session's write ownership when persistence is mounted.
4274
- * Nothing is appended here: the constructor seed (which never re-emits
4275
- * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
4276
- * publication commit point, so a failed or cancelled setup closes an
4277
- * unmaterialized handle and leaves no stored residue — the same id can be
4278
- * created again.
4279
- * @param session - the unpublished session to store.
4280
- * @param signal - optional cancellation forwarded to the backend create.
4281
- * @returns the owned handle and stored cursor, or `undefined` without a backend.
4282
- */
4283
- async createStoredSession(session, signal) {
4284
- const persistence = this.runtime.ctx.get("sessionPersistence");
4285
- if (persistence === void 0) return void 0;
4286
- const handle = await persistence.create(session.header, {
4287
- inheritedEventCount: session.inheritedEventCount,
4288
- ...signal === void 0 ? {} : { signal }
4289
- });
4290
- return { handle, storedCount: 0 };
4291
- }
4292
- /**
4293
- * Durably store the session events appended since the last stored cursor.
4294
- * Pre-publication appends (constructor seed markers, setup-window events)
4295
- * never re-emit through `session/event`, so publication must flush them
4296
- * through the handle before live events start routing into it.
4297
- * @param stored - the session's owned handle and stored cursor, if any.
4298
- * @param session - the unpublished session whose suffix is stored.
4299
- */
4300
- async appendUnstoredSuffix(stored, session) {
4301
- if (stored === void 0) return;
4302
- const suffix = session.snapshotEvents(SessionLogOffset3(stored.storedCount));
4303
- if (suffix.length > 0) await stored.handle.append(suffix);
4304
- stored.storedCount += suffix.length;
4305
- }
4306
- /**
4307
- * Resume an owned agent from the configured persistence service.
4308
- * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
4309
- * @param options - persisted identity, optional live parent, loop options, setup, and cancellation.
4310
- * @returns the published handle.
4311
- */
4312
- async resume(ownerCtx, options) {
4313
- const persistence = this.runtime.ctx.get("sessionPersistence");
4314
- if (persistence === void 0) {
4315
- throw new Error("cannot resume: session persistence is not configured (load a dsh-session-persistence backend)");
4316
- }
4317
- return this.resumeWith(ownerCtx, persistence, options);
4318
- }
4319
- /** Resume through an explicit persistence service. */
4320
- resumeWith(ownerCtx, persistence, options) {
4321
- const id = options.resumeSessionId;
4322
- const published = (async () => {
4323
- const ownerAbort = new AbortController();
4324
- const unfollowOwner = ownerCtx.effect(() => () => {
4325
- ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
4326
- }, `agentLoopPi.resume-load(${id})`);
4327
- const fused = AbortSignal.any([
4328
- ...options.signal === void 0 ? [] : [options.signal],
4329
- ownerAbort.signal,
4330
- this.ownership.signal
4331
- ]);
4332
- let handle;
4333
- let stored;
4334
- let preparation;
4335
- try {
4336
- try {
4337
- handle = await raceAbortCall(
4338
- () => persistence.open(id, "write", { signal: fused }),
4339
- fused,
4340
- id,
4341
- (abandoned) => {
4342
- void abandoned.close();
4343
- }
4344
- );
4345
- const coldRead = await handle.read(0, void 0, { signal: fused });
4346
- fused.throwIfAborted();
4347
- const persisted = coldRead.events;
4348
- const closers = interruptedTurnClosers3(persisted);
4349
- if (closers.length > 0) await handle.append(closers);
4350
- preparation = SessionPreparation3.create(this.runtime.ctx.sessions.prepare(id, {
4351
- seed: [...persisted, ...closers],
4352
- meta: structuredClone(handle.header),
4353
- inheritedEventCount: handle.inheritedEventCount,
4354
- eventState: coldRead.eventState
4355
- }));
4356
- stored = { handle, storedCount: persisted.length + closers.length };
4357
- await this.appendUnstoredSuffix(stored, preparation.session);
4358
- } finally {
4359
- await unfollowOwner();
4360
- }
4361
- ownerCtx.fiber.assertActive();
4362
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
4363
- const owned = stored;
4364
- handle = void 0;
4365
- return await this.setupAndPublish(
4366
- ownerCtx,
4367
- id,
4368
- preparation,
4369
- options.agentOptions ?? {},
4370
- options.setup,
4371
- options.signal,
4372
- "resume",
4373
- owned,
4374
- options.parentAgent
4375
- );
4376
- } finally {
4377
- preparation?.[Symbol.dispose]();
4378
- await handle?.close().catch(() => {
4379
- });
4380
- }
4381
- })();
4382
- this.ownership.trackWrapper(published);
4383
- return published;
4134
+ /** Construct the Pi RPC driver for one prepared session. */
4135
+ buildAgent(loopCtx, id, options, session) {
4136
+ return new PiAgent(
4137
+ loopCtx,
4138
+ id,
4139
+ options,
4140
+ session,
4141
+ this.config,
4142
+ this.spawn,
4143
+ this.bin,
4144
+ this.catalog
4145
+ );
4384
4146
  }
4385
4147
  };
4386
4148
 
4387
4149
  // src/engine-kimi/loop.ts
4388
- import { Service as Service4 } from "@deepseek-ai/cordis";
4389
4150
  import z4 from "@deepseek-ai/schemastery";
4390
- import { emitAgentEvent as emitAgentEvent4 } from "@deepseek-ai/dsh-agent";
4391
- import { interruptedTurnClosers as interruptedTurnClosers4, SessionLogOffset as SessionLogOffset4, SessionPreparation as SessionPreparation4 } from "@deepseek-ai/dsh-session";
4392
4151
 
4393
4152
  // src/engine-kimi/agent.ts
4394
4153
  import { agentEvents as agentEvents4 } from "@deepseek-ai/dsh-agent";
4395
- import { ToolCallId as ToolCallId6, LlmError as LlmError4, createAssistantMessage as createAssistantMessage4, createUserMessage as createUserMessage4, errorChain as errorChain4 } from "@deepseek-ai/dsh-llm";
4154
+ import { ToolCallId as ToolCallId7, LlmError as LlmError4, createAssistantMessage as createAssistantMessage4, createUserMessage as createUserMessage4, errorChain as errorChain4 } from "@deepseek-ai/dsh-llm";
4396
4155
  import { createScope as createScope4 } from "@deepseek-ai/dsh-scope";
4397
4156
  import { canonicalHeader as canonicalHeader4 } from "@deepseek-ai/dsh-session";
4398
4157
 
@@ -4450,6 +4209,17 @@ function isPermissionRequestFrame(frame2) {
4450
4209
  }
4451
4210
 
4452
4211
  // src/engine-kimi/acp/client.ts
4212
+ function permissionOptionsOf(frame2) {
4213
+ const params = frame2.params;
4214
+ if (params === void 0 || !Array.isArray(params.options)) return [];
4215
+ return params.options.filter((option) => typeof option === "object" && option !== null && typeof option.optionId === "string");
4216
+ }
4217
+ function permissionResponse(approved, options) {
4218
+ const candidates = options.filter((option) => option.kind === (approved ? "allow_once" : "reject_once"));
4219
+ const match = candidates.length === 1 ? candidates[0] : void 0;
4220
+ if (match === void 0) return { outcome: { outcome: "cancelled" } };
4221
+ return { outcome: { outcome: "selected", optionId: match.optionId } };
4222
+ }
4453
4223
  function defaultSpawn2(spec) {
4454
4224
  const child = spawn3(spec.argv[0], spec.argv.slice(1), {
4455
4225
  cwd: spec.cwd,
@@ -4558,9 +4328,13 @@ var AcpClient = class _AcpClient {
4558
4328
  cancel(sessionId) {
4559
4329
  this.request("session/cancel", { sessionId }).catch(() => void 0);
4560
4330
  }
4561
- /** Answer a pending `session/request_permission`. */
4562
- respondPermission(id, approved) {
4563
- this.process.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, result: { approved } })}
4331
+ /**
4332
+ * Answer a pending `session/request_permission`.
4333
+ * @param id - the reverse-RPC request id.
4334
+ * @param response - the ACP outcome to answer with.
4335
+ */
4336
+ respondPermission(id, response) {
4337
+ this.process.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, result: response })}
4564
4338
  `);
4565
4339
  }
4566
4340
  /** Consume every buffered update as an async generator. */
@@ -4642,12 +4416,12 @@ var AcpClient = class _AcpClient {
4642
4416
  }
4643
4417
  async handlePermission(id, frame2) {
4644
4418
  const approved = this.permissionHandler === void 0 ? false : await this.permissionHandler(frame2);
4645
- this.respondPermission(id, approved);
4419
+ this.respondPermission(id, permissionResponse(approved, permissionOptionsOf(frame2)));
4646
4420
  }
4647
4421
  };
4648
4422
 
4649
4423
  // src/engine-kimi/acp/mapping.ts
4650
- import { ToolCallId as ToolCallId5, createToolResultMessage as createToolResultMessage4 } from "@deepseek-ai/dsh-llm";
4424
+ import { ToolCallId as ToolCallId6, createToolResultMessage as createToolResultMessage4 } from "@deepseek-ai/dsh-llm";
4651
4425
  function isTextChunk(update) {
4652
4426
  return update.sessionUpdate === "agent_message_chunk";
4653
4427
  }
@@ -4672,6 +4446,11 @@ function toolCallIdOf(update) {
4672
4446
  function toolCallName(update) {
4673
4447
  return update.title;
4674
4448
  }
4449
+ function toolRawInput(update) {
4450
+ const raw = update.rawInput;
4451
+ if (raw === void 0) return void 0;
4452
+ return typeof raw === "string" ? raw : JSON.stringify(raw);
4453
+ }
4675
4454
  function isToolSettledStatus(status) {
4676
4455
  return status !== "pending" && status !== "queued" && status !== "running" && status !== "in_progress";
4677
4456
  }
@@ -4679,12 +4458,13 @@ function isToolErrorStatus(status) {
4679
4458
  return status === "failed" || status === "error" || status === "denied";
4680
4459
  }
4681
4460
  function toolContentText(update) {
4682
- const blocks = update.content ?? [];
4461
+ const blocks = update.content;
4462
+ if (!Array.isArray(blocks)) return void 0;
4683
4463
  return blocks.map((block) => block.type === "content" && block.content.type === "text" ? block.content.text : "").join("");
4684
4464
  }
4685
4465
  function toolResult(callId, text, isError) {
4686
4466
  return createToolResultMessage4({
4687
- callId: ToolCallId5(callId),
4467
+ callId: ToolCallId6(callId),
4688
4468
  content: [{ type: "text", text: text.length > 0 ? text : "(no content)" }],
4689
4469
  isError
4690
4470
  });
@@ -4965,7 +4745,7 @@ var KimiAgent = class {
4965
4745
  const stepEnd = await this.step();
4966
4746
  if (turnEnds === null) turnEnds = stepEnd;
4967
4747
  } finally {
4968
- this.session.append("step/end", { turn, step });
4748
+ this.session.append("step/end", { turn, step: phase.step });
4969
4749
  }
4970
4750
  signal.throwIfAborted();
4971
4751
  if (turnEnds && this.inbox.nextStep.length === 0) {
@@ -5028,507 +4808,313 @@ var KimiAgent = class {
5028
4808
  const client = AcpClient.create(spec, this.spawn);
5029
4809
  this.acp = client;
5030
4810
  this.lastSpec = spec;
5031
- try {
5032
- await client.initialize();
5033
- } catch (error) {
5034
- this.acp = void 0;
5035
- this.lastSpec = void 0;
5036
- client.dispose();
5037
- throw error;
5038
- }
5039
- return client;
5040
- }
5041
- /** Build the `kimi acp` argv/cwd/env for the persistent child. */
5042
- spawnSpec(cwd) {
5043
- return {
5044
- argv: kimiAcpArgv(this.bin),
5045
- cwd,
5046
- env: this.config.env
5047
- };
5048
- }
5049
- /** Run one `kimi acp` step for the current session history and map the streamed updates. */
5050
- async step() {
5051
- if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
5052
- const { turn, step, abort: { signal } } = this.phase;
5053
- signal.throwIfAborted();
5054
- this.blocks = [];
5055
- this.emittedToolCalls = /* @__PURE__ */ new Set();
5056
- this.toolText = /* @__PURE__ */ new Map();
5057
- const cwd = this.session.header.cwd;
5058
- if (cwd === void 0 || cwd.length === 0) {
5059
- throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
5060
- }
5061
- const history = this.session.deriveMessages();
5062
- const prompt = serializeHistory(history);
5063
- if (prompt.length === 0) {
5064
- throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
5065
- }
5066
- this.assertRequestHeader();
5067
- signal.throwIfAborted();
5068
- const client = await this.acpClient(cwd);
5069
- signal.throwIfAborted();
5070
- client.onPermission(() => resolveToolApproval(this.session.snapshotEvents()));
5071
- const acpSessionId = await client.newSession(cwd);
5072
- signal.throwIfAborted();
5073
- let live;
5074
- try {
5075
- const currentStream = () => {
5076
- if (live === void 0) {
5077
- live = new DriverAssistantStream(
5078
- this.id,
5079
- ++this.streamAttempts,
5080
- turn,
5081
- step,
5082
- (frame2) => this.dispatch.emit("agent/assistant-stream", { frame: frame2 })
5083
- );
5084
- live.start();
5085
- }
5086
- return live;
5087
- };
5088
- client.onUpdate((update) => this.applyUpdate(turn, step, update, currentStream));
5089
- const cancel = () => {
5090
- client.cancel(acpSessionId);
5091
- };
5092
- signal.addEventListener("abort", cancel, { once: true });
5093
- try {
5094
- await raceAbort(client.prompt(acpSessionId, prompt), signal, this.id);
5095
- } finally {
5096
- signal.removeEventListener("abort", cancel);
5097
- }
5098
- this.flushAssistant(turn, step, currentStream);
5099
- if (this.blocks.length === 0 && this.emittedToolCalls.size === 0) {
5100
- throw new LlmError4(
5101
- `agent "${this.id}": kimi query produced no assistant output`,
5102
- "KIMI_NO_RESULT"
5103
- );
5104
- }
5105
- return { kind: "completed" };
5106
- } finally {
5107
- if (live !== void 0 && !live.ended) live.abandon();
5108
- }
5109
- }
5110
- /** Per-step accumulation state for streamed assistant blocks and tool calls. */
5111
- blocks = [];
5112
- emittedToolCalls = /* @__PURE__ */ new Set();
5113
- toolText = /* @__PURE__ */ new Map();
5114
- blockRef(type) {
5115
- return this.blocks.find((block) => block.type === type);
5116
- }
5117
- ensureBlock(type) {
5118
- const existing = this.blockRef(type);
5119
- if (existing !== void 0) return existing;
5120
- const index = this.blocks.length;
5121
- const block = { index, type, text: "" };
5122
- this.blocks.push(block);
5123
- return block;
5124
- }
5125
- /** Apply one streamed update to the current step's blocks and stream. */
5126
- applyUpdate(turn, step, update, currentStream) {
5127
- if (isThoughtChunk(update)) {
5128
- const delta = chunkDelta(update);
5129
- if (delta === "") return;
5130
- const block = this.ensureBlock("reasoning");
5131
- if (block.text === "") currentStream().push({ type: "block-start", index: block.index, blockType: "reasoning" });
5132
- currentStream().push({ type: "reasoning-delta", index: block.index, text: delta });
5133
- block.text += delta;
5134
- return;
5135
- }
5136
- if (isTextChunk(update)) {
5137
- const delta = chunkDelta(update);
5138
- if (delta === "") return;
5139
- const block = this.ensureBlock("text");
5140
- if (block.text === "") currentStream().push({ type: "block-start", index: block.index, blockType: "text" });
5141
- currentStream().push({ type: "text-delta", index: block.index, text: delta });
5142
- block.text += delta;
5143
- return;
5144
- }
5145
- if (isToolCall(update)) {
5146
- const callId = toolCallIdOf(update);
5147
- if (callId === "" || this.emittedToolCalls.has(callId)) return;
5148
- this.emittedToolCalls.add(callId);
5149
- const name2 = toolCallName(update);
5150
- this.session.append("tool/call", { turn, step, callId: ToolCallId6(callId), name: name2, arguments: "{}" });
5151
- this.toolText.set(callId, "");
5152
- return;
5153
- }
5154
- if (isToolCallUpdate(update)) {
5155
- const callId = toolCallIdOf(update);
5156
- if (callId === "" || !this.toolText.has(callId)) return;
5157
- const delta = toolContentText(update);
5158
- const accumulated = `${this.toolText.get(callId)}${delta}`;
5159
- this.toolText.set(callId, accumulated);
5160
- const status = update.status;
5161
- if (isToolSettledStatus(status)) {
5162
- const message = toolResult(callId, accumulated, isToolErrorStatus(status));
5163
- this.session.append("tool/result", { turn, step, message }, { surfaceOp: "append" });
5164
- this.toolText.delete(callId);
5165
- }
5166
- return;
5167
- }
5168
- }
5169
- /**
5170
- * Flush the accumulated assistant blocks into one durable assistant/message
5171
- * carrying the exact stream the attempt published live.
5172
- * @param turn - durable turn owning the message.
5173
- * @param step - durable step owning the message.
5174
- * @param currentStream - the step's live attempt accessor; a step whose
5175
- * blocks streamed always has one open, while a tool-only step has none.
5176
- */
5177
- flushAssistant(turn, step, currentStream) {
5178
- if (this.blocks.length === 0 && this.emittedToolCalls.size === 0) return;
5179
- const content = [];
5180
- const attempt = this.blocks.length === 0 ? void 0 : currentStream();
5181
- for (const block of this.blocks) {
5182
- const delta = block.text;
5183
- content.push(block.type === "text" ? { type: "text", text: delta } : { type: "reasoning", text: delta });
5184
- attempt?.push({ type: "block-end", index: block.index, block: block.type === "text" ? { type: "text", text: delta } : { type: "reasoning", text: delta } });
5185
- }
5186
- const data = {
5187
- turn,
5188
- step,
5189
- message: createAssistantMessage4({
5190
- content,
5191
- source: { provider: PROVIDER4, model: this.modelLabel() }
5192
- }),
5193
- // The attempt's exact timed stream travels with its message.
5194
- stream: attempt?.stream ?? []
5195
- };
5196
- if (attempt === void 0) {
5197
- this.session.append("assistant/message", data, { surfaceOp: "append" });
5198
- } else {
5199
- attempt.settle(() => this.session.append("assistant/message", data, { surfaceOp: "append" }).seq);
5200
- }
5201
- }
5202
- };
5203
-
5204
- // src/engine-kimi/loop.ts
5205
- var KIMI_DISPOSE_GRACE_MS = 3e3;
5206
- var Config4 = z4.object({
5207
- model: z4.string(),
5208
- env: z4.dict(z4.string()).default({}),
5209
- bin: z4.string()
5210
- });
5211
- function resolveConfig4(config) {
5212
- return {
5213
- model: config.model,
5214
- env: config.env ?? {},
5215
- bin: kimiBinResolver(config.bin)
5216
- };
5217
- }
5218
- var KimiLoop = class extends Service4 {
5219
- /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
5220
- static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
5221
- /** Validated configuration owned by the loop plugin. */
5222
- config;
5223
- ownership;
5224
- /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
5225
- runtime;
5226
- /** One-shot spawn capability handed to every agent, sandboxed by the subprocess seam. */
5227
- spawn;
5228
- constructor(ctx, config) {
5229
- super(ctx, "agentLoopKimi");
5230
- this.config = resolveConfig4(config);
5231
- this.ownership = new FactoryOwnership(ctx.fiber);
5232
- this.runtime = { ctx };
5233
- this.spawn = (spec) => fromSubprocess2(this.runtime.ctx.subprocess.spawn(kimiSubprocessSpec(spec, KIMI_DISPOSE_GRACE_MS)));
5234
- ctx.effect(() => () => this.ownership.dispose(), "agentLoopKimi.transactions()");
5235
- ctx.effect(() => ctx.agents.setFactory(this), "agentLoopKimi.setFactory()");
5236
- ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
5237
- ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
5238
- ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
5239
- }
5240
- /**
5241
- * Construct the driver, scope, and one memoized reverse teardown for a new
5242
- * agent. The teardown is registered with the factory and the owner fiber
5243
- * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
5244
- * fuses caller cancellation with lifecycle teardown for setup awaits.
5245
- */
5246
- /* jscpd:ignore-start -- ownership/transaction machinery mirrors the Claude Code loop factory. */
5247
- prepare(ownerCtx, id, options, session, callerSignal, handle, parentAgent) {
5248
- ownerCtx.fiber.assertActive();
5249
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
5250
- if (callerSignal?.aborted) {
5251
- throw callerSignal.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal.reason });
5252
- }
5253
- const loopCtx = this.runtime.ctx;
5254
- const abort = new AbortController();
5255
- const onCallerAbort = () => {
5256
- abort.abort(callerSignal?.reason instanceof Error ? callerSignal.reason : new Error(`agent "${id}" creation aborted`, { cause: callerSignal?.reason }));
5257
- };
5258
- const onFactoryTeardown = () => {
5259
- abort.abort(this.ownership.signal.reason);
5260
- };
5261
- callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
5262
- this.ownership.signal.addEventListener("abort", onFactoryTeardown, { once: true });
5263
- let machine;
5264
- let detachSession;
5265
- let detachAgent;
5266
- let disposing;
5267
- const machineReady = Promise.withResolvers();
5268
- const dispose = (ownerTriggered = false) => disposing ??= (async () => {
5269
- abort.abort(new Error(`agent "${id}" lifecycle disposed`));
5270
- callerSignal?.removeEventListener("abort", onCallerAbort);
5271
- this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
5272
- try {
5273
- if (machine === void 0) await machineReady.promise;
5274
- if (machine !== void 0) {
5275
- machine.cancel({ kind: "disposed" });
5276
- await machine.whenIdle();
5277
- await machine.scope.dispose();
5278
- }
5279
- } finally {
5280
- try {
5281
- await handle?.close();
5282
- } finally {
5283
- try {
5284
- detachAgent?.();
5285
- detachSession?.();
5286
- } finally {
5287
- untrack();
5288
- if (!ownerTriggered) await unfollowOwner();
5289
- }
5290
- }
5291
- }
5292
- })();
5293
- const untrack = this.ownership.track(dispose);
5294
- let unfollowOwner;
5295
- try {
5296
- unfollowOwner = ownerCtx.effect(() => () => {
5297
- if (disposing !== void 0) return;
5298
- abort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
5299
- return dispose(true);
5300
- }, `agentLoopKimi.lifecycle(${id})`);
5301
- } catch (error) {
5302
- untrack();
5303
- callerSignal?.removeEventListener("abort", onCallerAbort);
5304
- this.ownership.signal.removeEventListener("abort", onFactoryTeardown);
5305
- throw error;
5306
- }
5307
- const assertLive = () => {
5308
- if (!abort.signal.aborted) return;
5309
- throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
5310
- };
5311
- try {
5312
- const agent = machine = new KimiAgent(loopCtx, id, options, session, this.config, this.spawn, this.config.bin);
5313
- machineReady.resolve();
5314
- assertLive();
5315
- return {
5316
- agent,
5317
- signal: abort.signal,
5318
- publish: (source) => {
5319
- assertLive();
5320
- detachSession = agent.ctx.sessions.enter(session);
5321
- detachAgent = loopCtx.agents.enter(agent, parentAgent);
5322
- agent.ctx.sessions.announce(session);
5323
- assertLive();
5324
- loopCtx.agents.announce(agent);
5325
- assertLive();
5326
- emitAgentEvent4(loopCtx, agent, "agent/session-start", { source });
5327
- assertLive();
5328
- return { agent, dispose };
5329
- },
5330
- dispose
5331
- };
4811
+ try {
4812
+ await client.initialize();
5332
4813
  } catch (error) {
5333
- machineReady.resolve();
5334
- void dispose();
4814
+ this.acp = void 0;
4815
+ this.lastSpec = void 0;
4816
+ client.dispose();
5335
4817
  throw error;
5336
4818
  }
4819
+ return client;
5337
4820
  }
5338
- /** Prepare one Agent around an acquired Session, run setup, and publish it. */
5339
- async setupAndPublish(ownerCtx, id, preparation, agentOptions, setup, signal, source, stored, parentAgent) {
5340
- var _stack = [];
4821
+ /** Build the `kimi acp` argv/cwd/env for the persistent child. */
4822
+ spawnSpec(cwd) {
4823
+ return {
4824
+ argv: kimiAcpArgv(this.bin),
4825
+ cwd,
4826
+ env: this.config.env
4827
+ };
4828
+ }
4829
+ /** Run one `kimi acp` step for the current session history and map the streamed updates. */
4830
+ async step() {
4831
+ if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
4832
+ const phase = this.phase;
4833
+ const { abort: { signal } } = phase;
4834
+ signal.throwIfAborted();
4835
+ this.blocks = [];
4836
+ this.pendingCalls = /* @__PURE__ */ new Map();
4837
+ this.segmentCalls = [];
4838
+ this.toolContent = /* @__PURE__ */ new Map();
4839
+ this.producedOutput = false;
4840
+ this.stepSettledTools = 0;
4841
+ const cwd = this.session.header.cwd;
4842
+ if (cwd === void 0 || cwd.length === 0) {
4843
+ throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
4844
+ }
4845
+ const history = this.session.deriveMessages();
4846
+ const prompt = serializeHistory(history);
4847
+ if (prompt.length === 0) {
4848
+ throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
4849
+ }
4850
+ this.assertRequestHeader();
4851
+ signal.throwIfAborted();
4852
+ const client = await this.acpClient(cwd);
4853
+ signal.throwIfAborted();
4854
+ client.onPermission(() => resolveToolApproval(this.session.snapshotEvents()));
4855
+ const acpSessionId = await client.newSession(cwd);
4856
+ signal.throwIfAborted();
5341
4857
  try {
5342
- const ownedPreparation = __using(_stack, preparation);
5343
- const session = ownedPreparation.session;
5344
- let prepared;
4858
+ client.onUpdate((update) => this.applyUpdate(phase, update));
4859
+ const cancel = () => {
4860
+ client.cancel(acpSessionId);
4861
+ };
4862
+ signal.addEventListener("abort", cancel, { once: true });
5345
4863
  try {
5346
- prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle, parentAgent);
5347
- } catch (error) {
5348
- await stored?.handle.close().catch(() => {
5349
- });
5350
- throw error;
4864
+ await raceAbort(client.prompt(acpSessionId, prompt), signal, this.id);
4865
+ } finally {
4866
+ signal.removeEventListener("abort", cancel);
5351
4867
  }
5352
- try {
5353
- const setupCommit = await raceAbort(setup?.(prepared.agent.ctx, prepared.agent), prepared.signal, id);
5354
- setupCommit?.commit();
5355
- await this.appendUnstoredSuffix(stored, session);
5356
- return prepared.publish(source);
5357
- } catch (error) {
5358
- await prepared.dispose().catch(() => {
5359
- });
5360
- throw error;
4868
+ for (const [callId, call] of this.pendingCalls) {
4869
+ this.segmentCalls.push({ callId, name: call.name, arguments: "{}" });
5361
4870
  }
5362
- } catch (_) {
5363
- var _error = _, _hasError = true;
4871
+ this.pendingCalls.clear();
4872
+ this.flushSegment(phase);
4873
+ if (!this.producedOutput) {
4874
+ throw new LlmError4(
4875
+ `agent "${this.id}": kimi query produced no assistant output`,
4876
+ "KIMI_NO_RESULT"
4877
+ );
4878
+ }
4879
+ return { kind: "completed" };
5364
4880
  } finally {
5365
- __callDispose(_stack, _error, _hasError);
4881
+ if (this.live !== void 0 && !this.live.ended) this.live.abandon();
4882
+ this.live = void 0;
5366
4883
  }
5367
4884
  }
4885
+ /** Per-step accumulation state for streamed assistant blocks and tool calls. */
4886
+ blocks = [];
5368
4887
  /**
5369
- * Create an agent and session under one caller-supplied identity, owned by
5370
- * the accessing fiber. When a persistence backend is mounted, the session's
5371
- * durable identity is stored before publication.
5372
- * @param ownerCtx - caller context that structurally owns the lifecycle.
5373
- * @param options - identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.
5374
- * @returns the published handle.
4888
+ * Announced calls still waiting for the update that carries their input,
4889
+ * keyed by call id, holding the latest content snapshot seen meanwhile (a
4890
+ * frame can report output before it reports input). `toolContent` holds the
4891
+ * calls already logged, so a call is in exactly one of the two.
5375
4892
  */
5376
- async createAgent(ownerCtx, options) {
5377
- const preparation = SessionPreparation4.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
5378
- ...options.seed === void 0 ? {} : { seed: options.seed },
5379
- ...options.meta === void 0 ? {} : { meta: options.meta },
5380
- ...options.inheritedEventCount === void 0 ? {} : { inheritedEventCount: options.inheritedEventCount }
5381
- }));
5382
- const published = (async () => {
5383
- let stored;
5384
- try {
5385
- stored = options.signal === void 0 ? await this.createStoredSession(preparation.session) : await raceAbortCall(
5386
- () => this.createStoredSession(preparation.session, options.signal),
5387
- options.signal,
5388
- options.sessionId,
5389
- (abandoned) => {
5390
- void abandoned?.handle.close().catch(() => {
5391
- });
5392
- }
5393
- );
5394
- } catch (error) {
5395
- preparation[Symbol.dispose]();
5396
- throw error;
5397
- }
5398
- return this.setupAndPublish(
5399
- ownerCtx,
5400
- options.sessionId,
5401
- preparation,
5402
- options.agentOptions ?? {},
5403
- options.setup,
5404
- options.signal,
5405
- "startup",
5406
- stored,
5407
- options.parentAgent
5408
- );
5409
- })();
5410
- this.ownership.trackWrapper(published);
5411
- return published;
5412
- }
4893
+ pendingCalls = /* @__PURE__ */ new Map();
4894
+ /** Whether the current step published any assistant message at all. */
4895
+ producedOutput = false;
5413
4896
  /**
5414
- * Take a fresh session's write ownership when persistence is mounted.
5415
- * Nothing is appended here: the constructor seed (which never re-emits
5416
- * through `session/event`) is stored by {@link appendUnstoredSuffix} at the
5417
- * publication commit point, so a failed or cancelled setup closes an
5418
- * unmaterialized handle and leaves no stored residue — the same id can be
5419
- * created again.
5420
- * @param session - the unpublished session to store.
5421
- * @param signal - optional cancellation forwarded to the backend create.
5422
- * @returns the owned handle and stored cursor, or `undefined` without a backend.
4897
+ * Tool results logged into the currently open step. A result means the
4898
+ * segment that requested the call is finished, so the next assistant content
4899
+ * opens the next step (see {@link beginSegment}).
5423
4900
  */
5424
- async createStoredSession(session, signal) {
5425
- const persistence = this.runtime.ctx.get("sessionPersistence");
5426
- if (persistence === void 0) return void 0;
5427
- const handle = await persistence.create(session.header, {
5428
- inheritedEventCount: session.inheritedEventCount,
5429
- ...signal === void 0 ? {} : { signal }
5430
- });
5431
- return { handle, storedCount: 0 };
5432
- }
4901
+ stepSettledTools = 0;
5433
4902
  /**
5434
- * Durably store the session events appended since the last stored cursor.
5435
- * Pre-publication appends (constructor seed markers, setup-window events)
5436
- * never re-emit through `session/event`, so publication must flush them
5437
- * through the handle before live events start routing into it.
5438
- * @param stored - the session's owned handle and stored cursor, if any.
5439
- * @param session - the unpublished session whose suffix is stored.
4903
+ * Tool calls the current segment has already received input for, awaiting the
4904
+ * segment's single assistant message. A model turn that announces several
4905
+ * calls before any result must land them all in ONE message — the chat node
4906
+ * keys by `${turn}:${step}` and replaces blocks on every message, so a second
4907
+ * message would overwrite the first one's reasoning/text (and tool-call head).
4908
+ * The list is flushed once, when the segment closes (its first settled result).
5440
4909
  */
5441
- async appendUnstoredSuffix(stored, session) {
5442
- if (stored === void 0) return;
5443
- const suffix = session.snapshotEvents(SessionLogOffset4(stored.storedCount));
5444
- if (suffix.length > 0) await stored.handle.append(suffix);
5445
- stored.storedCount += suffix.length;
4910
+ segmentCalls = [];
4911
+ /** Latest content snapshot per logged tool call (see {@link applyUpdate}). */
4912
+ toolContent = /* @__PURE__ */ new Map();
4913
+ /** The live attempt framing the assistant message being assembled right now. */
4914
+ live;
4915
+ blockRef(type) {
4916
+ return this.blocks.find((block) => block.type === type);
4917
+ }
4918
+ ensureBlock(type) {
4919
+ const existing = this.blockRef(type);
4920
+ if (existing !== void 0) return existing;
4921
+ const index = this.blocks.length;
4922
+ const block = { index, type, text: "" };
4923
+ this.blocks.push(block);
4924
+ return block;
5446
4925
  }
5447
4926
  /**
5448
- * Resume an owned agent from the configured persistence service.
5449
- * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
5450
- * @param options - persisted identity, optional live parent, loop options, setup, and cancellation.
5451
- * @returns the published handle.
4927
+ * Rotate to the next step when the segment that ran a tool has finished, so
4928
+ * each assistant segment lands in its own step.
4929
+ *
4930
+ * Called as new assistant content begins. A step holding a settled tool
4931
+ * result means the previous segment is complete, and the content about to be
4932
+ * applied belongs to the next one. Rotating here (rather than when a call is
4933
+ * announced) keeps calls that were announced before any result — one model
4934
+ * turn — in a single step.
5452
4935
  */
5453
- async resume(ownerCtx, options) {
5454
- const persistence = this.runtime.ctx.get("sessionPersistence");
5455
- if (persistence === void 0) {
5456
- throw new Error("cannot resume: session persistence is not configured (load a dsh-session-persistence backend)");
4936
+ beginSegment(phase) {
4937
+ if (this.stepSettledTools === 0) return;
4938
+ this.session.append("step/end", { turn: phase.turn, step: phase.step });
4939
+ phase.step += 1;
4940
+ this.session.append("step/start", { turn: phase.turn, step: phase.step });
4941
+ this.stepSettledTools = 0;
4942
+ }
4943
+ /** Apply one streamed update to the open step's blocks and stream. */
4944
+ applyUpdate(phase, update) {
4945
+ if (isThoughtChunk(update)) {
4946
+ const delta = chunkDelta(update);
4947
+ if (delta === "") return;
4948
+ this.beginSegment(phase);
4949
+ const block = this.ensureBlock("reasoning");
4950
+ if (block.text === "") this.currentStream(phase).push({ type: "block-start", index: block.index, blockType: "reasoning" });
4951
+ this.currentStream(phase).push({ type: "reasoning-delta", index: block.index, text: delta });
4952
+ block.text += delta;
4953
+ return;
5457
4954
  }
5458
- return this.resumeWith(ownerCtx, persistence, options);
5459
- }
5460
- /** Resume through an explicit persistence service. */
5461
- resumeWith(ownerCtx, persistence, options) {
5462
- const id = options.resumeSessionId;
5463
- const published = (async () => {
5464
- const ownerAbort = new AbortController();
5465
- const unfollowOwner = ownerCtx.effect(() => () => {
5466
- ownerAbort.abort(new Error(`agent "${id}" setup aborted: owner disposed during setup`));
5467
- }, `agentLoopKimi.resume-load(${id})`);
5468
- const fused = AbortSignal.any([
5469
- ...options.signal === void 0 ? [] : [options.signal],
5470
- ownerAbort.signal,
5471
- this.ownership.signal
5472
- ]);
5473
- let handle;
5474
- let stored;
5475
- let preparation;
5476
- try {
5477
- try {
5478
- handle = await raceAbortCall(
5479
- () => persistence.open(id, "write", { signal: fused }),
5480
- fused,
5481
- id,
5482
- (abandoned) => {
5483
- void abandoned.close();
5484
- }
5485
- );
5486
- const coldRead = await handle.read(0, void 0, { signal: fused });
5487
- fused.throwIfAborted();
5488
- const persisted = coldRead.events;
5489
- const closers = interruptedTurnClosers4(persisted);
5490
- if (closers.length > 0) await handle.append(closers);
5491
- preparation = SessionPreparation4.create(this.runtime.ctx.sessions.prepare(id, {
5492
- seed: [...persisted, ...closers],
5493
- meta: structuredClone(handle.header),
5494
- inheritedEventCount: handle.inheritedEventCount,
5495
- eventState: coldRead.eventState
5496
- }));
5497
- stored = { handle, storedCount: persisted.length + closers.length };
5498
- await this.appendUnstoredSuffix(stored, preparation.session);
5499
- } finally {
5500
- await unfollowOwner();
5501
- }
5502
- ownerCtx.fiber.assertActive();
5503
- if (!this.ownership.isActive()) throw new Error("agent loop is not active");
5504
- const owned = stored;
5505
- handle = void 0;
5506
- return await this.setupAndPublish(
5507
- ownerCtx,
5508
- id,
5509
- preparation,
5510
- options.agentOptions ?? {},
5511
- options.setup,
5512
- options.signal,
5513
- "resume",
5514
- owned,
5515
- options.parentAgent
5516
- );
5517
- } finally {
5518
- preparation?.[Symbol.dispose]();
5519
- await handle?.close().catch(() => {
5520
- });
4955
+ if (isTextChunk(update)) {
4956
+ const delta = chunkDelta(update);
4957
+ if (delta === "") return;
4958
+ this.beginSegment(phase);
4959
+ const block = this.ensureBlock("text");
4960
+ if (block.text === "") this.currentStream(phase).push({ type: "block-start", index: block.index, blockType: "text" });
4961
+ this.currentStream(phase).push({ type: "text-delta", index: block.index, text: delta });
4962
+ block.text += delta;
4963
+ return;
4964
+ }
4965
+ if (isToolCall(update)) {
4966
+ const callId = toolCallIdOf(update);
4967
+ if (callId === "" || this.pendingCalls.has(callId) || this.toolContent.has(callId)) return;
4968
+ this.beginSegment(phase);
4969
+ this.pendingCalls.set(callId, { name: toolCallName(update) });
4970
+ return;
4971
+ }
4972
+ if (isToolCallUpdate(update)) {
4973
+ const callId = toolCallIdOf(update);
4974
+ if (callId === "") return;
4975
+ const status = update.status;
4976
+ const settled = isToolSettledStatus(status);
4977
+ const snapshot = toolContentText(update);
4978
+ const pending = this.pendingCalls.get(callId);
4979
+ if (pending !== void 0) {
4980
+ if (snapshot !== void 0) pending.content = snapshot;
4981
+ const args = toolRawInput(update);
4982
+ if (args === void 0 && !settled) return;
4983
+ this.pendingCalls.delete(callId);
4984
+ this.beginSegment(phase);
4985
+ this.segmentCalls.push({ callId, name: pending.name, arguments: args ?? "{}" });
4986
+ this.toolContent.set(callId, pending.content ?? "");
4987
+ } else if (!this.toolContent.has(callId)) {
4988
+ return;
4989
+ } else if (snapshot !== void 0) {
4990
+ this.toolContent.set(callId, snapshot);
5521
4991
  }
5522
- })();
5523
- this.ownership.trackWrapper(published);
5524
- return published;
4992
+ if (settled) {
4993
+ this.flushSegment(phase);
4994
+ const message = toolResult(callId, this.toolContent.get(callId), isToolErrorStatus(status));
4995
+ this.session.append("tool/result", { turn: phase.turn, step: phase.step, message }, { surfaceOp: "append" });
4996
+ this.toolContent.delete(callId);
4997
+ this.stepSettledTools += 1;
4998
+ }
4999
+ return;
5000
+ }
5001
+ }
5002
+ /** The live attempt framing the current segment, opened on its first chunk. */
5003
+ currentStream(phase) {
5004
+ if (this.live === void 0) {
5005
+ this.live = new DriverAssistantStream(
5006
+ this.id,
5007
+ ++this.streamAttempts,
5008
+ phase.turn,
5009
+ phase.step,
5010
+ (frame2) => this.dispatch.emit("agent/assistant-stream", { frame: frame2 })
5011
+ );
5012
+ this.live.start();
5013
+ }
5014
+ return this.live;
5015
+ }
5016
+ /**
5017
+ * Flush the open segment's single assistant message and every `tool/call` it
5018
+ * accumulated, in the load-bearing order assistant/message → tool/call. One
5019
+ * segment produces exactly ONE message even when the model announced several
5020
+ * calls before any result, because the chat view keys an assistant node by
5021
+ * `${turn}:${step}` and replaces its blocks on every message — a second
5022
+ * message in the same step would overwrite the first one's reasoning/text and
5023
+ * leave only the last bare tool-call head visible.
5024
+ * @param phase - the open step the segment belongs to.
5025
+ */
5026
+ flushSegment(phase) {
5027
+ const calls = this.segmentCalls;
5028
+ this.segmentCalls = [];
5029
+ this.flushAssistant(phase, calls);
5030
+ for (const call of calls) {
5031
+ this.session.append("tool/call", { turn: phase.turn, step: phase.step, callId: ToolCallId7(call.callId), name: call.name, arguments: call.arguments });
5032
+ }
5033
+ }
5034
+ /**
5035
+ * Flush the accumulated assistant blocks into one durable assistant/message
5036
+ * carrying the exact stream the attempt published live, optionally closing it
5037
+ * with the tool-call block(s) that ended the segment.
5038
+ *
5039
+ * Every flush settles its own attempt (and so its own live `end` frame): a
5040
+ * step emits one message per assistant segment, and the client pairs a durable
5041
+ * message with the attempt whose `end` cites it, so two messages may not share
5042
+ * one attempt.
5043
+ * @param phase - the open step the message belongs to.
5044
+ * @param toolCalls - the calls that closed this segment, rendered as trailing
5045
+ * `tool-call` content blocks. A segment with no blocks of its own — a step
5046
+ * whose only activity was a tool call — still emits this message, so its
5047
+ * `tool/call` and `tool/result` events have a parent to pair with.
5048
+ */
5049
+ flushAssistant(phase, toolCalls = []) {
5050
+ if (this.blocks.length === 0 && toolCalls.length === 0) return;
5051
+ const content = [];
5052
+ this.producedOutput = true;
5053
+ const attempt = this.blocks.length === 0 ? void 0 : this.currentStream(phase);
5054
+ for (const block of this.blocks) {
5055
+ const delta = block.text;
5056
+ content.push(block.type === "text" ? { type: "text", text: delta } : { type: "reasoning", text: delta });
5057
+ attempt?.push({ type: "block-end", index: block.index, block: block.type === "text" ? { type: "text", text: delta } : { type: "reasoning", text: delta } });
5058
+ }
5059
+ for (const call of toolCalls) {
5060
+ content.push({ type: "tool-call", id: ToolCallId7(call.callId), name: call.name, arguments: call.arguments });
5061
+ }
5062
+ const data = {
5063
+ turn: phase.turn,
5064
+ step: phase.step,
5065
+ message: createAssistantMessage4({
5066
+ content,
5067
+ source: { provider: PROVIDER4, model: this.modelLabel() }
5068
+ }),
5069
+ // The attempt's exact timed stream travels with its message.
5070
+ stream: attempt?.stream ?? []
5071
+ };
5072
+ if (attempt === void 0) {
5073
+ this.session.append("assistant/message", data, { surfaceOp: "append" });
5074
+ } else {
5075
+ attempt.settle(() => this.session.append("assistant/message", data, { surfaceOp: "append" }).seq);
5076
+ this.live = void 0;
5077
+ }
5078
+ this.blocks = [];
5079
+ }
5080
+ };
5081
+
5082
+ // src/engine-kimi/loop.ts
5083
+ var KIMI_DISPOSE_GRACE_MS = 3e3;
5084
+ var Config4 = z4.object({
5085
+ model: z4.string(),
5086
+ env: z4.dict(z4.string()).default({}),
5087
+ bin: z4.string()
5088
+ });
5089
+ function resolveConfig4(config) {
5090
+ return {
5091
+ model: config.model,
5092
+ env: config.env ?? {},
5093
+ bin: kimiBinResolver(config.bin)
5094
+ };
5095
+ }
5096
+ var KimiLoop = class extends HostedLoopFactory {
5097
+ /** Services the loop resolves through its own fiber; blessed identically to the package-level entry inject. */
5098
+ static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
5099
+ /** One-shot spawn capability handed to every agent, sandboxed by the subprocess seam. */
5100
+ spawn;
5101
+ constructor(ctx, config) {
5102
+ super(ctx, "agentLoopKimi", resolveConfig4(config));
5103
+ this.spawn = (spec) => fromSubprocess2(this.runtime.ctx.subprocess.spawn(kimiSubprocessSpec(spec, KIMI_DISPOSE_GRACE_MS)));
5104
+ }
5105
+ /** Construct the Kimi ACP driver for one prepared session. */
5106
+ buildAgent(loopCtx, id, options, session) {
5107
+ return new KimiAgent(loopCtx, id, options, session, this.config, this.spawn, this.config.bin);
5525
5108
  }
5526
5109
  };
5527
5110
 
5528
5111
  // src/engine-kimi/skills.ts
5529
- import { readdir as readdir2, readFile as readFile3, stat as stat3 } from "node:fs/promises";
5530
5112
  import { homedir as homedir3 } from "node:os";
5531
- import { dirname as dirname3, join as join6, resolve as resolve3 } from "node:path";
5113
+ import { join as join7, resolve as resolve3 } from "node:path";
5114
+
5115
+ // src/driver-core/agents-md-skill-provider.ts
5116
+ import { readdir as readdir2, readFile as readFile3, stat as stat3 } from "node:fs/promises";
5117
+ import { dirname as dirname3, join as join6 } from "node:path";
5532
5118
 
5533
5119
  // src/driver-core/context-files.ts
5534
5120
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
@@ -5813,37 +5399,39 @@ async function readSources(paths) {
5813
5399
  return parts.length > 0 ? parts.join("\n\n") : void 0;
5814
5400
  }
5815
5401
 
5816
- // src/engine-kimi/skills.ts
5817
- var PROVIDER_NAME2 = "kimi";
5818
- var KIMI_AGENTS_PROJECT_RANK = 140;
5819
- var KIMI_SKILL_PROJECT_RANK = 150;
5820
- var KIMI_SKILL_USER_RANK = 160;
5821
- var KIMI_CONTEXT_POLICY = {
5822
- primary: ["AGENTS.md"]
5823
- };
5824
- function kimiAgentDir() {
5825
- const override = process.env.KIMI_CODE_HOME;
5826
- if (override !== void 0 && override.length > 0) return resolve3(override);
5827
- return join6(homedir3(), ".kimi-code");
5828
- }
5829
- var KimiSkillProvider = class {
5830
- constructor(control) {
5402
+ // src/driver-core/agents-md-skill-provider.ts
5403
+ var AGENTS_MD = "agents-md";
5404
+ var AgentsMdSkillProvider = class {
5405
+ constructor(spec, control) {
5406
+ this.spec = spec;
5831
5407
  this.control = control;
5408
+ this.name = spec.name;
5832
5409
  }
5410
+ spec;
5833
5411
  control;
5834
- name = PROVIDER_NAME2;
5412
+ name;
5835
5413
  async list(options) {
5836
5414
  const candidates = [];
5837
5415
  const cwd = options.cwd;
5838
5416
  if (cwd !== void 0) {
5839
- const projectDirs = await projectAncestors(cwd);
5840
- const contextPaths = await collectProjectContextFiles(cwd, KIMI_CONTEXT_POLICY);
5841
- if (await anySourceNonEmpty(contextPaths)) candidates.push(this.agentsCandidate(contextPaths, KIMI_AGENTS_PROJECT_RANK));
5842
- for (const dir of projectDirs) {
5843
- await this.collectSkillsDir(join6(dir, ".kimi-code", "skills"), KIMI_SKILL_PROJECT_RANK, candidates);
5417
+ const contextPaths = await collectProjectContextFiles(cwd, this.spec.contextPolicy);
5418
+ if (await anySourceNonEmpty(contextPaths)) candidates.push(this.agentsCandidate(contextPaths, this.spec.projectRank));
5419
+ if (this.spec.skills !== void 0) {
5420
+ const catalog = this.spec.skills;
5421
+ for (const dir of await projectAncestors(cwd)) {
5422
+ await this.collectSkillsDir(join6(dir, ...catalog.project), catalog.projectRank, candidates);
5423
+ }
5844
5424
  }
5845
5425
  }
5846
- await this.collectSkillsDir(join6(kimiAgentDir(), "skills"), KIMI_SKILL_USER_RANK, candidates);
5426
+ const userDir = this.spec.userDir();
5427
+ const userContext = this.spec.userContext;
5428
+ if (userContext !== void 0) {
5429
+ const userFile = join6(userDir, userContext.file);
5430
+ if (await fileNonEmpty(userFile)) candidates.push(this.agentsCandidate([userFile], userContext.rank));
5431
+ }
5432
+ if (this.spec.skills !== void 0) {
5433
+ await this.collectSkillsDir(join6(userDir, this.spec.skills.userDir), this.spec.skills.userRank, candidates);
5434
+ }
5847
5435
  if (this.control.signal.aborted) return [];
5848
5436
  return candidates;
5849
5437
  }
@@ -5882,8 +5470,8 @@ var KimiSkillProvider = class {
5882
5470
  agentsCandidate(paths, rank) {
5883
5471
  const first = paths[0];
5884
5472
  return {
5885
- name: "agents-md",
5886
- description: "Kimi project instructions (AGENTS.md)",
5473
+ name: AGENTS_MD,
5474
+ description: this.spec.agentsMdDescription,
5887
5475
  invocation: { modelInvocable: true, userInvocable: true },
5888
5476
  source: "custom",
5889
5477
  provider: this.name,
@@ -5893,7 +5481,7 @@ var KimiSkillProvider = class {
5893
5481
  resourceBase: { kind: "file", path: first }
5894
5482
  };
5895
5483
  }
5896
- /** Collect every skill in one skills directory, both kimi layouts. */
5484
+ /** Collect every skill in one skills directory, both nesting layouts. */
5897
5485
  async collectSkillsDir(skillsDir, rank, candidates) {
5898
5486
  let entries;
5899
5487
  try {
@@ -5944,6 +5532,37 @@ var KimiSkillProvider = class {
5944
5532
  }
5945
5533
  };
5946
5534
 
5535
+ // src/engine-kimi/skills.ts
5536
+ var KIMI_AGENTS_PROJECT_RANK = 140;
5537
+ var KIMI_SKILL_PROJECT_RANK = 150;
5538
+ var KIMI_SKILL_USER_RANK = 160;
5539
+ var KIMI_CONTEXT_POLICY = {
5540
+ primary: ["AGENTS.md"]
5541
+ };
5542
+ function kimiAgentDir() {
5543
+ const override = process.env.KIMI_CODE_HOME;
5544
+ if (override !== void 0 && override.length > 0) return resolve3(override);
5545
+ return join7(homedir3(), ".kimi-code");
5546
+ }
5547
+ var KIMI_SPEC = {
5548
+ name: "kimi",
5549
+ agentsMdDescription: "Kimi project instructions (AGENTS.md)",
5550
+ contextPolicy: KIMI_CONTEXT_POLICY,
5551
+ userDir: kimiAgentDir,
5552
+ projectRank: KIMI_AGENTS_PROJECT_RANK,
5553
+ skills: {
5554
+ project: [".kimi-code", "skills"],
5555
+ projectRank: KIMI_SKILL_PROJECT_RANK,
5556
+ userDir: "skills",
5557
+ userRank: KIMI_SKILL_USER_RANK
5558
+ }
5559
+ };
5560
+ var KimiSkillProvider = class extends AgentsMdSkillProvider {
5561
+ constructor(control) {
5562
+ super(KIMI_SPEC, control);
5563
+ }
5564
+ };
5565
+
5947
5566
  // src/engine-kimi/commands.ts
5948
5567
  import { createUserMessage as createUserMessage5 } from "@deepseek-ai/dsh-llm";
5949
5568
  function forwardKimiCommand(name2) {
@@ -5979,7 +5598,7 @@ var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
5979
5598
  // src/settings.ts
5980
5599
  var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi", "kimi"];
5981
5600
  var LOOP_ENGINE_SETTINGS_SCHEMA = z5.object({
5982
- engine: z5.union([z5.const("in-process"), z5.const("claude-code"), z5.const("codex"), z5.const("pi"), z5.const("kimi")]).default("in-process"),
5601
+ engine: z5.union(LOOP_ENGINE_IDS.map((id) => z5.const(id))).default("in-process"),
5983
5602
  showInComposer: z5.boolean().default(true)
5984
5603
  });
5985
5604
  function loopEngineSettingsNamespace() {
@@ -6002,10 +5621,16 @@ function renderManagedBlock(engine) {
6002
5621
  END_MARKER_LINE
6003
5622
  ].join("\n");
6004
5623
  }
5624
+ function hasManagedBlock(text) {
5625
+ return text.includes(MANAGED_BLOCK_BEGIN);
5626
+ }
6005
5627
  var BEGIN_MARKER_RE = /^# -- dsh-loop-engine managed block: (\S+) --$/m;
6006
- function currentEngineOf(text) {
5628
+ function managedBlockEngineOf(text) {
6007
5629
  const engine = BEGIN_MARKER_RE.exec(text)?.[1];
6008
- return LOOP_ENGINE_IDS.includes(engine ?? "") ? engine : "in-process";
5630
+ return LOOP_ENGINE_IDS.includes(engine ?? "") ? engine : void 0;
5631
+ }
5632
+ function currentEngineOf(text) {
5633
+ return managedBlockEngineOf(text) ?? "in-process";
6009
5634
  }
6010
5635
  function managedSpan(text) {
6011
5636
  const begin = text.indexOf(MANAGED_BLOCK_BEGIN);
@@ -6063,7 +5688,7 @@ ${block}`;
6063
5688
  // src/preset.ts
6064
5689
  import { mkdir, readFile as readFile4, rename, writeFile } from "node:fs/promises";
6065
5690
  import { randomUUID } from "node:crypto";
6066
- import { dirname as dirname4, join as join7 } from "node:path";
5691
+ import { dirname as dirname4, join as join8 } from "node:path";
6067
5692
  var HOSTED_PRESET_ID = "loop-engine";
6068
5693
  var USER_PRESET_DIR = ".agent-presets";
6069
5694
  var COMPOSITION_FILE = "agent.cordis.yml";
@@ -6131,9 +5756,9 @@ async function ensureHostedPreset(dshHome, source) {
6131
5756
  const composition = await source.read(SOURCE_PRESET_ID);
6132
5757
  const stripped = `${MANAGED_HEADER}
6133
5758
  ${stripPresetRows(composition)}`;
6134
- const dir = join7(dshHome, USER_PRESET_DIR, HOSTED_PRESET_ID);
6135
- const compositionChanged = await writeIfDifferent(join7(dir, COMPOSITION_FILE), stripped);
6136
- const metadataChanged = await writeIfDifferent(join7(dir, METADATA_FILE), MANAGED_METADATA);
5759
+ const dir = join8(dshHome, USER_PRESET_DIR, HOSTED_PRESET_ID);
5760
+ const compositionChanged = await writeIfDifferent(join8(dir, COMPOSITION_FILE), stripped);
5761
+ const metadataChanged = await writeIfDifferent(join8(dir, METADATA_FILE), MANAGED_METADATA);
6137
5762
  return compositionChanged || metadataChanged;
6138
5763
  }
6139
5764
 
@@ -6157,13 +5782,20 @@ var HostedEngineRouteAdapter = class extends LlmAdapter {
6157
5782
  }
6158
5783
  label;
6159
5784
  options;
6160
- /** Advertise the injected Pi models (if any) under this route's provider label. */
5785
+ /**
5786
+ * Advertise the injected Pi models (if any) under this route's provider label.
5787
+ *
5788
+ * The model is what the picker shows at the top level, so `name` is the bare
5789
+ * model; the `provider/model` composite stays the submitted `id`, which is
5790
+ * both what the engine receives as `--model` and how the driver validates a
5791
+ * session-selected model against its catalog.
5792
+ */
6161
5793
  async listModels(_provider) {
6162
5794
  const catalog = this.options.listModels?.() ?? [];
6163
5795
  return catalog.map((entry) => ({
6164
5796
  provider: this.label,
6165
5797
  id: `${entry.provider}/${entry.model}`,
6166
- name: `${entry.provider}/${entry.model}`
5798
+ name: entry.model
6167
5799
  }));
6168
5800
  }
6169
5801
  stream(_options) {
@@ -6177,7 +5809,7 @@ var HostedEngineRouteAdapter = class extends LlmAdapter {
6177
5809
  // src/commands.ts
6178
5810
  import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
6179
5811
  import { homedir as homedir4 } from "node:os";
6180
- import { join as join8 } from "node:path";
5812
+ import { join as join9 } from "node:path";
6181
5813
  import { createUserMessage as createUserMessage6 } from "@deepseek-ai/dsh-llm";
6182
5814
  var COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
6183
5815
  function forwardClaudeCodeCommand(name2) {
@@ -6214,7 +5846,7 @@ function discoverUserSlashCommands() {
6214
5846
  if (!entry.endsWith(".md")) continue;
6215
5847
  const name2 = entry.slice(0, -".md".length);
6216
5848
  if (!COMMAND_NAME.test(name2) || seen.has(name2)) continue;
6217
- const path = join8(userCommandsDir(), entry);
5849
+ const path = join9(userCommandsDir(), entry);
6218
5850
  let raw;
6219
5851
  try {
6220
5852
  raw = readFileSync2(path, "utf8");
@@ -6229,7 +5861,7 @@ function discoverUserSlashCommands() {
6229
5861
  return definitions;
6230
5862
  }
6231
5863
  function userCommandsDir() {
6232
- return join8(homedir4(), ".claude", "commands");
5864
+ return join9(homedir4(), ".claude", "commands");
6233
5865
  }
6234
5866
  function commandDescription(raw) {
6235
5867
  const trimmed = raw.trim();
@@ -6256,67 +5888,27 @@ function commandDescription(raw) {
6256
5888
 
6257
5889
  // src/engine-codex/skills.ts
6258
5890
  import { homedir as homedir5 } from "node:os";
6259
- import { join as join9 } from "node:path";
6260
- var PROVIDER_NAME3 = "codex";
5891
+ import { join as join10 } from "node:path";
6261
5892
  var CODEX_PROJECT_RANK = 140;
6262
5893
  var CODEX_USER_RANK = 160;
6263
5894
  var CODEX_CONTEXT_POLICY = { primary: ["AGENTS.md"] };
6264
- var CodexSkillProvider = class {
5895
+ var CODEX_SPEC = {
5896
+ name: "codex",
5897
+ agentsMdDescription: "Codex project/user instructions (AGENTS.md)",
5898
+ contextPolicy: CODEX_CONTEXT_POLICY,
5899
+ userDir: () => join10(homedir5(), ".codex"),
5900
+ projectRank: CODEX_PROJECT_RANK,
5901
+ userContext: { file: "AGENTS.md", rank: CODEX_USER_RANK }
5902
+ };
5903
+ var CodexSkillProvider = class extends AgentsMdSkillProvider {
6265
5904
  constructor(control) {
6266
- this.control = control;
6267
- }
6268
- control;
6269
- name = PROVIDER_NAME3;
6270
- async list(options) {
6271
- const candidates = [];
6272
- const cwd = options.cwd;
6273
- if (cwd !== void 0) {
6274
- const paths = await collectProjectContextFiles(cwd, CODEX_CONTEXT_POLICY);
6275
- if (await anySourceNonEmpty(paths)) candidates.push(this.agentsCandidate(paths, CODEX_PROJECT_RANK));
6276
- }
6277
- const userPath = join9(homedir5(), ".codex", "AGENTS.md");
6278
- if (await fileNonEmpty(userPath)) candidates.push(this.agentsCandidate([userPath], CODEX_USER_RANK));
6279
- if (this.control.signal.aborted) return [];
6280
- return candidates;
6281
- }
6282
- async get(candidate, _options) {
6283
- const locator = candidate.locator;
6284
- const content = await readSources(locator.paths);
6285
- if (content === void 0) return void 0;
6286
- const first = locator.paths[0];
6287
- return {
6288
- name: candidate.name,
6289
- description: candidate.description,
6290
- invocation: candidate.invocation,
6291
- source: candidate.source,
6292
- provider: this.name,
6293
- content,
6294
- path: first,
6295
- resourceBase: { kind: "file", path: first }
6296
- };
6297
- }
6298
- /** One merged `agents-md` candidate for a ranked file set. */
6299
- agentsCandidate(paths, rank) {
6300
- const first = paths[0];
6301
- return {
6302
- name: "agents-md",
6303
- description: "Codex project/user instructions (AGENTS.md)",
6304
- invocation: { modelInvocable: true, userInvocable: true },
6305
- source: "custom",
6306
- provider: this.name,
6307
- rank,
6308
- locator: { kind: "agents-md", paths },
6309
- path: first,
6310
- resourceBase: { kind: "file", path: first }
6311
- };
5905
+ super(CODEX_SPEC, control);
6312
5906
  }
6313
5907
  };
6314
5908
 
6315
5909
  // src/engine-pi/skills.ts
6316
- import { readdir as readdir3, readFile as readFile5, stat as stat4 } from "node:fs/promises";
6317
5910
  import { homedir as homedir6 } from "node:os";
6318
- import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
6319
- var PROVIDER_NAME4 = "pi";
5911
+ import { join as join11, resolve as resolve4 } from "node:path";
6320
5912
  var PI_AGENTS_PROJECT_RANK = 140;
6321
5913
  var PI_SKILL_PROJECT_RANK = 150;
6322
5914
  var PI_AGENTS_USER_RANK = 160;
@@ -6328,126 +5920,25 @@ var PI_CONTEXT_POLICY = {
6328
5920
  function piAgentDir() {
6329
5921
  const override = process.env.PI_CODING_AGENT_DIR;
6330
5922
  if (override !== void 0 && override.length > 0) return resolve4(override);
6331
- return join10(homedir6(), ".pi", "agent");
6332
- }
6333
- var PiSkillProvider = class {
6334
- constructor(control) {
6335
- this.control = control;
6336
- }
6337
- control;
6338
- name = PROVIDER_NAME4;
6339
- async list(options) {
6340
- const candidates = [];
6341
- const cwd = options.cwd;
6342
- if (cwd !== void 0) {
6343
- const projectDirs = await projectAncestors(cwd);
6344
- const contextPaths = await collectProjectContextFiles(cwd, PI_CONTEXT_POLICY);
6345
- if (await anySourceNonEmpty(contextPaths)) candidates.push(this.agentsCandidate(contextPaths, PI_AGENTS_PROJECT_RANK));
6346
- for (const dir of projectDirs) {
6347
- await this.collectSkillsDir(join10(dir, ".pi", "skills"), PI_SKILL_PROJECT_RANK, candidates);
6348
- }
6349
- }
6350
- const userAgentDir = piAgentDir();
6351
- const userContext = join10(userAgentDir, "AGENTS.md");
6352
- if (await fileNonEmpty(userContext)) candidates.push(this.agentsCandidate([userContext], PI_AGENTS_USER_RANK));
6353
- await this.collectSkillsDir(join10(userAgentDir, "skills"), PI_SKILL_USER_RANK, candidates);
6354
- if (this.control.signal.aborted) return [];
6355
- return candidates;
6356
- }
6357
- async get(candidate, _options) {
6358
- const locator = candidate.locator;
6359
- if (locator.kind === "skill-file") {
6360
- const parsed = await this.tryParse(locator.path);
6361
- if (parsed === void 0) return void 0;
6362
- return {
6363
- name: parsed.name,
6364
- description: parsed.description,
6365
- ...parsed.whenToUse === void 0 ? {} : { whenToUse: parsed.whenToUse },
6366
- invocation: parsed.invocation,
6367
- source: candidate.source,
6368
- provider: this.name,
6369
- content: parsed.content,
6370
- path: locator.path,
6371
- resourceBase: { kind: "directory", path: dirname5(locator.path) }
6372
- };
6373
- }
6374
- const content = await readSources(locator.paths);
6375
- if (content === void 0) return void 0;
6376
- const first = locator.paths[0];
6377
- return {
6378
- name: candidate.name,
6379
- description: candidate.description,
6380
- invocation: candidate.invocation,
6381
- source: candidate.source,
6382
- provider: this.name,
6383
- content,
6384
- path: first,
6385
- resourceBase: { kind: "file", path: first }
6386
- };
6387
- }
6388
- /** One merged `agents-md` candidate for a ranked file set. */
6389
- agentsCandidate(paths, rank) {
6390
- const first = paths[0];
6391
- return {
6392
- name: "agents-md",
6393
- description: "Pi project/user instructions (AGENTS.md / CLAUDE.md)",
6394
- invocation: { modelInvocable: true, userInvocable: true },
6395
- source: "custom",
6396
- provider: this.name,
6397
- rank,
6398
- locator: { kind: "agents-md", paths },
6399
- path: first,
6400
- resourceBase: { kind: "file", path: first }
6401
- };
6402
- }
6403
- /** Collect every skill in one skills directory, both pi layouts. */
6404
- async collectSkillsDir(skillsDir, rank, candidates) {
6405
- let entries;
6406
- try {
6407
- entries = await readdir3(skillsDir, { withFileTypes: true, encoding: "utf8" });
6408
- } catch {
6409
- return;
6410
- }
6411
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
6412
- const entryPath = join10(skillsDir, entry.name);
6413
- const info = await stat4(entryPath).catch(() => void 0);
6414
- if (info === void 0) continue;
6415
- if (info.isDirectory()) {
6416
- const path = join10(entryPath, "SKILL.md");
6417
- const parsed2 = await this.tryParse(path);
6418
- if (parsed2 === void 0) continue;
6419
- candidates.push(this.skillCandidate(parsed2, path, rank, entryPath));
6420
- continue;
6421
- }
6422
- if (!entry.name.endsWith(".md")) continue;
6423
- const parsed = await this.tryParse(entryPath);
6424
- if (parsed === void 0) continue;
6425
- candidates.push(this.skillCandidate(parsed, entryPath, rank, skillsDir));
6426
- }
6427
- }
6428
- /** One parsed skill as a ranked candidate. */
6429
- skillCandidate(skill, path, rank, resourceDir) {
6430
- return {
6431
- name: skill.name,
6432
- description: skill.description,
6433
- ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
6434
- invocation: skill.invocation,
6435
- source: "custom",
6436
- provider: this.name,
6437
- rank,
6438
- locator: { kind: "skill-file", path },
6439
- path,
6440
- resourceBase: { kind: "directory", path: resourceDir }
6441
- };
5923
+ return join11(homedir6(), ".pi", "agent");
5924
+ }
5925
+ var PI_SPEC = {
5926
+ name: "pi",
5927
+ agentsMdDescription: "Pi project/user instructions (AGENTS.md / CLAUDE.md)",
5928
+ contextPolicy: PI_CONTEXT_POLICY,
5929
+ userDir: piAgentDir,
5930
+ projectRank: PI_AGENTS_PROJECT_RANK,
5931
+ userContext: { file: "AGENTS.md", rank: PI_AGENTS_USER_RANK },
5932
+ skills: {
5933
+ project: [".pi", "skills"],
5934
+ projectRank: PI_SKILL_PROJECT_RANK,
5935
+ userDir: "skills",
5936
+ userRank: PI_SKILL_USER_RANK
6442
5937
  }
6443
- /** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
6444
- async tryParse(path) {
6445
- try {
6446
- const raw = await readFile5(path, { encoding: "utf8" });
6447
- return parseSkillFile(raw);
6448
- } catch {
6449
- return void 0;
6450
- }
5938
+ };
5939
+ var PiSkillProvider = class extends AgentsMdSkillProvider {
5940
+ constructor(control) {
5941
+ super(PI_SPEC, control);
6451
5942
  }
6452
5943
  };
6453
5944
 
@@ -6473,7 +5964,7 @@ var Config5 = z6.object({
6473
5964
  });
6474
5965
  function resolvePatchPath(config) {
6475
5966
  if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
6476
- return join11(
5967
+ return join12(
6477
5968
  resolveDshHome(),
6478
5969
  "profiles",
6479
5970
  config.profile ?? "web",
@@ -6485,20 +5976,20 @@ function isMissing(error) {
6485
5976
  }
6486
5977
  async function readPatchOrUndefined(path) {
6487
5978
  try {
6488
- return await readFile6(path, "utf8");
5979
+ return await readFile5(path, "utf8");
6489
5980
  } catch (error) {
6490
5981
  if (isMissing(error)) return void 0;
6491
5982
  throw error;
6492
5983
  }
6493
5984
  }
6494
5985
  async function writePatchFile(path, text) {
6495
- await mkdir2(dirname6(path), { recursive: true });
5986
+ await mkdir2(dirname5(path), { recursive: true });
6496
5987
  const tmp = `${path}.tmp-${randomUUID2()}`;
6497
5988
  await writeFile2(tmp, text, "utf8");
6498
5989
  await rename2(tmp, path);
6499
5990
  }
6500
5991
  function writePatchFileSync(path, text) {
6501
- mkdirSync(dirname6(path), { recursive: true });
5992
+ mkdirSync(dirname5(path), { recursive: true });
6502
5993
  const tmp = `${path}.tmp-${randomUUID2()}`;
6503
5994
  writeFileSync(tmp, text, "utf8");
6504
5995
  renameSync(tmp, path);
@@ -6553,12 +6044,29 @@ function kimiConfig(config) {
6553
6044
  }
6554
6045
  function apply(ctx, config) {
6555
6046
  const patchPath = resolvePatchPath(config);
6556
- let fileEngine = currentEngineOf(readPatchFileSync(patchPath));
6047
+ const patchText = readPatchFileSync(patchPath);
6048
+ let fileEngine = currentEngineOf(patchText);
6049
+ if (hasManagedBlock(patchText) && managedBlockEngineOf(patchText) === void 0) {
6050
+ try {
6051
+ writePatchFileSync(patchPath, applyManagedBlock(patchText, "in-process"));
6052
+ fileEngine = "in-process";
6053
+ ctx.logger.error(
6054
+ "loop-engine: the managed block names an engine this build does not recognize; removed it so the base agent loop can own the factory slot. Restart `dsh web` to bring the base loop back."
6055
+ );
6056
+ } catch (error) {
6057
+ ctx.logger.error(`loop-engine: could not repair the unrecognized managed block: ${String(error)}`);
6058
+ }
6059
+ }
6557
6060
  const AGENT_PRESETS_NS = "agent-presets";
6558
6061
  const PRESET_DEFAULT_ATTEMPTS = 30;
6559
6062
  const PRESET_DEFAULT_RETRY_MS = 100;
6560
6063
  let engineFiber;
6561
6064
  let mountedEngine;
6065
+ let mountGeneration = 0;
6066
+ let disposed = false;
6067
+ const retryLater = (run, ms) => setTimeout(() => {
6068
+ if (!disposed) run();
6069
+ }, ms);
6562
6070
  let commandDisposers;
6563
6071
  let skillDisposer;
6564
6072
  const piCatalogHolder = { entries: [] };
@@ -6604,9 +6112,7 @@ function apply(ctx, config) {
6604
6112
  const llm = ctx.get("llm");
6605
6113
  if (llm === void 0) {
6606
6114
  if (attempt < ROUTE_ATTEMPTS) {
6607
- routeRetry = setTimeout(() => {
6608
- mountProviderRoute(engine, attempt + 1);
6609
- }, ROUTE_RETRY_MS);
6115
+ routeRetry = retryLater(() => mountProviderRoute(engine, attempt + 1), ROUTE_RETRY_MS);
6610
6116
  }
6611
6117
  return;
6612
6118
  }
@@ -6615,7 +6121,8 @@ function apply(ctx, config) {
6615
6121
  routeHandle = llm.registerAdapter([label], new HostedEngineRouteAdapter(label, options));
6616
6122
  routeEngine = engine;
6617
6123
  } catch (error) {
6618
- if (error instanceof Error && error.message.includes("already registered")) {
6124
+ const duplicateAdapter = error?.code === "DUPLICATE_ADAPTER";
6125
+ if (error instanceof Error && (duplicateAdapter || error.message.includes("already registered"))) {
6619
6126
  ctx.logger.warn(`loop-engine: provider route "${label}" is already served by another adapter`);
6620
6127
  return;
6621
6128
  }
@@ -6625,11 +6132,17 @@ function apply(ctx, config) {
6625
6132
  const mutatePresetDefault = (op, attempt = 0) => {
6626
6133
  const settings = ctx.get("settings");
6627
6134
  if (settings === void 0) return;
6135
+ if (settings.describe !== void 0 && !settings.describe().some((entry) => entry.ns === AGENT_PRESETS_NS)) {
6136
+ if (attempt < PRESET_DEFAULT_ATTEMPTS) {
6137
+ presetRetry = retryLater(() => mutatePresetDefault(op, attempt + 1), PRESET_DEFAULT_RETRY_MS);
6138
+ } else {
6139
+ ctx.logger.error(`loop-engine: preset default switch failed: the "${AGENT_PRESETS_NS}" settings namespace never registered`);
6140
+ }
6141
+ return;
6142
+ }
6628
6143
  settings.mutate(AGENT_PRESETS_NS, [op]).then(() => void 0, (error) => {
6629
6144
  if (error instanceof Error && error.message.includes("not registered") && attempt < PRESET_DEFAULT_ATTEMPTS) {
6630
- presetRetry = setTimeout(() => {
6631
- mutatePresetDefault(op, attempt + 1);
6632
- }, PRESET_DEFAULT_RETRY_MS);
6145
+ presetRetry = retryLater(() => mutatePresetDefault(op, attempt + 1), PRESET_DEFAULT_RETRY_MS);
6633
6146
  return;
6634
6147
  }
6635
6148
  ctx.logger.error(`loop-engine: preset default switch failed: ${String(error)}`);
@@ -6645,9 +6158,7 @@ function apply(ctx, config) {
6645
6158
  return;
6646
6159
  }
6647
6160
  if (attempt < PRESET_DEFAULT_ATTEMPTS) {
6648
- presetRetry = setTimeout(() => {
6649
- restorePresetDefault(attempt + 1);
6650
- }, PRESET_DEFAULT_RETRY_MS);
6161
+ presetRetry = retryLater(() => restorePresetDefault(attempt + 1), PRESET_DEFAULT_RETRY_MS);
6651
6162
  }
6652
6163
  };
6653
6164
  const steerPresetDefault = (engine) => {
@@ -6682,21 +6193,30 @@ function apply(ctx, config) {
6682
6193
  };
6683
6194
  const hostFactory = (engine, mount) => {
6684
6195
  if (engineFiber !== void 0) return;
6196
+ const generation = ++mountGeneration;
6685
6197
  const fiber = mount();
6686
6198
  engineFiber = fiber;
6687
6199
  mountedEngine = engine;
6688
6200
  void fiber.then(() => void 0, (error) => {
6201
+ if (generation !== mountGeneration) return;
6689
6202
  cleanupEngineRegistrations();
6690
6203
  engineFiber = void 0;
6691
6204
  mountedEngine = void 0;
6692
- if (error instanceof Error && error.message.includes("an agent factory is already registered") && mountAttempts < MAX_MOUNT_ATTEMPTS) {
6205
+ const baseLoopHoldsSlot = ctx.get("agentLoop") !== void 0;
6206
+ if (error instanceof Error && (baseLoopHoldsSlot || error.message.includes("an agent factory is already registered")) && mountAttempts < MAX_MOUNT_ATTEMPTS) {
6693
6207
  mountAttempts += 1;
6694
- mountRetry = setTimeout(() => {
6695
- mountEngine(engine);
6208
+ mountRetry = retryLater(() => {
6209
+ try {
6210
+ mountEngine(engine);
6211
+ } catch (retryError) {
6212
+ ctx.logger.error(`loop-engine: ${engine} mount retry failed: ${String(retryError)}`);
6213
+ }
6696
6214
  }, MOUNT_RETRY_MS);
6697
6215
  return;
6698
6216
  }
6699
- ctx.logger.error(`loop-engine: ${engine} factory failed to start: ${String(error)}`);
6217
+ ctx.logger.error(
6218
+ `loop-engine: ${engine} factory failed to start: ${String(error)} \u2014 restart \`dsh web\` to release the factory slot`
6219
+ );
6700
6220
  });
6701
6221
  };
6702
6222
  const mountClaude = () => {
@@ -6764,20 +6284,29 @@ function apply(ctx, config) {
6764
6284
  };
6765
6285
  const unmountEngine = () => {
6766
6286
  const fiber = engineFiber;
6287
+ const engine = mountedEngine;
6767
6288
  mountAttempts = 0;
6289
+ mountGeneration += 1;
6768
6290
  CLEAR_RETRY();
6769
6291
  releaseRoute();
6770
6292
  cleanupEngineRegistrations();
6771
6293
  mountedEngine = void 0;
6772
6294
  if (fiber === void 0) return;
6773
6295
  engineFiber = void 0;
6774
- void fiber.then((resolved) => {
6775
- void resolved.dispose();
6776
- }, () => void 0);
6296
+ void fiber.then(
6297
+ (resolved) => {
6298
+ resolved.dispose().catch((error) => {
6299
+ ctx.logger.error(`loop-engine: ${String(engine)} factory dispose failed: ${String(error)}`);
6300
+ });
6301
+ },
6302
+ /* v8 ignore next -- a fiber that failed already cleared engineFiber in hostFactory's rejection handler, so this rejection arm is unreachable */
6303
+ () => void 0
6304
+ );
6777
6305
  };
6778
6306
  mountEngine(fileEngine);
6779
6307
  steerPresetDefault(fileEngine);
6780
6308
  ctx.effect(() => () => {
6309
+ disposed = true;
6781
6310
  CLEAR_RETRY();
6782
6311
  CLEAR_PRESET_RETRY();
6783
6312
  releaseRoute();
@@ -6791,18 +6320,23 @@ function apply(ctx, config) {
6791
6320
  onChange: () => {
6792
6321
  const next = source().engine;
6793
6322
  if (next === fileEngine) return;
6794
- if (mountedEngine !== next) {
6795
- unmountEngine();
6796
- mountEngine(next);
6797
- }
6798
6323
  try {
6799
6324
  const updated = applyManagedBlock(readPatchFileSync(patchPath), next);
6800
6325
  writePatchFileSync(patchPath, updated);
6801
- fileEngine = next;
6802
6326
  } catch (error) {
6803
6327
  ctx.logger.error(`loop-engine: managed block write failed: ${String(error)}`);
6328
+ setTimeout(() => {
6329
+ settingsCtx.settings.mutate(loopEngineSettingsNamespace(), [{ op: "set", path: ["engine"], value: fileEngine }]).catch((revertError) => {
6330
+ ctx.logger.error(`loop-engine: engine selection revert failed: ${String(revertError)}`);
6331
+ });
6332
+ }, 0);
6804
6333
  return;
6805
6334
  }
6335
+ fileEngine = next;
6336
+ if (mountedEngine !== next) {
6337
+ unmountEngine();
6338
+ mountEngine(next);
6339
+ }
6806
6340
  steerPresetDefault(next);
6807
6341
  }
6808
6342
  });