@astrosheep/keiyaku 4.6.11 → 4.6.13

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 (138) hide show
  1. package/build/plugins/claude/skills/keiyaku/SKILL.md +3 -1
  2. package/build/plugins/claude/skills/keiyaku-babysit/SKILL.md +1 -1
  3. package/build/plugins/claude/skills/keiyaku-bind/SKILL.md +3 -2
  4. package/build/plugins/claude/skills/keiyaku-workflow/SKILL.md +9 -8
  5. package/build/plugins/codex/skills/keiyaku/SKILL.md +3 -1
  6. package/build/plugins/codex/skills/keiyaku-babysit/SKILL.md +1 -1
  7. package/build/plugins/codex/skills/keiyaku-bind/SKILL.md +3 -2
  8. package/build/plugins/codex/skills/keiyaku-workflow/SKILL.md +9 -8
  9. package/build/plugins/opencode/skills/keiyaku/SKILL.md +3 -1
  10. package/build/plugins/opencode/skills/keiyaku-babysit/SKILL.md +1 -1
  11. package/build/plugins/opencode/skills/keiyaku-bind/SKILL.md +3 -2
  12. package/build/plugins/opencode/skills/keiyaku-workflow/SKILL.md +9 -8
  13. package/build/skills/keiyaku/SKILL.md +3 -1
  14. package/build/skills/keiyaku-babysit/SKILL.md +1 -1
  15. package/build/skills/keiyaku-bind/SKILL.md +3 -2
  16. package/build/skills/keiyaku-workflow/SKILL.md +9 -8
  17. package/build/src/akuma/akuma-errors.d.ts +5 -0
  18. package/build/src/akuma/akuma-errors.js +9 -0
  19. package/build/src/akuma/akuma-owner.d.ts +14 -19
  20. package/build/src/akuma/akuma-owner.js +56 -54
  21. package/build/src/akuma/akuma-selection-execution.d.ts +5 -4
  22. package/build/src/akuma/akuma-selection-execution.js +59 -17
  23. package/build/src/akuma/akuma.d.ts +1 -2
  24. package/build/src/akuma/allowed.d.ts +2 -3
  25. package/build/src/akuma/allowed.js +1 -3
  26. package/build/src/akuma/body.js +17 -7
  27. package/build/src/akuma/call-initial-tell.d.ts +18 -0
  28. package/build/src/akuma/call-initial-tell.js +18 -0
  29. package/build/src/akuma/call-request.d.ts +20 -4
  30. package/build/src/akuma/heart/facts.d.ts +13 -0
  31. package/build/src/akuma/heart/facts.js +6 -0
  32. package/build/src/akuma/heart/index.d.ts +29 -14
  33. package/build/src/akuma/heart/index.js +75 -29
  34. package/build/src/akuma/heart/rows.d.ts +9 -2
  35. package/build/src/akuma/heart/rows.js +72 -11
  36. package/build/src/akuma/heart/storage.d.ts +2 -8
  37. package/build/src/akuma/heart/storage.js +13 -13
  38. package/build/src/akuma/heart/tells.d.ts +3 -1
  39. package/build/src/akuma/heart/tells.js +18 -2
  40. package/build/src/akuma/provider.d.ts +2 -4
  41. package/build/src/akuma/provider.js +29 -6
  42. package/build/src/akuma/providers/acp/core.js +23 -21
  43. package/build/src/akuma/providers/claude/index.js +0 -8
  44. package/build/src/akuma/providers/codex-app-server/index.js +9 -8
  45. package/build/src/akuma/providers/execution-environment.d.ts +1 -1
  46. package/build/src/akuma/providers/execution-environment.js +1 -0
  47. package/build/src/akuma/providers/opencode-sdk/events.js +111 -81
  48. package/build/src/akuma/providers/opencode-sdk/index.js +179 -150
  49. package/build/src/akuma/providers/opencode-sdk/native-http.js +7 -3
  50. package/build/src/akuma/providers/opencode-sdk/session.d.ts +5 -3
  51. package/build/src/akuma/providers/opencode-sdk/session.js +32 -27
  52. package/build/src/akuma/providers/paseo/index.js +0 -2
  53. package/build/src/akuma/providers/pi/index.js +23 -20
  54. package/build/src/akuma/selection-observation.d.ts +78 -9
  55. package/build/src/akuma/selection-owner-port.d.ts +2 -1
  56. package/build/src/akuma/selection-owner-port.js +6 -1
  57. package/build/src/akuma/selection-request.d.ts +1 -1
  58. package/build/src/akuma/selection-request.js +8 -3
  59. package/build/src/akuma/turn-drive.js +79 -48
  60. package/build/src/akuma-body.js +5 -3
  61. package/build/src/cli/commands/akuma-grammar.js +2 -2
  62. package/build/src/cli/commands/akuma.js +24 -16
  63. package/build/src/cli/commands/contract-grammar.d.ts +3 -7
  64. package/build/src/cli/commands/contract-grammar.js +12 -10
  65. package/build/src/cli/commands/contract-help.d.ts +3 -13
  66. package/build/src/cli/commands/contract-help.js +7 -16
  67. package/build/src/cli/commands/contract.js +19 -35
  68. package/build/src/cli/commands/debt.js +30 -29
  69. package/build/src/cli/parse.js +0 -1
  70. package/build/src/cli/render/akuma-activity.d.ts +36 -9
  71. package/build/src/cli/render/akuma-activity.js +142 -65
  72. package/build/src/cli/render/akuma.d.ts +6 -0
  73. package/build/src/cli/render/akuma.js +67 -24
  74. package/build/src/cli/render/catalog.js +0 -1
  75. package/build/src/cli/render/contract-observation.d.ts +16 -3
  76. package/build/src/cli/render/contract-observation.js +3 -0
  77. package/build/src/cli/render/contract.d.ts +2 -2
  78. package/build/src/cli/render/contract.js +4 -4
  79. package/build/src/cli/render/delivery-preview.d.ts +7 -0
  80. package/build/src/cli/render/{audit.js → delivery-preview.js} +29 -27
  81. package/build/src/cli/render/kanshi-akuma.d.ts +3 -1
  82. package/build/src/cli/render/kanshi-akuma.js +23 -8
  83. package/build/src/cli/render/kanshi.js +4 -1
  84. package/build/src/cli/render/receipt.js +3 -3
  85. package/build/src/cli/render/refusal.js +2 -2
  86. package/build/src/cli/render/terminal.d.ts +0 -1
  87. package/build/src/cli/render/terminal.js +1 -1
  88. package/build/src/cli/square-identity.d.ts +15 -0
  89. package/build/src/cli/square-identity.js +180 -0
  90. package/build/src/contract-worktree.js +1 -1
  91. package/build/src/core/verbs/placement.d.ts +2 -1
  92. package/build/src/core/verbs/placement.js +1 -1
  93. package/build/src/debt/context.d.ts +0 -5
  94. package/build/src/debt/context.js +32 -44
  95. package/build/src/git/process.d.ts +1 -0
  96. package/build/src/git/process.js +18 -1
  97. package/build/src/git/read-observation.js +2 -1
  98. package/build/src/git/scratch.d.ts +5 -1
  99. package/build/src/git/scratch.js +41 -12
  100. package/build/src/git/target-placement.d.ts +22 -4
  101. package/build/src/git/target-placement.js +44 -20
  102. package/build/src/index.d.ts +2 -2
  103. package/build/src/kanshi/read.js +187 -105
  104. package/build/src/kanshi/report.d.ts +5 -0
  105. package/build/src/library/address.d.ts +18 -4
  106. package/build/src/library/address.js +27 -6
  107. package/build/src/library/contract-address.d.ts +6 -0
  108. package/build/src/library/contract-address.js +12 -1
  109. package/build/src/library/contract-execution.d.ts +2 -2
  110. package/build/src/library/contract-execution.js +13 -17
  111. package/build/src/library/contract-operations.d.ts +59 -94
  112. package/build/src/library/contract-operations.js +23 -19
  113. package/build/src/library/contract-outcomes.d.ts +3 -3
  114. package/build/src/library/contract-settings.d.ts +1 -0
  115. package/build/src/library/contract-settings.js +1 -1
  116. package/build/src/library/invocation.d.ts +2 -2
  117. package/build/src/library/invocation.js +1 -1
  118. package/build/src/library/keiyaku.d.ts +2 -2
  119. package/build/src/library/keiyaku.js +2 -2
  120. package/build/src/library/outcome.d.ts +148 -268
  121. package/build/src/library/outcome.js +4 -8
  122. package/build/src/library/refusal.d.ts +2 -2
  123. package/build/src/library/refusal.js +2 -2
  124. package/build/src/plugin/runtime.js +15 -19
  125. package/build/src/protocol/{audit.d.ts → delivery-preview.d.ts} +62 -97
  126. package/build/src/protocol/delivery-preview.js +167 -0
  127. package/build/src/protocol/progress.d.ts +1 -1
  128. package/build/src/runtime/proc/run.js +22 -4
  129. package/build/src/runtime/proc/windows-launch.exe +0 -0
  130. package/build/src/verification/execution.d.ts +1 -1
  131. package/build/src/verification/execution.js +5 -3
  132. package/package.json +5 -2
  133. package/skills/keiyaku/SKILL.md +3 -1
  134. package/skills/keiyaku-babysit/SKILL.md +1 -1
  135. package/skills/keiyaku-bind/SKILL.md +3 -2
  136. package/skills/keiyaku-workflow/SKILL.md +9 -8
  137. package/build/src/cli/render/audit.d.ts +0 -7
  138. package/build/src/protocol/audit.js +0 -203
@@ -4,37 +4,53 @@ export function diagnostic(value) {
4
4
  const data = object(value);
5
5
  return text(data?.message) ?? text(object(data?.data)?.message) ?? "OpenCode execution failed";
6
6
  }
7
+ function shellCall(value) {
8
+ return { kind: "run", command: text(value?.command) ?? "shell" };
9
+ }
10
+ function readCall(value) {
11
+ const path = text(value?.filePath) ?? text(value?.path);
12
+ if (path === undefined)
13
+ return undefined;
14
+ return {
15
+ kind: "read",
16
+ path,
17
+ ...(positiveLine(value?.offset) === undefined ? {} : { offset: positiveLine(value?.offset) }),
18
+ ...(positiveLine(value?.limit) === undefined ? {} : { limit: positiveLine(value?.limit) }),
19
+ };
20
+ }
21
+ function searchCall(name, value) {
22
+ const query = text(value?.pattern);
23
+ const path = text(value?.path);
24
+ if (query === undefined)
25
+ return undefined;
26
+ return {
27
+ kind: "search",
28
+ scope: name === "glob" ? "files" : "content",
29
+ query,
30
+ ...(path === undefined ? {} : { path }),
31
+ };
32
+ }
33
+ function editCall(value) {
34
+ const path = text(value?.path);
35
+ if (path !== undefined)
36
+ return { kind: "fileChange", changes: [{ op: "update", path }] };
37
+ return undefined;
38
+ }
7
39
  function callFor(name, input) {
8
40
  const value = object(input);
9
- if (name === "shell")
10
- return { kind: "run", command: text(value?.command) ?? name };
11
- if (name === "read") {
12
- const path = text(value?.filePath) ?? text(value?.path);
13
- if (path !== undefined)
14
- return {
15
- kind: "read",
16
- path,
17
- ...(positiveLine(value?.offset) === undefined ? {} : { offset: positiveLine(value?.offset) }),
18
- ...(positiveLine(value?.limit) === undefined ? {} : { limit: positiveLine(value?.limit) }),
19
- };
20
- }
21
- if (name === "grep" || name === "glob") {
22
- const query = text(value?.pattern);
23
- const path = text(value?.path);
24
- if (query !== undefined)
25
- return {
26
- kind: "search",
27
- scope: name === "glob" ? "files" : "content",
28
- query,
29
- ...(path === undefined ? {} : { path }),
30
- };
31
- }
32
- if (name === "edit") {
33
- const path = text(value?.path);
34
- if (path !== undefined)
35
- return { kind: "fileChange", changes: [{ op: "update", path }] };
41
+ switch (name) {
42
+ case "shell":
43
+ return shellCall(value);
44
+ case "read":
45
+ return readCall(value) ?? otherToolCall(name, input);
46
+ case "grep":
47
+ case "glob":
48
+ return searchCall(name, value) ?? otherToolCall(name, input);
49
+ case "edit":
50
+ return editCall(value) ?? otherToolCall(name, input);
51
+ default:
52
+ return otherToolCall(name, input);
36
53
  }
37
- return otherToolCall(name, input);
38
54
  }
39
55
  function observedChanges(metadata) {
40
56
  const files = object(metadata)?.files;
@@ -101,6 +117,46 @@ const DROPPED = new Set([
101
117
  "session.tool.progress",
102
118
  "session.retry.scheduled",
103
119
  ]);
120
+ function toolResult(type, data) {
121
+ const observed = output(data.content);
122
+ const nativeExit = object(data.metadata)?.exit;
123
+ const exit = typeof nativeExit === "number" && Number.isInteger(nativeExit) ? nativeExit : undefined;
124
+ return {
125
+ status: type === "session.tool.failed" || (exit !== undefined && exit !== 0) ? "error" : "ok",
126
+ ...(type === "session.tool.failed" ? { message: diagnostic(data.error) } : {}),
127
+ ...(observed === undefined ? {} : { output: observed }),
128
+ ...(exit === undefined ? {} : { exitCode: exit }),
129
+ };
130
+ }
131
+ function toolEvent(type, data, events, state) {
132
+ const id = text(data.id);
133
+ const started = id === undefined ? undefined : state.tools.get(id);
134
+ if (id === undefined || started === undefined || state.completed.has(id))
135
+ return;
136
+ if (started.call === undefined) {
137
+ started.call = callFor(started.name, data.input);
138
+ events.emit({ type: "tool", phase: "started", id, name: started.name, call: started.call });
139
+ }
140
+ if (type === "session.tool.called")
141
+ return;
142
+ if (started.name === "edit" || started.name === "patch" || started.name === "write") {
143
+ started.call = observedChanges(data.metadata) ?? started.call;
144
+ }
145
+ const result = toolResult(type, data);
146
+ state.completed.add(id);
147
+ events.emit({ type: "tool", phase: "completed", id, name: started.name, call: started.call, result });
148
+ }
149
+ function stepStarted(data, events) {
150
+ const model = object(data.model);
151
+ const provider = text(model?.providerID);
152
+ const id = text(model?.id);
153
+ if (provider !== undefined && id !== undefined)
154
+ events.emit(modelEvent(`${provider}/${id}`));
155
+ }
156
+ function toolInputStarted(data, state) {
157
+ if (typeof data.id === "string" && typeof data.name === "string")
158
+ state.tools.set(data.id, { name: data.name });
159
+ }
104
160
  /** Only complete native values enter narration. The caller enforces submission/step correlation. */
105
161
  export function mapEvent(value, events, state) {
106
162
  const event = object(value);
@@ -110,62 +166,36 @@ export function mapEvent(value, events, state) {
110
166
  if (state.sessionId !== undefined && data.sessionID !== state.sessionId)
111
167
  return;
112
168
  const type = event.type;
113
- if (type === "session.step.started") {
114
- const model = object(data.model);
115
- const provider = text(model?.providerID);
116
- const id = text(model?.id);
117
- if (provider !== undefined && id !== undefined)
118
- events.emit(modelEvent(`${provider}/${id}`));
119
- return;
120
- }
121
- if (type === "session.text.ended" || type === "session.reasoning.ended") {
122
- if (typeof data.text === "string")
123
- events.emit({ type: type === "session.text.ended" ? "assistant" : "thought", text: data.text });
124
- return;
125
- }
126
- if (type === "session.tool.input.started") {
127
- if (typeof data.id === "string" && typeof data.name === "string")
128
- state.tools.set(data.id, { name: data.name });
129
- return;
130
- }
131
- if (type === "session.tool.called" || type === "session.tool.success" || type === "session.tool.failed") {
132
- const id = text(data.id);
133
- const started = id === undefined ? undefined : state.tools.get(id);
134
- if (id === undefined || started === undefined || state.completed.has(id))
169
+ switch (type) {
170
+ case "session.step.started":
171
+ stepStarted(data, events);
135
172
  return;
136
- if (started.call === undefined) {
137
- started.call = callFor(started.name, data.input);
138
- events.emit({ type: "tool", phase: "started", id, name: started.name, call: started.call });
139
- }
140
- if (type === "session.tool.called")
173
+ case "session.text.ended":
174
+ case "session.reasoning.ended":
175
+ if (typeof data.text === "string")
176
+ events.emit({ type: type === "session.text.ended" ? "assistant" : "thought", text: data.text });
141
177
  return;
142
- if (started.name === "edit" || started.name === "patch" || started.name === "write") {
143
- started.call = observedChanges(data.metadata) ?? started.call;
144
- }
145
- const observed = output(data.content);
146
- const nativeExit = object(data.metadata)?.exit;
147
- const exit = typeof nativeExit === "number" && Number.isInteger(nativeExit) ? nativeExit : undefined;
148
- const result = {
149
- status: type === "session.tool.failed" || (exit !== undefined && exit !== 0) ? "error" : "ok",
150
- ...(type === "session.tool.failed" ? { message: diagnostic(data.error) } : {}),
151
- ...(observed === undefined ? {} : { output: observed }),
152
- ...(exit === undefined ? {} : { exitCode: exit }),
153
- };
154
- state.completed.add(id);
155
- events.emit({ type: "tool", phase: "completed", id, name: started.name, call: started.call, result });
156
- return;
157
- }
158
- if (type === "session.compaction.ended") {
159
- events.emit(noteEvent("Context compacted"));
160
- return;
161
- }
162
- if (type === "session.compaction.failed") {
163
- events.emit(noteEvent(diagnostic(data.error)));
164
- return;
178
+ case "session.tool.input.started":
179
+ toolInputStarted(data, state);
180
+ return;
181
+ case "session.tool.called":
182
+ case "session.tool.success":
183
+ case "session.tool.failed":
184
+ toolEvent(type, data, events, state);
185
+ return;
186
+ case "session.compaction.ended":
187
+ events.emit(noteEvent("Context compacted"));
188
+ return;
189
+ case "session.compaction.failed":
190
+ events.emit(noteEvent(diagnostic(data.error)));
191
+ return;
192
+ case "session.compaction.started":
193
+ case "session.compaction.delta":
194
+ return;
195
+ default:
196
+ if (!DROPPED.has(type))
197
+ events.emit({ type: "unknown", kind: type });
165
198
  }
166
- if (type === "session.compaction.started" || type === "session.compaction.delta" || DROPPED.has(type))
167
- return;
168
- events.emit({ type: "unknown", kind: type });
169
199
  }
170
200
  export function createEventState(sessionId) {
171
201
  return { ...(sessionId === undefined ? {} : { sessionId }), tools: new Map(), completed: new Set() };
@@ -34,8 +34,16 @@ function ownRuntime(custody, runtime) {
34
34
  }
35
35
  return closing;
36
36
  };
37
- custody.own({ closed, abort: close, forceDispose: close });
38
- return close;
37
+ let abort = close;
38
+ let forceDispose = close;
39
+ custody.own({ closed, abort: () => abort(), forceDispose: () => forceDispose() });
40
+ return {
41
+ close,
42
+ setControls(controls) {
43
+ abort = controls.abort;
44
+ forceDispose = controls.forceDispose;
45
+ },
46
+ };
39
47
  }
40
48
  function liveAdmission(initial) {
41
49
  let latest = initial;
@@ -88,10 +96,52 @@ async function answer(runtime, id, assistantID, terminalID) {
88
96
  }
89
97
  return { kind: "answered", answer: text, historyId: boundary };
90
98
  }
99
+ function executionResult(input, state, event, data) {
100
+ const type = event?.type;
101
+ if (type === "session.step.failed" || type === "session.execution.failed") {
102
+ return { kind: "failed", diagnostic: diagnostic(data.error) };
103
+ }
104
+ if (type === "session.execution.interrupted")
105
+ return { kind: "failed", diagnostic: "OpenCode native execution interrupted" };
106
+ if (type !== "session.execution.succeeded" || state.delivered === undefined)
107
+ return;
108
+ const step = state.step;
109
+ if (step === undefined || !step.ended || step.input !== input.admission.latest()) {
110
+ return { kind: "failed", diagnostic: "OpenCode completed without an assistant for the admitted input" };
111
+ }
112
+ const terminalID = nonblank(event?.id);
113
+ if (terminalID === undefined || !terminalID.startsWith("evt_"))
114
+ return { kind: "failed", diagnostic: "OpenCode terminal identity missing" };
115
+ return answer(input.runtime, input.id, step.id, terminalID);
116
+ }
117
+ function observeEvent(input, state, event, data) {
118
+ const type = event?.type;
119
+ if (type === "session.inbox.enqueued" && typeof data.inboxID === "string" && object(data.item)?.type === "user") {
120
+ input.admission.admit(data.inboxID);
121
+ }
122
+ if (type === "session.inbox.delivered" && typeof data.inboxID === "string") {
123
+ if (!input.admission.owns(data.inboxID)) {
124
+ return {
125
+ kind: "failed",
126
+ diagnostic: "OpenCode consumed another writer's input; exact answer attribution unavailable",
127
+ };
128
+ }
129
+ input.admission.admit(data.inboxID);
130
+ state.delivered = data.inboxID;
131
+ }
132
+ if (type === "session.step.started" && state.delivered !== undefined && typeof data.assistantMessageID === "string") {
133
+ state.step = { id: data.assistantMessageID, input: state.delivered, ended: false };
134
+ }
135
+ if (data.assistantMessageID !== undefined && data.assistantMessageID !== state.step?.id)
136
+ return;
137
+ if (state.delivered !== undefined)
138
+ mapEvent(event, input.events, state.narration);
139
+ if (type === "session.step.ended" && state.step !== undefined)
140
+ state.step.ended = true;
141
+ return executionResult(input, state, event, data);
142
+ }
91
143
  async function observe(input) {
92
- const state = createEventState(input.id);
93
- let delivered;
94
- let step;
144
+ const state = { narration: createEventState(input.id), delivered: undefined, step: undefined };
95
145
  try {
96
146
  for (;;) {
97
147
  const next = await input.stream.next();
@@ -101,43 +151,9 @@ async function observe(input) {
101
151
  const data = object(event?.data);
102
152
  if (data?.sessionID !== input.id)
103
153
  continue;
104
- const type = event?.type;
105
- if (type === "session.inbox.enqueued" && typeof data.inboxID === "string" && object(data.item)?.type === "user") {
106
- input.admission.admit(data.inboxID);
107
- }
108
- if (type === "session.inbox.delivered" && typeof data.inboxID === "string") {
109
- if (!input.admission.owns(data.inboxID)) {
110
- return {
111
- kind: "failed",
112
- diagnostic: "OpenCode consumed another writer's input; exact answer attribution unavailable",
113
- };
114
- }
115
- input.admission.admit(data.inboxID);
116
- delivered = data.inboxID;
117
- }
118
- if (type === "session.step.started" && delivered !== undefined && typeof data.assistantMessageID === "string") {
119
- step = { id: data.assistantMessageID, input: delivered, ended: false };
120
- }
121
- if (data.assistantMessageID !== undefined && data.assistantMessageID !== step?.id)
122
- continue;
123
- if (delivered !== undefined)
124
- mapEvent(event, input.events, state);
125
- if (type === "session.step.ended" && step !== undefined)
126
- step.ended = true;
127
- if (type === "session.step.failed" || type === "session.execution.failed") {
128
- return { kind: "failed", diagnostic: diagnostic(data.error) };
129
- }
130
- if (type === "session.execution.interrupted")
131
- return { kind: "failed", diagnostic: "OpenCode native execution interrupted" };
132
- if (type !== "session.execution.succeeded" || delivered === undefined)
133
- continue;
134
- if (step === undefined || !step.ended || step.input !== input.admission.latest()) {
135
- return { kind: "failed", diagnostic: "OpenCode completed without an assistant for the admitted input" };
136
- }
137
- const terminalID = nonblank(event?.id);
138
- if (terminalID === undefined || !terminalID.startsWith("evt_"))
139
- return { kind: "failed", diagnostic: "OpenCode terminal identity missing" };
140
- return await answer(input.runtime, input.id, step.id, terminalID);
154
+ const result = observeEvent(input, state, event, data);
155
+ if (result !== undefined)
156
+ return await result;
141
157
  }
142
158
  }
143
159
  catch (error) {
@@ -147,6 +163,25 @@ async function observe(input) {
147
163
  input.admission.end();
148
164
  }
149
165
  }
166
+ async function configureSession(runtime, input, id, info, model) {
167
+ const session = runtime.client.session;
168
+ let selected = object(info?.model);
169
+ if (input.session.kind === "resume" && model !== undefined) {
170
+ await session.switchModel({ id, model });
171
+ selected = object(object(object(await session.get({ id }))?.data)?.model);
172
+ }
173
+ if (model !== undefined &&
174
+ (selected?.providerID !== model.providerID ||
175
+ selected.id !== model.id ||
176
+ (selected.variant ?? "default") !== (input.options.effort ?? "default"))) {
177
+ throw new Error("OpenCode did not select the requested model");
178
+ }
179
+ if (input.options.systemPrompt !== undefined) {
180
+ // Native instruction entries join the system transcript and survive resume
181
+ // and fork. Updating the same entry announces an upstream instruction change.
182
+ await session.instructions({ id, text: input.options.systemPrompt });
183
+ }
184
+ }
150
185
  async function openSession(runtime, input) {
151
186
  const model = input.options.model === undefined
152
187
  ? undefined
@@ -169,38 +204,18 @@ async function openSession(runtime, input) {
169
204
  throw new Error("OpenCode active execution response missing");
170
205
  if (active[id] !== undefined)
171
206
  throw new Error("OpenCode native session is already busy");
172
- let selected = object(info?.model);
173
- if (input.session.kind === "resume" && model !== undefined) {
174
- await session.switchModel({ id, model });
175
- selected = object(object(object(await session.get({ id }))?.data)?.model);
176
- }
177
- if (model !== undefined &&
178
- (selected?.providerID !== model.providerID ||
179
- selected.id !== model.id ||
180
- (selected.variant ?? "default") !== (input.options.effort ?? "default"))) {
181
- throw new Error("OpenCode did not select the requested model");
182
- }
183
- if (input.options.systemPrompt !== undefined) {
184
- // Native instruction entries join the system transcript and survive resume
185
- // and fork. Updating the same entry announces an upstream instruction change.
186
- await session.instructions({ id, text: input.options.systemPrompt });
187
- }
207
+ await configureSession(runtime, input, id, info, model);
188
208
  return id;
189
209
  }
190
- async function drive(execution, input, loader, custody) {
191
- admit(input.options);
192
- if (input.schemaJson !== undefined)
193
- throw new Error("OpenCode v2 does not expose native structured answer schemas");
194
- if (input.session.kind === "resume")
195
- sessionId(input.session.coordinate);
196
- let closeRuntime;
210
+ async function openResources(execution, input, loader, custody) {
211
+ let runtimeOwner;
197
212
  const runtime = await loadOpencode({
198
213
  execution,
199
214
  cwd: input.cwd,
200
215
  signal: custody.signal,
201
216
  ...(loader === undefined ? {} : { loader }),
202
217
  onRuntime: (ready) => {
203
- closeRuntime = ownRuntime(custody, ready);
218
+ runtimeOwner = ownRuntime(custody, ready);
204
219
  },
205
220
  ...(input.requests === undefined ? {} : { requests: input.requests.dir }),
206
221
  });
@@ -211,91 +226,105 @@ async function drive(execution, input, loader, custody) {
211
226
  subscribe: runtime.client.event.subscribe,
212
227
  });
213
228
  let closing;
214
- const close = () => (closing ??= Promise.all([stream.retire(), closeRuntime()]).then(() => undefined));
215
- try {
216
- const id = await openSession(runtime, input);
217
- await stream.ready;
218
- custody.signal.throwIfAborted();
219
- const events = new AgentEventChannel();
220
- events.emit({ type: "session", coordinate: coordinate(id) });
221
- let sequence = 0;
222
- const initial = messageId(sequence++);
223
- const admission = liveAdmission(initial);
224
- let control;
225
- let finishing = false;
226
- let finish;
227
- const completion = new Promise((resolve) => {
228
- let settlement;
229
- finish = (result) => {
230
- finishing = true;
231
- return (settlement ??= (async () => {
232
- await close();
233
- events.end();
234
- resolve(result);
235
- })());
236
- };
237
- });
238
- const observed = observe({ runtime, stream, id, admission, events });
239
- // Start observation before submission: admission broadcasts can precede HTTP completion.
240
- void observed
241
- .then(async (result) => {
242
- // Explicit interrupt owns its response too: a terminal broadcast can be
243
- // published before that response, and must not close its connection early.
244
- if (control !== undefined)
245
- await control;
246
- await finish(result);
247
- })
248
- .catch(() => undefined);
249
- const prompt = async (nativeID, text) => {
250
- const response = object(object(await runtime.client.session.prompt({ id, body: { id: nativeID, text } }))?.data);
251
- if (response?.id !== nativeID || response.sessionID !== id || response.type !== "user") {
252
- throw new Error("OpenCode returned no exact native inbox admission");
253
- }
229
+ const close = () => (closing ??= Promise.all([stream.retire(), runtimeOwner.close()]).then(() => undefined));
230
+ return { runtime, stream, runtimeOwner, close };
231
+ }
232
+ async function submitPrompt(runtime, id, nativeID, text) {
233
+ const response = object(object(await runtime.client.session.prompt({ id, body: { id: nativeID, text } }))?.data);
234
+ if (response?.id !== nativeID || response.sessionID !== id || response.type !== "user") {
235
+ throw new Error("OpenCode returned no exact native inbox admission");
236
+ }
237
+ }
238
+ async function openTurn(input, signal, resources, id) {
239
+ const { runtime, stream, runtimeOwner, close } = resources;
240
+ const events = new AgentEventChannel();
241
+ events.emit({ type: "session", coordinate: coordinate(id) });
242
+ const initial = messageId();
243
+ const admission = liveAdmission(initial);
244
+ let control;
245
+ let finishing = false;
246
+ let finish;
247
+ const completion = new Promise((resolve) => {
248
+ let settlement;
249
+ finish = (result) => {
250
+ finishing = true;
251
+ return (settlement ??= (async () => {
252
+ await close();
253
+ events.end();
254
+ resolve(result);
255
+ })());
254
256
  };
255
- await prompt(initial, launchPrompt(input.body, input.launchTells));
257
+ });
258
+ const observed = observe({ runtime, stream, id, admission, events });
259
+ // Start observation before submission: admission broadcasts can precede HTTP completion.
260
+ void observed
261
+ .then(async (result) => {
262
+ // Explicit interrupt owns its response too: a terminal broadcast can be
263
+ // published before that response, and must not close its connection early.
264
+ if (control !== undefined)
265
+ await control;
266
+ await finish(result);
267
+ })
268
+ .catch(() => undefined);
269
+ const prompt = (nativeID, text) => submitPrompt(runtime, id, nativeID, text);
270
+ await prompt(initial, launchPrompt(input.body, input.launchTells));
271
+ signal.throwIfAborted();
272
+ let submissions = admission.initial.then(() => undefined);
273
+ const abort = async () => {
274
+ admission.end();
275
+ if (finishing) {
276
+ await close();
277
+ return;
278
+ }
279
+ // The v2 interrupt response follows execution settlement. Do not hide its
280
+ // refusal; server and connection retirement must still run on failure.
281
+ control ??= runtime.client.session.interrupt({ id });
282
+ try {
283
+ await control;
284
+ }
285
+ finally {
286
+ await finish({ kind: "failed", diagnostic: "OpenCode session interrupted" });
287
+ }
288
+ };
289
+ const forceDispose = async () => {
290
+ admission.end();
291
+ await finish({ kind: "failed", diagnostic: "OpenCode session force-disposed" });
292
+ };
293
+ runtimeOwner.setControls({ abort, forceDispose });
294
+ return {
295
+ admission: { fence: initial },
296
+ events,
297
+ completion,
298
+ tell(tell) {
299
+ const submit = async () => {
300
+ if (!admission.open())
301
+ return { kind: "turn-ended" };
302
+ const nativeID = messageId();
303
+ const receipt = admission.wait(nativeID);
304
+ void prompt(nativeID, tell.text).catch((error) => admission.fail(nativeID, error));
305
+ return await receipt;
306
+ };
307
+ const next = submissions.then(submit);
308
+ submissions = next.then(() => undefined, () => undefined);
309
+ return next;
310
+ },
311
+ };
312
+ }
313
+ async function drive(execution, input, loader, custody) {
314
+ admit(input.options);
315
+ if (input.schemaJson !== undefined)
316
+ throw new Error("OpenCode v2 does not expose native structured answer schemas");
317
+ if (input.session.kind === "resume")
318
+ sessionId(input.session.coordinate);
319
+ const resources = await openResources(execution, input, loader, custody);
320
+ try {
321
+ const id = await openSession(resources.runtime, input);
322
+ await resources.stream.ready;
256
323
  custody.signal.throwIfAborted();
257
- let submissions = admission.initial.then(() => undefined);
258
- return {
259
- admission: { fence: initial },
260
- events,
261
- completion,
262
- tell(tell) {
263
- const submit = async () => {
264
- if (!admission.open())
265
- return { kind: "turn-ended" };
266
- const nativeID = messageId(sequence++);
267
- const receipt = admission.wait(nativeID);
268
- void prompt(nativeID, tell.text).catch((error) => admission.fail(nativeID, error));
269
- return await receipt;
270
- };
271
- const next = submissions.then(submit);
272
- submissions = next.then(() => undefined, () => undefined);
273
- return next;
274
- },
275
- abort: async () => {
276
- admission.end();
277
- if (finishing) {
278
- await close();
279
- return;
280
- }
281
- // The v2 interrupt response follows execution settlement. Do not hide its
282
- // refusal; server and connection retirement must still run on failure.
283
- control ??= runtime.client.session.interrupt({ id });
284
- try {
285
- await control;
286
- }
287
- finally {
288
- await finish({ kind: "failed", diagnostic: "OpenCode session interrupted" });
289
- }
290
- },
291
- forceDispose: async () => {
292
- admission.end();
293
- await finish({ kind: "failed", diagnostic: "OpenCode session force-disposed" });
294
- },
295
- };
324
+ return await openTurn(input, custody.signal, resources, id);
296
325
  }
297
326
  catch (error) {
298
- await close();
327
+ await resources.close();
299
328
  throw error;
300
329
  }
301
330
  }
@@ -315,14 +344,14 @@ export function createOpencodeProvider(input = { name: OPENCODE_SDK_PROVIDER, ki
315
344
  ...providerLifecycle((input, custody) => drive(execution, input, loader, custody)),
316
345
  fork: (input) => createProviderAttempt(new AbortController().signal, async (custody) => {
317
346
  const id = sessionId(input.session);
318
- let close;
347
+ let runtimeOwner;
319
348
  const runtime = await loadOpencode({
320
349
  execution,
321
350
  cwd: input.cwd,
322
351
  signal: custody.signal,
323
352
  ...(loader === undefined ? {} : { loader }),
324
353
  onRuntime: (ready) => {
325
- close = ownRuntime(custody, ready);
354
+ runtimeOwner = ownRuntime(custody, ready);
326
355
  },
327
356
  });
328
357
  try {
@@ -345,7 +374,7 @@ export function createOpencodeProvider(input = { name: OPENCODE_SDK_PROVIDER, ki
345
374
  return { session: coordinate(child) };
346
375
  }
347
376
  finally {
348
- await close();
377
+ await runtimeOwner.close();
349
378
  }
350
379
  }),
351
380
  };
@@ -1,11 +1,15 @@
1
1
  import { OpenCode } from "@opencode/client";
2
- import { Agent } from "undici";
2
+ import { Agent, fetch as undiciFetch } from "undici";
3
3
  import { object } from "../decode.js";
4
+ // A dispatcher is only understood by the fetch built for the same Undici, and
5
+ // Node's global fetch carries its own copy. Take both halves from the declared
6
+ // dependency so the generated SDK cannot hand this Agent to a foreign Undici.
7
+ const matchedFetch = undiciFetch;
4
8
  /** The matching native SDK owns the dialect; this attempt owns its HTTP connections. */
5
9
  export function createNativeClient(baseUrl, password) {
6
10
  const dispatcher = new Agent();
7
11
  const controller = new AbortController();
8
- // Node's fetch accepts an Undici dispatcher in addition to standard RequestInit.
12
+ // Undici's fetch accepts a dispatcher in addition to standard RequestInit.
9
13
  // Keep that native extension at this one SDK seam.
10
14
  const ownedFetch = (input, init) => {
11
15
  controller.signal.throwIfAborted();
@@ -14,7 +18,7 @@ export function createNativeClient(baseUrl, password) {
14
18
  dispatcher,
15
19
  signal: init?.signal == null ? controller.signal : AbortSignal.any([controller.signal, init.signal]),
16
20
  };
17
- return globalThis.fetch(input, options);
21
+ return matchedFetch(input, options);
18
22
  };
19
23
  const sdk = OpenCode.make({
20
24
  baseUrl,