@astrosheep/keiyaku 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +73 -56
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +42 -26
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -3,6 +3,8 @@ import { FlowError } from "../../flow-error.js";
3
3
  import { appendDebugLog } from "../../telemetry/debug-log.js";
4
4
  import { SUBAGENT_SESSION_VERSION } from "../session.js";
5
5
  import { createProjectionDriver } from "../harness/execution-handle.js";
6
+ import { activityCallFromNamedTool, boundActivityExcerpt, diffstatFromUnifiedPatch } from "../harness/activity-values.js";
7
+ import { AGENT_THOUGHT_TEXT_MAX_CHARS } from "../harness/events.js";
6
8
  import { createAbortError, emitProviderEvidence, sha256Utf8 } from "../harness/runtime.js";
7
9
  import { SubagentFailure, } from "../types.js";
8
10
  /** Opaque Pi resume handle: exact session file path (UUID alone is ambiguous). */
@@ -13,6 +15,34 @@ export const piSessionSchema = z.object({
13
15
  const PI_PROVIDER = "pi";
14
16
  /** Exact model shape: `<provider>/<id>` (single slash separating non-empty sides). */
15
17
  const PI_MODEL_PATTERN = /^[^/]+\/[^/]+$/;
18
+ const PI_ACKNOWLEDGED_UNPERSISTED = new Set(["message_update", "queue_update"]);
19
+ const PI_MAPPED = new Set([
20
+ "agent_start",
21
+ "agent_end",
22
+ "turn_start",
23
+ "turn_end",
24
+ "message_start",
25
+ "message_end",
26
+ "tool_execution_start",
27
+ "tool_execution_update",
28
+ "tool_execution_end",
29
+ "compaction_start",
30
+ "compaction_end",
31
+ "session_info_changed",
32
+ "thinking_level_changed",
33
+ "auto_retry_start",
34
+ "auto_retry_end",
35
+ ]);
36
+ function piIngressDisposition(type) {
37
+ if (PI_ACKNOWLEDGED_UNPERSISTED.has(type))
38
+ return "acknowledged-unpersisted";
39
+ if (PI_MAPPED.has(type))
40
+ return "mapped";
41
+ return "unknown";
42
+ }
43
+ function toPiSdkThinkingLevel(effort) {
44
+ return effort; // opacity boundary
45
+ }
16
46
  let piSdkLoader;
17
47
  const importPiSdk = new Function("specifier", "return import(specifier)");
18
48
  async function loadPiSdk() {
@@ -39,7 +69,7 @@ export function parsePiModelRef(model) {
39
69
  };
40
70
  }
41
71
  function extractAssistantText(event) {
42
- if (event.type === "message_update" || event.type === "message_end") {
72
+ if (event.type === "message_end") {
43
73
  const message = event.message;
44
74
  if (message?.role !== "assistant")
45
75
  return undefined;
@@ -58,7 +88,9 @@ function extractContentText(content) {
58
88
  .map((block) => {
59
89
  if (typeof block === "string")
60
90
  return block;
61
- if (block && typeof block === "object" && "text" in block) {
91
+ if (block &&
92
+ typeof block === "object" &&
93
+ block.type === "text") {
62
94
  const candidate = block.text;
63
95
  return typeof candidate === "string" ? candidate : "";
64
96
  }
@@ -68,15 +100,123 @@ function extractContentText(content) {
68
100
  .trim();
69
101
  return text || undefined;
70
102
  }
103
+ function emitPiThoughts(builder, event, turnOrdinal, messageOrdinal) {
104
+ if (event.type !== "message_end")
105
+ return;
106
+ const message = event.message;
107
+ if (message?.role !== "assistant" || !Array.isArray(message.content))
108
+ return;
109
+ message.content.forEach((block, blockIndex) => {
110
+ if (!block || typeof block !== "object")
111
+ return;
112
+ const record = block;
113
+ if (record.type !== "thinking" || record.redacted === true)
114
+ return;
115
+ if (typeof record.thinking !== "string" || record.thinking.trim().length === 0)
116
+ return;
117
+ const truncated = record.thinking.length > AGENT_THOUGHT_TEXT_MAX_CHARS;
118
+ builder.emit({
119
+ type: "thought",
120
+ id: `t${turnOrdinal}:m${messageOrdinal}:b${blockIndex}`,
121
+ text: record.thinking.slice(0, AGENT_THOUGHT_TEXT_MAX_CHARS),
122
+ final: true,
123
+ ...(truncated ? { truncated: true } : {}),
124
+ });
125
+ });
126
+ }
127
+ function piRecord(value) {
128
+ return value !== null && typeof value === "object" && !Array.isArray(value)
129
+ ? value
130
+ : undefined;
131
+ }
132
+ function piCallFacts(args) {
133
+ const value = piRecord(args);
134
+ if (!value)
135
+ return {};
136
+ return {
137
+ ...(typeof value.command === "string" ? { command: value.command } : {}),
138
+ ...(typeof value.description === "string" ? { description: value.description } : {}),
139
+ ...(typeof value.path === "string" ? { path: value.path } : {}),
140
+ ...(typeof value.offset === "number" && Number.isFinite(value.offset) ? { offset: value.offset } : {}),
141
+ ...(typeof value.limit === "number" && Number.isFinite(value.limit) ? { limit: value.limit } : {}),
142
+ ...(typeof value.query === "string" ? { query: value.query } : {}),
143
+ };
144
+ }
145
+ function piResult(value, isError) {
146
+ const payload = piRecord(value);
147
+ const content = Array.isArray(payload?.content)
148
+ ? payload.content.flatMap((entry) => {
149
+ const block = piRecord(entry);
150
+ return block?.type === "text" && typeof block.text === "string" ? [block.text] : [];
151
+ }).join("\n")
152
+ : undefined;
153
+ const details = piRecord(payload?.details);
154
+ const diffstat = typeof details?.patch === "string" ? diffstatFromUnifiedPatch(details.patch) : undefined;
155
+ return {
156
+ status: isError ? "error" : "ok",
157
+ ...(content ? { message: boundActivityExcerpt(content) } : {}),
158
+ ...(diffstat ? { diffstat } : {}),
159
+ };
160
+ }
161
+ function piActivityBody(event, calls) {
162
+ if (event.type !== "tool_execution_start" &&
163
+ event.type !== "tool_execution_update" &&
164
+ event.type !== "tool_execution_end") {
165
+ return undefined;
166
+ }
167
+ if (typeof event.toolCallId !== "string" || typeof event.toolName !== "string")
168
+ return undefined;
169
+ const phase = event.type === "tool_execution_start"
170
+ ? "started"
171
+ : event.type === "tool_execution_update"
172
+ ? "updated"
173
+ : "completed";
174
+ if (event.type === "tool_execution_start") {
175
+ calls.set(event.toolCallId, activityCallFromNamedTool(event.toolName, piCallFacts(event.args)));
176
+ }
177
+ const call = calls.get(event.toolCallId) ?? (event.type === "tool_execution_end"
178
+ ? { kind: "other", display: event.toolName }
179
+ : activityCallFromNamedTool(event.toolName, piCallFacts(event.args)));
180
+ if (event.type === "tool_execution_end")
181
+ calls.delete(event.toolCallId);
182
+ return {
183
+ type: "activity",
184
+ phase,
185
+ id: event.toolCallId,
186
+ tool: event.toolName,
187
+ call,
188
+ ...(event.type === "tool_execution_start"
189
+ ? {}
190
+ : { result: piResult(event.type === "tool_execution_update" ? event.partialResult : event.result, event.type === "tool_execution_end" && event.isError) }),
191
+ };
192
+ }
193
+ function piUsageBody(event) {
194
+ if (event.type !== "message_end")
195
+ return undefined;
196
+ const message = event.message;
197
+ if (message?.role !== "assistant")
198
+ return undefined;
199
+ const usage = message.usage;
200
+ if (!usage || typeof usage.input !== "number" || typeof usage.output !== "number")
201
+ return undefined;
202
+ return {
203
+ type: "usage",
204
+ inputTokens: usage.input,
205
+ outputTokens: usage.output,
206
+ ...(typeof usage.cacheRead === "number" ? { cachedInputTokens: usage.cacheRead } : {}),
207
+ ...(typeof usage.cost?.total === "number" ? { costUsd: usage.cost.total } : {}),
208
+ ...(typeof message.responseModel === "string"
209
+ ? { model: message.responseModel }
210
+ : typeof message.model === "string"
211
+ ? { model: message.model }
212
+ : {}),
213
+ };
214
+ }
71
215
  /** True when native messages prove the run stopped via abort (stopReason aborted). */
72
216
  function agentEndLooksAborted(event) {
73
- if (event.aborted === true)
74
- return true;
75
- if (event.stopReason === "aborted")
76
- return true;
77
- const messages = event.messages;
78
- if (!Array.isArray(messages))
217
+ if (event.type !== "agent_end")
79
218
  return false;
219
+ const messages = event.messages;
80
220
  for (let i = messages.length - 1; i >= 0; i -= 1) {
81
221
  const message = messages[i];
82
222
  if (!message || message.role !== "assistant")
@@ -88,6 +228,24 @@ function agentEndLooksAborted(event) {
88
228
  }
89
229
  return false;
90
230
  }
231
+ /** Returns the terminal Pi provider error, if the final assistant message ended in error. */
232
+ function agentEndProviderError(event) {
233
+ if (event.type !== "agent_end")
234
+ return undefined;
235
+ const messages = event.messages;
236
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
237
+ const message = messages[i];
238
+ if (!message || message.role !== "assistant")
239
+ continue;
240
+ if (message.stopReason !== "error")
241
+ return undefined;
242
+ if (typeof message.errorMessage === "string" && message.errorMessage.trim()) {
243
+ return message.errorMessage;
244
+ }
245
+ return `${PI_PROVIDER} provider error (stopReason error)`;
246
+ }
247
+ return undefined;
248
+ }
91
249
  function normalizeClearQueue(result) {
92
250
  if (Array.isArray(result))
93
251
  return result;
@@ -112,39 +270,83 @@ function buildPiSessionOutcome(session, finalMessage) {
112
270
  }
113
271
  function bindPiSessionEvents(session, builder, options, steerControl, state, setTurnOrdinal, settle) {
114
272
  return session.subscribe((event) => {
115
- builder.emit({ type: "output", value: event });
116
- const assistantText = extractAssistantText(event);
117
- if (assistantText) {
118
- state.lastAssistantMessage = assistantText;
119
- builder.emit({ type: "message", text: assistantText });
120
- }
121
- if (event.type === "turn_end" && !state.abortIntent) {
122
- state.turnOrdinal += 1;
123
- setTurnOrdinal(state.turnOrdinal);
124
- const checkpoint = { fence: { kind: "ordinal", ordinal: state.turnOrdinal } };
125
- // Evidence identifies the native turn_end before the synchronous pump can
126
- // consume tells at this checkpoint.
127
- if (options.onEvidence) {
128
- emitProviderEvidence(options.onEvidence, { type: "checkpoint", provider: PI_PROVIDER, ...checkpoint, source: "notification" }, (text) => builder.emit({ type: "diagnostic", text }));
273
+ builder.ingress(PI_PROVIDER, event.type, event, piIngressDisposition(event.type), () => {
274
+ if (event.type === "agent_start") {
275
+ builder.emit({ type: "turn", phase: "started" });
129
276
  }
130
- // Sync checkpoint window: claim/steer must run before returning from turn_end.
131
- options.onSyncCheckpoint?.(steerControl, checkpoint);
132
- }
133
- if (event.type !== "agent_end" || state.settled) {
134
- return;
135
- }
136
- if (state.abortIntent || agentEndLooksAborted(event)) {
137
- settle.reject(createAbortError(`${PI_PROVIDER} exec cancelled by client`));
138
- return;
139
- }
140
- const outcome = buildPiSessionOutcome(session, state.lastAssistantMessage);
141
- if (outcome instanceof SubagentFailure) {
142
- builder.emit({ type: "fail", message: outcome.message });
143
- settle.reject(outcome);
144
- return;
145
- }
146
- builder.emit({ type: "done", finalMessage: outcome.finalMessage, session: outcome.session });
147
- settle.resolve(outcome);
277
+ if (event.type === "turn_start") {
278
+ state.thoughtTurnOrdinal += 1;
279
+ state.assistantMessageOrdinal = -1;
280
+ state.activeAssistantMessageOrdinal = undefined;
281
+ }
282
+ if (event.type === "message_start") {
283
+ const message = event.message;
284
+ if (message?.role === "assistant") {
285
+ state.assistantMessageOrdinal += 1;
286
+ state.activeAssistantMessageOrdinal = state.assistantMessageOrdinal;
287
+ }
288
+ }
289
+ const activity = piActivityBody(event, state.activityCalls);
290
+ if (activity)
291
+ builder.emit(activity);
292
+ if (event.type === "message_end") {
293
+ const message = event.message;
294
+ if (message?.role === "assistant") {
295
+ if (state.activeAssistantMessageOrdinal === undefined) {
296
+ state.assistantMessageOrdinal += 1;
297
+ state.activeAssistantMessageOrdinal = state.assistantMessageOrdinal;
298
+ }
299
+ emitPiThoughts(builder, event, state.thoughtTurnOrdinal, state.activeAssistantMessageOrdinal);
300
+ state.activeAssistantMessageOrdinal = undefined;
301
+ }
302
+ }
303
+ const assistantText = extractAssistantText(event);
304
+ if (assistantText) {
305
+ state.lastAssistantMessage = assistantText;
306
+ builder.emit({ type: "message", text: assistantText });
307
+ }
308
+ const usage = piUsageBody(event);
309
+ if (usage)
310
+ builder.emit(usage);
311
+ if (event.type === "turn_end" && !state.abortIntent) {
312
+ state.turnOrdinal += 1;
313
+ setTurnOrdinal(state.turnOrdinal);
314
+ const checkpoint = { fence: { kind: "ordinal", ordinal: state.turnOrdinal } };
315
+ // Evidence identifies the native turn_end before the synchronous pump can
316
+ // consume tells at this checkpoint.
317
+ if (options.onEvidence) {
318
+ emitProviderEvidence(options.onEvidence, { type: "checkpoint", provider: PI_PROVIDER, ...checkpoint, source: "notification" }, (text) => builder.emit({ type: "diagnostic", text }));
319
+ }
320
+ // Sync checkpoint window: claim/steer must run before returning from turn_end.
321
+ options.onSyncCheckpoint?.(steerControl, checkpoint);
322
+ }
323
+ if (event.type !== "agent_end" || state.settled) {
324
+ return;
325
+ }
326
+ if (state.abortIntent || agentEndLooksAborted(event)) {
327
+ settle.reject(createAbortError(`${PI_PROVIDER} exec cancelled by client`));
328
+ return;
329
+ }
330
+ if (event.willRetry) {
331
+ return;
332
+ }
333
+ const providerError = agentEndProviderError(event);
334
+ if (providerError) {
335
+ const failure = new SubagentFailure("SUBAGENT_EXEC_ERROR", providerError);
336
+ builder.emit({ type: "fail", message: failure.message });
337
+ settle.reject(failure);
338
+ return;
339
+ }
340
+ const outcome = buildPiSessionOutcome(session, state.lastAssistantMessage);
341
+ if (outcome instanceof SubagentFailure) {
342
+ builder.emit({ type: "fail", message: outcome.message });
343
+ settle.reject(outcome);
344
+ return;
345
+ }
346
+ builder.emit({ type: "turn", phase: "completed" });
347
+ builder.emit({ type: "done", finalMessage: outcome.finalMessage, session: outcome.session });
348
+ settle.resolve(outcome);
349
+ });
148
350
  });
149
351
  }
150
352
  export async function startPi(prompt, cwd, options = {}) {
@@ -201,7 +403,7 @@ export async function startPi(prompt, cwd, options = {}) {
201
403
  const created = await sdk.createAgentSession({
202
404
  cwd,
203
405
  sessionManager,
204
- ...(options.thinkingLevel ? { thinkingLevel: options.thinkingLevel } : {}),
406
+ ...(options.thinkingLevel ? { thinkingLevel: toPiSdkThinkingLevel(options.thinkingLevel) } : {}),
205
407
  ...(model ? { model, authStorage, modelRegistry } : {}),
206
408
  ...(resourceLoader ? { resourceLoader } : {}),
207
409
  });
@@ -210,6 +412,7 @@ export async function startPi(prompt, cwd, options = {}) {
210
412
  emitProviderEvidence(options.onEvidence, { type: "session", provider: PI_PROVIDER, sessionId: session.sessionId, ...(session.sessionFile ? { sessionFile: session.sessionFile } : {}) }, reportEvidenceDiagnostic);
211
413
  session.setSteeringMode("one-at-a-time");
212
414
  let submittedAtTurn = 0;
415
+ let emitTurnStarted;
213
416
  const enqueue = async (tell) => {
214
417
  // Acceptance into volatile queue only. text is core-rendered; send byte-for-byte.
215
418
  if (options.onEvidence)
@@ -217,9 +420,10 @@ export async function startPi(prompt, cwd, options = {}) {
217
420
  if (tell.effort) {
218
421
  if (!session.setThinkingLevel)
219
422
  throw new Error(`${PI_PROVIDER} session does not support thinking level control`);
220
- session.setThinkingLevel(tell.effort === "max" ? "xhigh" : tell.effort);
423
+ session.setThinkingLevel(toPiSdkThinkingLevel(tell.effort));
221
424
  }
222
425
  await session.steer(tell.text);
426
+ emitTurnStarted?.();
223
427
  if (options.onEvidence)
224
428
  emitProviderEvidence(options.onEvidence, { type: "ack", provider: PI_PROVIDER, fence: { kind: "ordinal", ordinal: submittedAtTurn }, outbound: "steer" }, reportEvidenceDiagnostic);
225
429
  return { fence: { kind: "ordinal", ordinal: submittedAtTurn } };
@@ -247,6 +451,8 @@ export async function startPi(prompt, cwd, options = {}) {
247
451
  return;
248
452
  }
249
453
  abortRequested = (async () => {
454
+ // Publish abortRequested before clearQueue can synchronously emit queue_update.
455
+ await Promise.resolve();
250
456
  try {
251
457
  session.clearQueue();
252
458
  await session.abort();
@@ -261,12 +467,15 @@ export async function startPi(prompt, cwd, options = {}) {
261
467
  };
262
468
  return createProjectionDriver({
263
469
  steerMode: "mid-execution",
470
+ initialTurn: options.initialTurn,
264
471
  enqueue,
472
+ onIngress: options.onIngress,
265
473
  async abort() {
266
474
  await requestAbort({ abortIntent: true, settled: false });
267
475
  },
268
476
  async run(builder) {
269
477
  reportEvidenceDiagnostic = () => builder.emit({ type: "diagnostic", text: "provider evidence callback failed" });
478
+ emitTurnStarted = () => builder.emit({ type: "turn", phase: "started" });
270
479
  while (pendingEvidenceDiagnostics > 0) {
271
480
  pendingEvidenceDiagnostics -= 1;
272
481
  reportEvidenceDiagnostic();
@@ -276,6 +485,9 @@ export async function startPi(prompt, cwd, options = {}) {
276
485
  lastAssistantMessage: "",
277
486
  settled: false,
278
487
  turnOrdinal: 0,
488
+ thoughtTurnOrdinal: 0,
489
+ assistantMessageOrdinal: -1,
490
+ activityCalls: new Map(),
279
491
  };
280
492
  let settleOutcome;
281
493
  const outcome = new Promise((resolve, reject) => {
@@ -0,0 +1,122 @@
1
+ import { getConfig } from "../config/env.js";
2
+ import { loadAkumaCatalogV2, } from "../config/akuma-loader_v2.js";
3
+ import { assertSettingsKnobUsable, loadKeiyakuSettings, resolveDefaultAgentName, } from "../config/settings.js";
4
+ import { FlowError } from "../flow-error.js";
5
+ const ACCESS_VALUES = ["read", "write", "auto"];
6
+ const NETWORK_VALUES = ["disabled", "enabled"];
7
+ const SUPPORTED_EXPLICIT = {
8
+ "codex-sdk": {
9
+ access: new Set(ACCESS_VALUES),
10
+ network: new Set(NETWORK_VALUES),
11
+ webSearch: new Set(["disabled"]),
12
+ },
13
+ "codex-app-server": {
14
+ access: new Set(["write"]),
15
+ network: new Set(NETWORK_VALUES),
16
+ webSearch: new Set(),
17
+ },
18
+ "claude-agent-sdk": {
19
+ access: new Set(ACCESS_VALUES),
20
+ network: new Set(),
21
+ webSearch: new Set(["disabled"]),
22
+ },
23
+ "opencode-sdk": {
24
+ access: new Set(),
25
+ network: new Set(),
26
+ webSearch: new Set(),
27
+ },
28
+ pi: {
29
+ access: new Set(),
30
+ network: new Set(),
31
+ webSearch: new Set(),
32
+ },
33
+ };
34
+ export function formatAvailableAgentNamesV2(names) {
35
+ if (names.length === 0)
36
+ return "(none configured)";
37
+ return [...names].sort().join(", ");
38
+ }
39
+ /**
40
+ * Reject the first present portable-policy axis that the adapter kind does not
41
+ * support. Omitted axes are always legal and are never filled with defaults.
42
+ */
43
+ export function assertPortablePolicySupported(adapter, policy) {
44
+ const supported = SUPPORTED_EXPLICIT[adapter];
45
+ const checks = [
46
+ ["access", policy.access],
47
+ ["network", policy.network],
48
+ ["webSearch", policy.webSearch],
49
+ ];
50
+ for (const [knob, value] of checks) {
51
+ if (value === undefined)
52
+ continue;
53
+ const allowed = supported[knob];
54
+ if (!allowed.has(value)) {
55
+ throw new FlowError("UNSUPPORTED_POLICY", `Provider kind '${adapter}' does not support ${knob} '${value}'.`);
56
+ }
57
+ }
58
+ }
59
+ function isValidAkumaCandidate(candidate) {
60
+ return !("reason" in candidate && candidate.reason !== undefined) && candidate.profile !== undefined && candidate.body !== undefined;
61
+ }
62
+ function freezeResolvedProfile(profile) {
63
+ if (profile.effortOptions === undefined) {
64
+ return Object.freeze({ ...profile });
65
+ }
66
+ return Object.freeze({
67
+ ...profile,
68
+ effortOptions: Object.freeze([...profile.effortOptions]),
69
+ });
70
+ }
71
+ export async function selectSubagentV2(agentName, cwd) {
72
+ const loaded = await loadKeiyakuSettings(cwd);
73
+ assertSettingsKnobUsable(loaded, "agents");
74
+ const settings = loaded.knobs;
75
+ const requested = agentName?.trim();
76
+ if (!requested && !getConfig().subagent.nameOverride) {
77
+ assertSettingsKnobUsable(loaded, "default");
78
+ }
79
+ const selected = requested || getConfig().subagent.nameOverride || resolveDefaultAgentName(settings);
80
+ const catalog = await loadAkumaCatalogV2(cwd);
81
+ const availableNames = Object.keys(catalog.selected);
82
+ const candidate = catalog.selected[selected];
83
+ if (candidate === undefined) {
84
+ throw new FlowError("UNKNOWN_SUBAGENT", `Unknown agent '${selected}'. Expected one of: ${formatAvailableAgentNamesV2(availableNames)}`);
85
+ }
86
+ if (!isValidAkumaCandidate(candidate)) {
87
+ throw new FlowError("INVALID_SETTINGS", `Akuma '${selected}' is invalid: ${candidate.reason}`);
88
+ }
89
+ const instanceName = candidate.profile.provider;
90
+ const providerCandidate = loaded.providerInstances.selected[instanceName];
91
+ if (providerCandidate === undefined) {
92
+ throw new FlowError("INVALID_SETTINGS", `Akuma '${selected}' references missing provider instance '${instanceName}'.`);
93
+ }
94
+ if (!("instance" in providerCandidate) || providerCandidate.instance === undefined) {
95
+ throw new FlowError("INVALID_SETTINGS", `Akuma '${selected}' references invalid provider instance '${instanceName}': ${providerCandidate.reason}`);
96
+ }
97
+ const providerInstance = providerCandidate.instance;
98
+ const adapter = providerInstance.kind;
99
+ assertPortablePolicySupported(adapter, {
100
+ ...(candidate.profile.access !== undefined ? { access: candidate.profile.access } : {}),
101
+ ...(candidate.profile.network !== undefined ? { network: candidate.profile.network } : {}),
102
+ ...(candidate.profile.webSearch !== undefined ? { webSearch: candidate.profile.webSearch } : {}),
103
+ });
104
+ const profile = freezeResolvedProfile({
105
+ provider: instanceName,
106
+ adapter,
107
+ providerSource: providerCandidate.source,
108
+ providerInstance,
109
+ ...(candidate.profile.description !== undefined ? { description: candidate.profile.description } : {}),
110
+ ...(candidate.profile.model !== undefined ? { model: candidate.profile.model } : {}),
111
+ ...(candidate.profile.effort !== undefined ? { effort: candidate.profile.effort } : {}),
112
+ ...(candidate.profile.effortOptions !== undefined ? { effortOptions: candidate.profile.effortOptions } : {}),
113
+ ...(candidate.profile.access !== undefined ? { access: candidate.profile.access } : {}),
114
+ ...(candidate.profile.network !== undefined ? { network: candidate.profile.network } : {}),
115
+ ...(candidate.profile.webSearch !== undefined ? { webSearch: candidate.profile.webSearch } : {}),
116
+ });
117
+ return Object.freeze({
118
+ name: selected,
119
+ definition: candidate,
120
+ profile,
121
+ });
122
+ }