@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.
- package/README.md +73 -56
- package/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms_v2.js +103 -0
- package/build/agents/harness/activity-values.js +54 -0
- package/build/agents/harness/event-channel.js +55 -20
- package/build/agents/harness/events.js +47 -1
- package/build/agents/harness/execution-handle.js +292 -61
- package/build/agents/harness/index.js +103 -121
- package/build/agents/harness/pump.js +36 -47
- package/build/agents/harness/runtime.js +3 -0
- package/build/agents/launch-snapshot_v2.js +348 -0
- package/build/agents/opencode-sdk.js +42 -169
- package/build/agents/providers/claude-agent-sdk.js +240 -79
- package/build/agents/providers/codex-app-server.js +280 -72
- package/build/agents/providers/codex-sdk.js +159 -41
- package/build/agents/providers/event-type-tail.js +16 -0
- package/build/agents/providers/opencode-sdk.js +165 -59
- package/build/agents/providers/pi.js +254 -42
- package/build/agents/selector_v2.js +122 -0
- package/build/cli/commands/akuma.js +120 -42
- package/build/cli/completion.js +6 -7
- package/build/cli/flags.js +36 -10
- package/build/cli/help.js +42 -26
- package/build/cli/index.js +225 -93
- package/build/cli/parse.js +194 -75
- package/build/cli/projection-address.js +11 -0
- package/build/cli/render/address.js +88 -0
- package/build/cli/render/arc.js +10 -10
- package/build/cli/render/call.js +59 -0
- package/build/cli/render/compact-text.js +3 -0
- package/build/cli/render/errors.js +4 -2
- package/build/cli/render/format.js +6 -0
- package/build/cli/render/line-width.js +70 -0
- package/build/cli/render/misc.js +7 -4
- package/build/cli/render/petition.js +4 -3
- package/build/cli/render/projection-activity.js +201 -0
- package/build/cli/render/shared.js +46 -12
- package/build/cli/render/status.js +59 -10
- package/build/cli/render/tool-ledger-rollup.js +74 -0
- package/build/cli/render/tool-presentation.js +97 -0
- package/build/cli/render/wait.js +229 -0
- package/build/cli/skills-install.js +42 -24
- package/build/cli/subagent-guard.js +1 -1
- package/build/config/akuma-loader_v2.js +222 -0
- package/build/config/env-keys.js +0 -4
- package/build/config/env.js +0 -6
- package/build/config/settings.js +325 -12
- package/build/core/addressing.js +224 -0
- package/build/core/amend.js +30 -47
- package/build/core/arc.js +68 -60
- package/build/core/bind.js +105 -25
- package/build/core/call-persist.js +69 -0
- package/build/core/call.js +658 -0
- package/build/core/claim.js +72 -44
- package/build/core/command-io.js +19 -19
- package/build/core/context.js +11 -36
- package/build/core/draft.js +2 -3
- package/build/core/entry.js +101 -6
- package/build/core/execution-coordinate.js +62 -0
- package/build/core/execution-pact.js +141 -0
- package/build/core/forfeit.js +2 -3
- package/build/core/hints.js +10 -10
- package/build/core/ids.js +6 -0
- package/build/core/ledger.js +16 -1
- package/build/core/petition-claim-gates.js +14 -12
- package/build/core/petition-claim.js +1 -0
- package/build/core/petition-run.js +6 -2
- package/build/core/petition.js +63 -21
- package/build/core/places.js +106 -13
- package/build/core/projection/generation.js +412 -0
- package/build/core/projection/heart.js +316 -0
- package/build/core/projection/identity.js +81 -0
- package/build/core/projection/leash.js +87 -0
- package/build/core/projection/mint.js +154 -0
- package/build/core/projection-activity.js +219 -0
- package/build/core/projection-coordinate.js +35 -0
- package/build/core/projection-core.js +69 -463
- package/build/core/projection-generation-continuation.js +31 -0
- package/build/core/projection-generation-execution.js +148 -0
- package/build/core/projection-generation-identity.js +17 -0
- package/build/core/projection-generation-launcher.js +90 -0
- package/build/core/projection-generation-process.js +43 -0
- package/build/core/projection-generation-runner.js +144 -0
- package/build/core/projection-generation-runtime.js +15 -0
- package/build/core/projection-generation-store.js +707 -0
- package/build/core/projection-identity.js +11 -0
- package/build/core/projection-life-observer.js +87 -0
- package/build/core/projection-life-protocol.js +95 -0
- package/build/core/projection-mint.js +96 -24
- package/build/core/projection-runner-lock.js +230 -0
- package/build/core/projection-status.js +207 -107
- package/build/core/projection-tells.js +401 -0
- package/build/core/projection-wait.js +155 -0
- package/build/core/projection-wake.js +109 -231
- package/build/core/queue_v2.js +342 -0
- package/build/core/registry.js +6 -3
- package/build/core/render.js +63 -1
- package/build/core/renew.js +80 -53
- package/build/core/scope.js +186 -137
- package/build/core/seal.js +16 -19
- package/build/core/status.js +88 -25
- package/build/core/stored-agent-event.js +101 -0
- package/build/core/transcripts.js +214 -228
- package/build/core/verdict.js +27 -15
- package/build/core/worktree-bootstrap.js +187 -0
- package/build/core/worktree-path.js +72 -18
- package/build/flow-error.js +4 -6
- package/build/generated/version.js +1 -1
- package/build/git/branches.js +6 -1
- package/build/index.js +8 -5
- package/build/keiyaku.js +1 -3
- package/package.json +6 -5
- package/skills/keiyaku/SKILL.md +6 -47
- package/skills/keiyaku-akuma/SKILL.md +75 -0
- package/build/agents/effective-policy.js +0 -32
- package/build/agents/index.js +0 -114
- package/build/agents/selector.js +0 -28
- package/build/cli/render/summon.js +0 -17
- package/build/cli/render/tell.js +0 -11
- package/build/config/akuma-loader.js +0 -268
- package/build/core/queue.js +0 -73
- package/build/core/summon-persist.js +0 -73
- 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 "
|
|
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
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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({
|
|
162
|
-
: await withAbort(sdk.client.session.create({
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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({
|
|
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,
|