@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
@@ -1,11 +1,27 @@
1
1
  import { z } from "zod";
2
- import { Codex } from "@openai/codex-sdk";
2
+ import { Codex, } from "@openai/codex-sdk";
3
3
  import { CODEX_MODEL_REASONING_EFFORT_CONFIG_KEY } from "../../keiyaku.js";
4
4
  import { appendDebugBlock, appendDebugLog } from "../../telemetry/debug-log.js";
5
5
  import { SUBAGENT_SESSION_VERSION } from "../session.js";
6
6
  import { createProjectionDriver } from "../harness/execution-handle.js";
7
+ import { boundActivityExcerpt } from "../harness/activity-values.js";
7
8
  import { buildSubagentEnv, emitProviderEvidence, sha256Utf8 } from "../harness/runtime.js";
8
- import { coerceString } from "../../agents/index.js";
9
+ import { coerceString } from "../harness/runtime.js";
10
+ import { AGENT_PLAN_ITEM_TEXT_MAX_CHARS, AGENT_PLAN_MAX_ITEMS, AGENT_THOUGHT_TEXT_MAX_CHARS, } from "../harness/events.js";
11
+ import { appendEventType, renderEventTypeSummary } from "./event-type-tail.js";
12
+ /** Identity-only opacity boundary for Codex SDK ThreadOptions modelReasoningEffort. */
13
+ function toCodexSdkModelReasoningEffort(effort) {
14
+ return effort;
15
+ }
16
+ function withCodexSdkModelReasoningEffort(threadOptions) {
17
+ const raw = threadOptions.modelReasoningEffort;
18
+ if (typeof raw !== "string")
19
+ return threadOptions;
20
+ return {
21
+ ...threadOptions,
22
+ modelReasoningEffort: toCodexSdkModelReasoningEffort(raw),
23
+ };
24
+ }
9
25
  let codexConstructor = Codex;
10
26
  export function setCodexSdkConstructorForTests(constructor) {
11
27
  codexConstructor = constructor;
@@ -18,30 +34,128 @@ const CODEX_EVENT_THREAD_STARTED = "thread.started";
18
34
  const CODEX_EVENT_TURN_STARTED = "turn.started";
19
35
  const CODEX_EVENT_TURN_FAILED = "turn.failed";
20
36
  const CODEX_EVENT_ERROR = "error";
37
+ const CODEX_EVENT_ITEM_STARTED = "item.started";
21
38
  const CODEX_EVENT_ITEM_COMPLETED = "item.completed";
22
39
  const CODEX_EVENT_ITEM_UPDATED = "item.updated";
40
+ const CODEX_EVENT_TURN_COMPLETED = "turn.completed";
23
41
  const CODEX_SDK_PROVIDER = "codex-sdk";
24
- const MAX_RECORDED_EVENT_TYPES = 100;
42
+ const CODEX_SDK_ACKNOWLEDGED_UNPERSISTED = new Set([CODEX_EVENT_THREAD_STARTED]);
43
+ const CODEX_SDK_MAPPED = new Set([
44
+ CODEX_EVENT_TURN_STARTED,
45
+ CODEX_EVENT_TURN_COMPLETED,
46
+ CODEX_EVENT_TURN_FAILED,
47
+ CODEX_EVENT_ERROR,
48
+ CODEX_EVENT_ITEM_STARTED,
49
+ CODEX_EVENT_ITEM_UPDATED,
50
+ CODEX_EVENT_ITEM_COMPLETED,
51
+ ]);
52
+ function codexSdkIngressDisposition(type) {
53
+ if (CODEX_SDK_ACKNOWLEDGED_UNPERSISTED.has(type))
54
+ return "acknowledged-unpersisted";
55
+ if (CODEX_SDK_MAPPED.has(type))
56
+ return "mapped";
57
+ return "unknown";
58
+ }
25
59
  function extractAgentMessage(event) {
26
60
  if (event.type !== CODEX_EVENT_ITEM_COMPLETED && event.type !== CODEX_EVENT_ITEM_UPDATED) {
27
61
  return undefined;
28
62
  }
29
63
  return event.item.type === "agent_message" ? coerceString(event.item.text) : undefined;
30
64
  }
31
- function appendEventType(recordedTypes, eventType) {
32
- if (recordedTypes.length === MAX_RECORDED_EVENT_TYPES) {
33
- recordedTypes.shift();
65
+ function codexActivityCall(item) {
66
+ switch (item.type) {
67
+ case "command_execution": return { kind: "run", command: item.command };
68
+ case "file_change": return {
69
+ kind: "fileChange",
70
+ changes: item.changes.map((change) => ({ op: change.kind, path: change.path })),
71
+ };
72
+ case "mcp_tool_call": return { kind: "other", display: `${item.server}.${item.tool}` };
73
+ case "web_search": return { kind: "search", query: item.query };
34
74
  }
35
- recordedTypes.push(eventType);
36
75
  }
37
- function renderEventTypeSummary(eventTypes, totalEventCount) {
38
- if (eventTypes.length === 0)
39
- return "(no streamed events)";
40
- const startIndex = Math.max(totalEventCount - eventTypes.length, 0);
41
- const summary = eventTypes.map((eventType, index) => `${startIndex + index + 1}. ${eventType}`).join("\n");
42
- if (totalEventCount <= eventTypes.length)
43
- return summary;
44
- return `...[showing last ${eventTypes.length} of ${totalEventCount} events]...\n${summary}`;
76
+ function codexMcpResultMessage(item) {
77
+ if (item.error?.message)
78
+ return boundActivityExcerpt(item.error.message);
79
+ const text = item.result?.content.flatMap((block) => (block.type === "text" && typeof block.text === "string" ? [block.text] : [])).join("\n");
80
+ return text ? boundActivityExcerpt(text) : undefined;
81
+ }
82
+ function codexActivityResult(item) {
83
+ switch (item.type) {
84
+ case "command_execution": return {
85
+ status: item.status === "failed" || (item.exit_code !== undefined && item.exit_code !== 0) ? "error" : "ok",
86
+ ...(item.aggregated_output ? { message: boundActivityExcerpt(item.aggregated_output) } : {}),
87
+ ...(item.exit_code === undefined ? {} : { exitCode: item.exit_code }),
88
+ };
89
+ case "file_change": return { status: item.status === "failed" ? "error" : "ok" };
90
+ case "mcp_tool_call": {
91
+ const message = codexMcpResultMessage(item);
92
+ return { status: item.status === "failed" ? "error" : "ok", ...(message ? { message } : {}) };
93
+ }
94
+ case "web_search": return { status: "ok" };
95
+ }
96
+ }
97
+ function emitCodexSemanticEvent(builder, event) {
98
+ if (event.type === CODEX_EVENT_TURN_STARTED) {
99
+ builder.emit({ type: "turn", phase: "started" });
100
+ return;
101
+ }
102
+ if (event.type === CODEX_EVENT_TURN_COMPLETED) {
103
+ builder.emit({
104
+ type: "usage",
105
+ inputTokens: event.usage.input_tokens,
106
+ outputTokens: event.usage.output_tokens,
107
+ cachedInputTokens: event.usage.cached_input_tokens,
108
+ });
109
+ builder.emit({ type: "turn", phase: "completed" });
110
+ return;
111
+ }
112
+ if (event.type !== CODEX_EVENT_ITEM_STARTED &&
113
+ event.type !== CODEX_EVENT_ITEM_UPDATED &&
114
+ event.type !== CODEX_EVENT_ITEM_COMPLETED) {
115
+ return;
116
+ }
117
+ if (event.item.type === "reasoning" &&
118
+ (event.type === CODEX_EVENT_ITEM_UPDATED || event.type === CODEX_EVENT_ITEM_COMPLETED)) {
119
+ const truncated = event.item.text.length > AGENT_THOUGHT_TEXT_MAX_CHARS;
120
+ builder.emit({
121
+ type: "thought",
122
+ id: event.item.id,
123
+ text: event.item.text.slice(0, AGENT_THOUGHT_TEXT_MAX_CHARS),
124
+ final: event.type === CODEX_EVENT_ITEM_COMPLETED,
125
+ ...(truncated ? { truncated: true } : {}),
126
+ });
127
+ return;
128
+ }
129
+ if (event.item.type === "todo_list") {
130
+ builder.emit({
131
+ type: "plan",
132
+ id: event.item.id,
133
+ items: event.item.items.slice(0, AGENT_PLAN_MAX_ITEMS).map((item) => ({
134
+ text: item.text.slice(0, AGENT_PLAN_ITEM_TEXT_MAX_CHARS),
135
+ done: item.completed,
136
+ })),
137
+ });
138
+ return;
139
+ }
140
+ if (event.item.type !== "command_execution" &&
141
+ event.item.type !== "file_change" &&
142
+ event.item.type !== "mcp_tool_call" &&
143
+ event.item.type !== "web_search") {
144
+ return;
145
+ }
146
+ const phase = event.type === CODEX_EVENT_ITEM_STARTED
147
+ ? "started"
148
+ : event.type === CODEX_EVENT_ITEM_UPDATED
149
+ ? "updated"
150
+ : "completed";
151
+ builder.emit({
152
+ type: "activity",
153
+ phase,
154
+ id: event.item.id,
155
+ tool: event.item.type,
156
+ call: codexActivityCall(event.item),
157
+ ...(phase === "started" ? {} : { result: codexActivityResult(event.item) }),
158
+ });
45
159
  }
46
160
  export async function startCodexSdk(prompt, cwd, options) {
47
161
  const threadId = options.threadId?.trim();
@@ -51,6 +165,8 @@ export async function startCodexSdk(prompt, cwd, options) {
51
165
  appendDebugLog(`startCodexSdk: executable=${options.executable ?? "codex"} model=${options.model ?? "default"} ${CODEX_MODEL_REASONING_EFFORT_CONFIG_KEY}=${String(options.config?.[CODEX_MODEL_REASONING_EFFORT_CONFIG_KEY] ?? "default")} mode=${threadId ? "resume" : "fresh"}`, { cwd, section: CODEX_SDK_PROVIDER });
52
166
  return createProjectionDriver({
53
167
  steerMode: "replay",
168
+ initialTurn: options.initialTurn,
169
+ onIngress: options.onIngress,
54
170
  async abort() {
55
171
  activeAbort?.abort();
56
172
  },
@@ -71,7 +187,7 @@ export async function startCodexSdk(prompt, cwd, options) {
71
187
  env,
72
188
  };
73
189
  const sdk = new codexConstructor(codexOptions);
74
- const threadOptions = options.threadOptions;
190
+ const threadOptions = withCodexSdkModelReasoningEffort(options.threadOptions);
75
191
  const thread = threadId ? sdk.resumeThread(threadId, threadOptions) : sdk.startThread(threadOptions);
76
192
  const streamed = await thread.runStreamed(prompt, { signal: activeAbort.signal });
77
193
  const eventIterator = streamed.events[Symbol.asyncIterator]();
@@ -116,31 +232,33 @@ export async function startCodexSdk(prompt, cwd, options) {
116
232
  }
117
233
  totalEventCount += 1;
118
234
  appendEventType(recordedEventTypes, event.type);
119
- builder.emit({ type: "output", value: event });
120
- if (event.type === CODEX_EVENT_THREAD_STARTED) {
121
- resumeToken = event.thread_id;
122
- if (options.onEvidence)
123
- emitProviderEvidence(options.onEvidence, { type: "session", provider: CODEX_SDK_PROVIDER, threadId: resumeToken }, (text) => builder.emit({ type: "diagnostic", text }));
124
- }
125
- else if (event.type === CODEX_EVENT_TURN_STARTED && !resumeToken) {
126
- builder.emit({
127
- type: "fail",
128
- message: `${CODEX_SDK_PROVIDER} exec emitted turn.started before thread.started with thread_id`,
129
- });
130
- throw new Error(`${CODEX_SDK_PROVIDER} exec emitted turn.started before thread.started with thread_id`);
131
- }
132
- else if (event.type === CODEX_EVENT_TURN_FAILED) {
133
- turnFailedMessage = coerceString(event.error.message) ?? `${CODEX_SDK_PROVIDER} turn failed`;
134
- }
135
- else if (event.type === CODEX_EVENT_ERROR) {
136
- streamErrorMessage = coerceString(event.message) ?? `${CODEX_SDK_PROVIDER} stream error`;
137
- builder.emit({ type: "diagnostic", text: `${streamErrorMessage}\n` });
138
- }
139
- const message = extractAgentMessage(event);
140
- if (message) {
141
- lastAgentMessage = message;
142
- builder.emit({ type: "message", text: message });
143
- }
235
+ builder.ingress(CODEX_SDK_PROVIDER, event.type, event, codexSdkIngressDisposition(event.type), () => {
236
+ if (event.type === CODEX_EVENT_THREAD_STARTED) {
237
+ resumeToken = event.thread_id;
238
+ if (options.onEvidence)
239
+ emitProviderEvidence(options.onEvidence, { type: "session", provider: CODEX_SDK_PROVIDER, threadId: resumeToken }, (text) => builder.emit({ type: "diagnostic", text }));
240
+ }
241
+ else if (event.type === CODEX_EVENT_TURN_STARTED && !resumeToken) {
242
+ builder.emit({
243
+ type: "fail",
244
+ message: `${CODEX_SDK_PROVIDER} exec emitted turn.started before thread.started with thread_id`,
245
+ });
246
+ throw new Error(`${CODEX_SDK_PROVIDER} exec emitted turn.started before thread.started with thread_id`);
247
+ }
248
+ else if (event.type === CODEX_EVENT_TURN_FAILED) {
249
+ turnFailedMessage = coerceString(event.error.message) ?? `${CODEX_SDK_PROVIDER} turn failed`;
250
+ }
251
+ else if (event.type === CODEX_EVENT_ERROR) {
252
+ streamErrorMessage = coerceString(event.message) ?? `${CODEX_SDK_PROVIDER} stream error`;
253
+ builder.emit({ type: "diagnostic", text: `${streamErrorMessage}\n` });
254
+ }
255
+ emitCodexSemanticEvent(builder, event);
256
+ const message = extractAgentMessage(event);
257
+ if (message) {
258
+ lastAgentMessage = message;
259
+ builder.emit({ type: "message", text: message });
260
+ }
261
+ });
144
262
  }
145
263
  if (turnFailedMessage) {
146
264
  builder.emit({ type: "diagnostic", text: turnFailedMessage });
@@ -0,0 +1,16 @@
1
+ export const MAX_RECORDED_EVENT_TYPES = 100;
2
+ export function appendEventType(recordedTypes, eventType) {
3
+ if (recordedTypes.length === MAX_RECORDED_EVENT_TYPES) {
4
+ recordedTypes.shift();
5
+ }
6
+ recordedTypes.push(eventType);
7
+ }
8
+ export function renderEventTypeSummary(eventTypes, totalEventCount) {
9
+ if (eventTypes.length === 0)
10
+ return "(no streamed events)";
11
+ const startIndex = Math.max(totalEventCount - eventTypes.length, 0);
12
+ const summary = eventTypes.map((eventType, index) => `${startIndex + index + 1}. ${eventType}`).join("\n");
13
+ if (totalEventCount <= eventTypes.length)
14
+ return summary;
15
+ return `...[showing last ${eventTypes.length} of ${totalEventCount} events]...\n${summary}`;
16
+ }
@@ -1,43 +1,54 @@
1
1
  import { z } from "zod";
2
+ import { FlowError } from "../../flow-error.js";
2
3
  import { appendDebugBlock, appendDebugLog } from "../../telemetry/debug-log.js";
3
4
  import { createOpencode } from "../opencode-sdk.js";
4
5
  import { SUBAGENT_SESSION_VERSION } from "../session.js";
5
6
  import { createProjectionDriver } from "../harness/execution-handle.js";
6
- import { buildSubagentEnv, createAbortError } from "../harness/runtime.js";
7
- import { emitProviderEvidence, sha256Utf8 } from "../harness/runtime.js";
8
- import { coerceString } from "../index.js";
7
+ import { buildSubagentEnv, coerceString, createAbortError, emitProviderEvidence, sha256Utf8 } from "../harness/runtime.js";
8
+ import { activityCallFromNamedTool } from "../harness/activity-values.js";
9
+ import { appendEventType, renderEventTypeSummary } from "./event-type-tail.js";
10
+ /** Identity-only opacity boundary for OpenCode session.prompt variant. */
11
+ function toOpencodeSdkVariant(effort) {
12
+ return effort;
13
+ }
9
14
  export const opencodeSessionSchema = z.object({ sessionId: z.string().trim().min(1) });
10
15
  const OPENCODE_EVENT_SESSION_IDLE = "session.idle";
11
16
  const OPENCODE_EVENT_SESSION_ERROR = "session.error";
12
- const OPENCODE_EVENT_MESSAGE_UPDATED = "message.updated";
13
- const OPENCODE_EVENT_MESSAGE_PART_UPDATED = "message.part.updated";
14
- const OPENCODE_EVENT_MESSAGE_PART_ADDED = "message.part.added";
15
17
  const OPENCODE_SDK_PROVIDER = "opencode-sdk";
18
+ function opencodeIngressDisposition(event) {
19
+ if (event.type === OPENCODE_EVENT_SESSION_IDLE)
20
+ return "acknowledged-unpersisted";
21
+ if (event.type === OPENCODE_EVENT_SESSION_ERROR)
22
+ return "mapped";
23
+ if (event.type === "message.part.updated" && event.properties.part.type === "tool") {
24
+ return event.properties.part.state.status === "pending" ? "acknowledged-unpersisted" : "mapped";
25
+ }
26
+ return "unknown";
27
+ }
16
28
  const OPENCODE_DEFAULT_EXECUTABLE = "opencode";
17
- const OPENCODE_MAX_RECORDED_EVENT_TYPES = 100;
18
29
  const OPENCODE_ASSISTANT_ROLE = "assistant";
19
30
  const OPENCODE_TEXT_PART_TYPE = "text";
20
31
  let opencodeSdkLoader;
21
- function appendEventType(recordedTypes, eventType) {
22
- if (recordedTypes.length === OPENCODE_MAX_RECORDED_EVENT_TYPES) {
23
- recordedTypes.shift();
32
+ /**
33
+ * Convert configured opaque OpenCode model string at the first slash into the
34
+ * 1.17.20 prompt model object. No string compatibility path remains.
35
+ */
36
+ export function parseOpencodeModelRef(model) {
37
+ const trimmed = model.trim();
38
+ const slash = trimmed.indexOf("/");
39
+ if (slash <= 0 || slash === trimmed.length - 1) {
40
+ throw new FlowError("INVALID_SETTINGS", `Invalid opencode model '${model}'. Expected opaque shape '<provider>/<model>' with non-empty segments split at the first slash.`);
24
41
  }
25
- recordedTypes.push(eventType);
26
- }
27
- function renderEventTypeSummary(eventTypes, totalEventCount) {
28
- if (eventTypes.length === 0)
29
- return "(no streamed events)";
30
- const startIndex = Math.max(totalEventCount - eventTypes.length, 0);
31
- const summary = eventTypes.map((eventType, index) => `${startIndex + index + 1}. ${eventType}`).join("\n");
32
- if (totalEventCount <= eventTypes.length)
33
- return summary;
34
- return `...[showing last ${eventTypes.length} of ${totalEventCount} events]...\n${summary}`;
42
+ return {
43
+ providerID: trimmed.slice(0, slash),
44
+ modelID: trimmed.slice(slash + 1),
45
+ };
35
46
  }
36
47
  function extractTextFromMessage(message) {
37
48
  if (!message?.parts)
38
49
  return undefined;
39
50
  const text = message.parts
40
- .filter((part) => !part.type || part.type === OPENCODE_TEXT_PART_TYPE)
51
+ .filter((part) => part.type === OPENCODE_TEXT_PART_TYPE)
41
52
  .map((part) => coerceString(part.text))
42
53
  .filter((part) => Boolean(part))
43
54
  .join("");
@@ -57,6 +68,64 @@ function extractFinalAssistantMessage(messages) {
57
68
  }
58
69
  return lastAssistantText ?? lastText;
59
70
  }
71
+ function eventSessionId(event) {
72
+ const properties = event.properties;
73
+ if (!properties || typeof properties !== "object" || !("sessionID" in properties))
74
+ return undefined;
75
+ return typeof properties.sessionID === "string" ? properties.sessionID : undefined;
76
+ }
77
+ function extractToolCall(part) {
78
+ const input = part.state.input;
79
+ const stringFact = (primary, fallback) => {
80
+ const first = input[primary];
81
+ if (typeof first === "string" && first.length > 0)
82
+ return first;
83
+ const second = fallback === undefined ? undefined : input[fallback];
84
+ return typeof second === "string" && second.length > 0 ? second : undefined;
85
+ };
86
+ const integerFact = (key) => {
87
+ const value = input[key];
88
+ return typeof value === "number" && Number.isInteger(value) ? value : undefined;
89
+ };
90
+ const tool = part.tool.toLowerCase();
91
+ let facts = {};
92
+ if (tool === "bash" || tool === "shell") {
93
+ facts = { command: stringFact("command") };
94
+ }
95
+ else if (tool === "read") {
96
+ facts = {
97
+ path: stringFact("filePath", "path"),
98
+ offset: integerFact("offset"),
99
+ limit: integerFact("limit"),
100
+ };
101
+ }
102
+ else if (tool === "write" || tool === "edit") {
103
+ facts = { path: stringFact("filePath", "path") };
104
+ }
105
+ else if (tool === "grep" || tool === "search" || tool === "glob") {
106
+ facts = { query: stringFact("pattern", "query") };
107
+ }
108
+ const classifierTool = tool === "grep" || tool === "glob" ? "search" : part.tool;
109
+ return activityCallFromNamedTool(classifierTool, facts);
110
+ }
111
+ function completedResult(state) {
112
+ const exit = state.metadata.exit;
113
+ const exitCode = typeof exit === "number" && Number.isInteger(exit) ? exit : undefined;
114
+ const message = coerceString(state.output);
115
+ return {
116
+ status: exitCode !== undefined && exitCode !== 0 ? "error" : "ok",
117
+ ...(message ? { message } : {}),
118
+ ...(exitCode === undefined ? {} : { exitCode }),
119
+ };
120
+ }
121
+ function isToolPartUpdated(event) {
122
+ return event.type === "message.part.updated" && event.properties.part.type === "tool";
123
+ }
124
+ function sessionErrorMessage(event) {
125
+ const data = event.properties.error?.data;
126
+ const message = data && "message" in data ? coerceString(data.message) : undefined;
127
+ return message ?? `${OPENCODE_SDK_PROVIDER} session error`;
128
+ }
60
129
  async function withAbort(promise, signal, message) {
61
130
  if (signal.aborted) {
62
131
  throw createAbortError(message);
@@ -81,7 +150,7 @@ async function withAbort(promise, signal, message) {
81
150
  }
82
151
  async function loadOpencodeSdk(executable, cwd, env, signal) {
83
152
  if (opencodeSdkLoader) {
84
- return opencodeSdkLoader();
153
+ return opencodeSdkLoader(cwd);
85
154
  }
86
155
  const sdk = await createOpencode({
87
156
  executable,
@@ -95,33 +164,65 @@ async function loadOpencodeSdk(executable, cwd, env, signal) {
95
164
  };
96
165
  }
97
166
  async function awaitSessionIdle(input) {
98
- const subscribed = await input.client.event.subscribe();
167
+ const firstRunning = new Map();
168
+ const running = new Set();
169
+ const subscribed = await input.client.event.subscribe({ directory: input.directory });
99
170
  for await (const event of subscribed.stream) {
100
- input.emit({ type: "output", value: event });
101
- input.incrementEventCount();
102
- appendEventType(input.recordedEventTypes, event.type);
103
- if (event.properties?.sessionID !== input.sessionId) {
171
+ if (eventSessionId(event) !== input.sessionId) {
104
172
  continue;
105
173
  }
106
- if (event.type === OPENCODE_EVENT_SESSION_ERROR) {
107
- const errorMessage = coerceString(event.error?.message) ??
108
- coerceString(event.properties?.message) ??
109
- `${OPENCODE_SDK_PROVIDER} session error`;
110
- input.emit({ type: "diagnostic", text: errorMessage });
111
- input.emit({ type: "fail", message: errorMessage });
112
- throw new Error(errorMessage);
113
- }
114
- if (event.type === OPENCODE_EVENT_SESSION_IDLE ||
115
- event.type === OPENCODE_EVENT_MESSAGE_UPDATED ||
116
- event.type === OPENCODE_EVENT_MESSAGE_PART_UPDATED ||
117
- event.type === OPENCODE_EVENT_MESSAGE_PART_ADDED) {
118
- if (event.type === OPENCODE_EVENT_SESSION_IDLE) {
174
+ input.incrementEventCount();
175
+ appendEventType(input.recordedEventTypes, event.type);
176
+ let sessionError;
177
+ input.builder.ingress(OPENCODE_SDK_PROVIDER, event.type, event, opencodeIngressDisposition(event), () => {
178
+ if (event.type === OPENCODE_EVENT_SESSION_ERROR) {
179
+ sessionError = sessionErrorMessage(event);
180
+ input.builder.emit({ type: "diagnostic", text: sessionError });
181
+ input.builder.emit({ type: "fail", message: sessionError });
119
182
  return;
120
183
  }
184
+ if (isToolPartUpdated(event)) {
185
+ const part = event.properties.part;
186
+ if (part.state.status === "pending")
187
+ return;
188
+ if (part.state.status === "running") {
189
+ const activity = firstRunning.get(part.id) ?? { tool: part.tool, call: extractToolCall(part) };
190
+ if (!firstRunning.has(part.id))
191
+ firstRunning.set(part.id, activity);
192
+ const phase = running.has(part.id) ? "updated" : "started";
193
+ running.add(part.id);
194
+ input.builder.emit({ type: "activity", phase, id: part.id, ...activity });
195
+ }
196
+ else if (part.state.status === "completed") {
197
+ const activity = firstRunning.get(part.id) ?? { tool: part.tool, call: extractToolCall(part) };
198
+ input.builder.emit({
199
+ type: "activity",
200
+ phase: "completed",
201
+ id: part.id,
202
+ ...activity,
203
+ result: completedResult(part.state),
204
+ });
205
+ }
206
+ else if (part.state.status === "error") {
207
+ const activity = firstRunning.get(part.id) ?? { tool: part.tool, call: extractToolCall(part) };
208
+ input.builder.emit({
209
+ type: "activity",
210
+ phase: "completed",
211
+ id: part.id,
212
+ ...activity,
213
+ result: { status: "error", message: part.state.error },
214
+ });
215
+ }
216
+ }
217
+ });
218
+ if (sessionError !== undefined)
219
+ throw new Error(sessionError);
220
+ if (event.type === OPENCODE_EVENT_SESSION_IDLE) {
221
+ return;
121
222
  }
122
223
  }
123
224
  const message = `${OPENCODE_SDK_PROVIDER} event stream ended before ${OPENCODE_EVENT_SESSION_IDLE}`;
124
- input.emit({ type: "fail", message });
225
+ input.builder.emit({ type: "fail", message });
125
226
  throw new Error(message);
126
227
  }
127
228
  export function setOpencodeSdkLoaderForTests(loader) {
@@ -133,6 +234,9 @@ export function resetOpencodeSdkLoaderForTests() {
133
234
  export async function startOpencode(prompt, cwd, options = {}) {
134
235
  const sessionId = options.sessionId?.trim();
135
236
  const executable = options.executable ?? OPENCODE_DEFAULT_EXECUTABLE;
237
+ // A configured value is always intentional. Validate even empty/blank input
238
+ // before loading the SDK so it cannot silently become the provider default.
239
+ const modelRef = options.model === undefined ? undefined : parseOpencodeModelRef(options.model);
136
240
  const recordedEventTypes = [];
137
241
  let totalEventCount = 0;
138
242
  let resumedSessionId;
@@ -141,6 +245,8 @@ export async function startOpencode(prompt, cwd, options = {}) {
141
245
  appendDebugLog(`startOpencode: executable=${executable} model=${options.model ?? "default"} mode=${sessionId ? "resume" : "fresh"}`, { cwd, section: OPENCODE_SDK_PROVIDER });
142
246
  return createProjectionDriver({
143
247
  steerMode: "replay",
248
+ initialTurn: options.initialTurn,
249
+ onIngress: options.onIngress,
144
250
  async abort() {
145
251
  activeAbort?.abort();
146
252
  },
@@ -157,15 +263,11 @@ export async function startOpencode(prompt, cwd, options = {}) {
157
263
  try {
158
264
  const env = buildSubagentEnv();
159
265
  sdk = await withAbort(loadOpencodeSdk(executable, cwd, env, combined.signal), combined.signal, `${OPENCODE_SDK_PROVIDER} exec cancelled by client`);
266
+ // OpenCode 1.17.20 session.create has no model field; keep cwd only.
160
267
  const session = sessionId
161
- ? await withAbort(sdk.client.session.get({ path: { id: sessionId } }), combined.signal, `${OPENCODE_SDK_PROVIDER} exec cancelled by client`)
162
- : await withAbort(sdk.client.session.create({
163
- body: {
164
- ...(options.model ? { model: options.model } : {}),
165
- cwd,
166
- },
167
- }), combined.signal, `${OPENCODE_SDK_PROVIDER} exec cancelled by client`);
168
- resumedSessionId = session.id || sessionId;
268
+ ? await withAbort(sdk.client.session.get({ sessionID: sessionId, directory: cwd }, { throwOnError: true }), combined.signal, `${OPENCODE_SDK_PROVIDER} exec cancelled by client`)
269
+ : await withAbort(sdk.client.session.create({ directory: cwd }, { throwOnError: true }), combined.signal, `${OPENCODE_SDK_PROVIDER} exec cancelled by client`);
270
+ resumedSessionId = session.data.id || sessionId;
169
271
  if (!resumedSessionId) {
170
272
  const message = `${OPENCODE_SDK_PROVIDER} exec could not determine session id`;
171
273
  builder.emit({ type: "fail", message });
@@ -176,8 +278,9 @@ export async function startOpencode(prompt, cwd, options = {}) {
176
278
  emitProviderEvidence(options.onEvidence, { type: "session", provider: OPENCODE_SDK_PROVIDER, sessionId: activeSessionId }, (text) => builder.emit({ type: "diagnostic", text }));
177
279
  const idlePromise = awaitSessionIdle({
178
280
  client: sdk.client,
179
- emit: builder.emit,
281
+ builder,
180
282
  sessionId: activeSessionId,
283
+ directory: cwd,
181
284
  recordedEventTypes,
182
285
  incrementEventCount: () => {
183
286
  totalEventCount += 1;
@@ -195,13 +298,15 @@ export async function startOpencode(prompt, cwd, options = {}) {
195
298
  ...sha256Utf8(prompt),
196
299
  }, (text) => builder.emit({ type: "diagnostic", text }));
197
300
  }
301
+ const textPart = { type: OPENCODE_TEXT_PART_TYPE, text: prompt };
302
+ builder.emit({ type: "turn", phase: "started" });
198
303
  const promptPromise = sdk.client.session.prompt({
199
- path: { id: activeSessionId },
200
- body: {
201
- ...(options.model ? { model: options.model } : {}),
202
- parts: [{ type: OPENCODE_TEXT_PART_TYPE, text: prompt }],
203
- },
204
- });
304
+ sessionID: activeSessionId,
305
+ directory: cwd,
306
+ ...(modelRef ? { model: modelRef } : {}),
307
+ ...(options.effort ? { variant: toOpencodeSdkVariant(options.effort) } : {}),
308
+ parts: [textPart],
309
+ }, { throwOnError: true });
205
310
  if (options.onEvidence) {
206
311
  emitProviderEvidence(options.onEvidence, {
207
312
  type: "ack",
@@ -211,10 +316,10 @@ export async function startOpencode(prompt, cwd, options = {}) {
211
316
  }, (text) => builder.emit({ type: "diagnostic", text }));
212
317
  }
213
318
  const [promptResult] = await withAbort(Promise.all([promptPromise, idlePromise]), combined.signal, `${OPENCODE_SDK_PROVIDER} exec cancelled by client`);
214
- let finalMessage = extractTextFromMessage(promptResult);
319
+ let finalMessage = extractTextFromMessage(promptResult.data);
215
320
  if (!finalMessage) {
216
- const messages = await withAbort(sdk.client.session.messages({ path: { id: activeSessionId } }), combined.signal, `${OPENCODE_SDK_PROVIDER} exec cancelled by client`);
217
- finalMessage = extractFinalAssistantMessage(messages);
321
+ const messages = await withAbort(sdk.client.session.messages({ sessionID: activeSessionId, directory: cwd }, { throwOnError: true }), combined.signal, `${OPENCODE_SDK_PROVIDER} exec cancelled by client`);
322
+ finalMessage = extractFinalAssistantMessage(messages.data);
218
323
  }
219
324
  if (finalMessage) {
220
325
  builder.emit({ type: "message", text: finalMessage });
@@ -233,6 +338,7 @@ export async function startOpencode(prompt, cwd, options = {}) {
233
338
  source: "response",
234
339
  }, (text) => builder.emit({ type: "diagnostic", text }));
235
340
  }
341
+ builder.emit({ type: "turn", phase: "completed" });
236
342
  builder.emit({
237
343
  type: "done",
238
344
  finalMessage: resolvedFinal,