@tanstack/ai-opencode 0.2.2 → 0.2.4

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.
@@ -1,305 +1,320 @@
1
1
  import { EventType, buildBaseUsage } from "@tanstack/ai";
2
- const SESSION_ID_EVENT = "opencode.session-id";
3
- const TODO_EVENT = "opencode.todo";
4
- const BRIDGED_MCP_SERVER_NAME = "tanstack";
2
+ //#region src/stream/translate.ts
3
+ /** Name of the CUSTOM event carrying the OpenCode session id. */
4
+ var SESSION_ID_EVENT = "opencode.session-id";
5
+ /** Name of the CUSTOM event carrying the harness's todo list updates. */
6
+ var TODO_EVENT = "opencode.todo";
7
+ /** Server name used for bridged TanStack tools. */
8
+ var BRIDGED_MCP_SERVER_NAME = "tanstack";
9
+ /**
10
+ * Resolve the AG-UI tool-call name for an OpenCode tool part. OpenCode names
11
+ * MCP tools `<server>_<tool>`, so bridged TanStack tools arrive as
12
+ * `tanstack_<tool>` and are surfaced under the names the application
13
+ * registered; everything else (built-in `read`, `edit`, `bash`, ... and
14
+ * foreign MCP tools) uses the harness tool name verbatim.
15
+ */
5
16
  function resolveToolName(tool, bridgedToolNames) {
6
- if (!bridgedToolNames || bridgedToolNames.size === 0) return tool;
7
- if (bridgedToolNames.has(tool)) return tool;
8
- if (tool.startsWith("tanstack_") && bridgedToolNames.has(tool.slice(9))) {
9
- return tool.slice(9);
10
- }
11
- return tool;
17
+ if (!bridgedToolNames || bridgedToolNames.size === 0) return tool;
18
+ if (bridgedToolNames.has(tool)) return tool;
19
+ if (tool.startsWith("tanstack_") && bridgedToolNames.has(tool.slice(9))) return tool.slice(9);
20
+ return tool;
12
21
  }
13
22
  function buildUsage(tokens) {
14
- if (!tokens) return void 0;
15
- const promptTokens = tokens.input ?? 0;
16
- const completionTokens = tokens.output ?? 0;
17
- const result = buildBaseUsage({
18
- promptTokens,
19
- completionTokens,
20
- totalTokens: promptTokens + completionTokens
21
- });
22
- if (tokens.cache?.read) {
23
- result.promptTokensDetails = { cachedTokens: tokens.cache.read };
24
- }
25
- if (tokens.reasoning) {
26
- result.completionTokensDetails = { reasoningTokens: tokens.reasoning };
27
- }
28
- return result;
23
+ if (!tokens) return void 0;
24
+ const promptTokens = tokens.input ?? 0;
25
+ const completionTokens = tokens.output ?? 0;
26
+ const result = buildBaseUsage({
27
+ promptTokens,
28
+ completionTokens,
29
+ totalTokens: promptTokens + completionTokens
30
+ });
31
+ if (tokens.cache?.read) result.promptTokensDetails = { cachedTokens: tokens.cache.read };
32
+ if (tokens.reasoning) result.completionTokensDetails = { reasoningTokens: tokens.reasoning };
33
+ return result;
29
34
  }
30
- const isTextPart = (part) => part.type === "text";
31
- const isReasoningPart = (part) => part.type === "reasoning";
32
- const isToolPart = (part) => part.type === "tool";
35
+ var isTextPart = (part) => part.type === "text";
36
+ var isReasoningPart = (part) => part.type === "reasoning";
37
+ var isToolPart = (part) => part.type === "tool";
33
38
  function messageError(message) {
34
- if (!message.error) return void 0;
35
- return { message: message.error.data?.message ?? message.error.name };
39
+ if (!message.error) return void 0;
40
+ return { message: message.error.data?.message ?? message.error.name };
36
41
  }
42
+ /**
43
+ * Translate an OpenCode event stream into AG-UI StreamChunk events.
44
+ *
45
+ * The harness runs its own agent loop and executes its own tools, so the
46
+ * translation always ends with `finishReason: 'stop'` (or `'length'` /
47
+ * RUN_ERROR) — never `'tool_calls'`. Harness tool activity is emitted as
48
+ * already-resolved TOOL_CALL_START/ARGS/END + TOOL_CALL_RESULT sequences so
49
+ * UIs can render it, while the TanStack engine never tries to execute them.
50
+ *
51
+ * OpenCode delivers true token-level deltas for both assistant text and
52
+ * reasoning via `message.part.updated` events (a `delta` string when
53
+ * incremental, otherwise the full part text, from which the delta is
54
+ * derived). The final assistant message — finish reason, token usage, and any
55
+ * fatal error — arrives as the terminal `done` event.
56
+ *
57
+ * Invariant: every TOOL_CALL_START is eventually paired with a
58
+ * TOOL_CALL_RESULT (synthesized as `{"status":"interrupted"}` when the run
59
+ * ends or aborts before the harness reported one) so the engine's
60
+ * pending-tool-call scan on the next request never force-executes them.
61
+ */
37
62
  async function* translateOpencodeStream(events, ctx) {
38
- const { model, runId, threadId, genId } = ctx;
39
- const now = () => Date.now();
40
- let runStarted = false;
41
- const unresolvedToolCalls = /* @__PURE__ */ new Set();
42
- const openedToolCalls = /* @__PURE__ */ new Set();
43
- const resolvedToolCalls = /* @__PURE__ */ new Set();
44
- const textAccumulators = /* @__PURE__ */ new Map();
45
- let openTextId = null;
46
- let openReasoningId = null;
47
- function* startRun() {
48
- if (runStarted) return;
49
- runStarted = true;
50
- yield {
51
- type: EventType.RUN_STARTED,
52
- runId,
53
- threadId,
54
- model,
55
- timestamp: now(),
56
- ...ctx.parentRunId !== void 0 && { parentRunId: ctx.parentRunId }
57
- };
58
- }
59
- function* closeText() {
60
- if (openTextId !== null) {
61
- yield {
62
- type: EventType.TEXT_MESSAGE_END,
63
- messageId: openTextId,
64
- model,
65
- timestamp: now()
66
- };
67
- openTextId = null;
68
- }
69
- }
70
- function* closeReasoning() {
71
- if (openReasoningId !== null) {
72
- yield {
73
- type: EventType.REASONING_MESSAGE_END,
74
- messageId: openReasoningId,
75
- model,
76
- timestamp: now()
77
- };
78
- yield {
79
- type: EventType.REASONING_END,
80
- messageId: openReasoningId,
81
- model,
82
- timestamp: now()
83
- };
84
- openReasoningId = null;
85
- }
86
- }
87
- function* synthesizeUnresolvedResults() {
88
- for (const toolCallId of unresolvedToolCalls) {
89
- yield {
90
- type: EventType.TOOL_CALL_RESULT,
91
- toolCallId,
92
- messageId: genId(),
93
- model,
94
- timestamp: now(),
95
- content: JSON.stringify({ status: "interrupted" })
96
- };
97
- }
98
- unresolvedToolCalls.clear();
99
- }
100
- function* handleTextPart(part, delta) {
101
- yield* closeReasoning();
102
- const prev = textAccumulators.get(part.id) ?? "";
103
- let deltaText;
104
- if (typeof delta === "string" && delta !== "") {
105
- deltaText = delta;
106
- textAccumulators.set(part.id, prev + delta);
107
- } else {
108
- const full = part.text;
109
- deltaText = full.startsWith(prev) ? full.slice(prev.length) : full;
110
- textAccumulators.set(part.id, full);
111
- }
112
- if (deltaText === "") return;
113
- if (openTextId !== part.id) {
114
- yield* closeText();
115
- openTextId = part.id;
116
- yield {
117
- type: EventType.TEXT_MESSAGE_START,
118
- messageId: part.id,
119
- model,
120
- timestamp: now(),
121
- role: "assistant"
122
- };
123
- }
124
- yield {
125
- type: EventType.TEXT_MESSAGE_CONTENT,
126
- messageId: part.id,
127
- model,
128
- timestamp: now(),
129
- delta: deltaText,
130
- content: textAccumulators.get(part.id) ?? deltaText
131
- };
132
- }
133
- function* handleReasoningPart(part, delta) {
134
- yield* closeText();
135
- const prev = textAccumulators.get(part.id) ?? "";
136
- let deltaText;
137
- if (typeof delta === "string" && delta !== "") {
138
- deltaText = delta;
139
- textAccumulators.set(part.id, prev + delta);
140
- } else {
141
- const full = part.text;
142
- deltaText = full.startsWith(prev) ? full.slice(prev.length) : full;
143
- textAccumulators.set(part.id, full);
144
- }
145
- if (deltaText === "") return;
146
- if (openReasoningId !== part.id) {
147
- yield* closeReasoning();
148
- openReasoningId = part.id;
149
- yield {
150
- type: EventType.REASONING_START,
151
- messageId: part.id,
152
- model,
153
- timestamp: now()
154
- };
155
- yield {
156
- type: EventType.REASONING_MESSAGE_START,
157
- messageId: part.id,
158
- role: "reasoning",
159
- model,
160
- timestamp: now()
161
- };
162
- }
163
- yield {
164
- type: EventType.REASONING_MESSAGE_CONTENT,
165
- messageId: part.id,
166
- delta: deltaText,
167
- model,
168
- timestamp: now()
169
- };
170
- }
171
- function* openToolCall(part) {
172
- if (openedToolCalls.has(part.callID)) return;
173
- openedToolCalls.add(part.callID);
174
- const toolCallName = resolveToolName(part.tool, ctx.bridgedToolNames);
175
- const input = part.state.input ?? {};
176
- const args = JSON.stringify(input);
177
- yield {
178
- type: EventType.TOOL_CALL_START,
179
- toolCallId: part.callID,
180
- toolCallName,
181
- toolName: toolCallName,
182
- model,
183
- timestamp: now()
184
- };
185
- yield {
186
- type: EventType.TOOL_CALL_ARGS,
187
- toolCallId: part.callID,
188
- model,
189
- timestamp: now(),
190
- delta: args,
191
- args
192
- };
193
- yield {
194
- type: EventType.TOOL_CALL_END,
195
- toolCallId: part.callID,
196
- toolCallName,
197
- toolName: toolCallName,
198
- model,
199
- timestamp: now(),
200
- input
201
- };
202
- unresolvedToolCalls.add(part.callID);
203
- }
204
- function* handleToolPart(part) {
205
- yield* closeText();
206
- yield* closeReasoning();
207
- yield* openToolCall(part);
208
- const state = part.state;
209
- if (state.status !== "completed" && state.status !== "error") return;
210
- if (resolvedToolCalls.has(part.callID)) return;
211
- resolvedToolCalls.add(part.callID);
212
- unresolvedToolCalls.delete(part.callID);
213
- const isError = state.status === "error";
214
- yield {
215
- type: EventType.TOOL_CALL_RESULT,
216
- toolCallId: part.callID,
217
- messageId: genId(),
218
- model,
219
- timestamp: now(),
220
- content: isError ? state.error : state.output,
221
- ...isError && { state: "output-error" }
222
- };
223
- }
224
- function* handleEvent(event) {
225
- if (event.type === "message.part.updated") {
226
- const { part, delta } = event.properties;
227
- if (isTextPart(part)) {
228
- yield* handleTextPart(part, delta);
229
- } else if (isReasoningPart(part)) {
230
- yield* handleReasoningPart(part, delta);
231
- } else if (isToolPart(part)) {
232
- yield* handleToolPart(part);
233
- }
234
- } else if (event.type === "todo.updated") {
235
- yield {
236
- type: EventType.CUSTOM,
237
- model,
238
- timestamp: now(),
239
- name: TODO_EVENT,
240
- value: { todos: event.properties.todos }
241
- };
242
- }
243
- }
244
- function* finish(message) {
245
- yield* startRun();
246
- yield* closeText();
247
- yield* closeReasoning();
248
- yield* synthesizeUnresolvedResults();
249
- const error = messageError(message);
250
- if (error) {
251
- yield {
252
- type: EventType.RUN_ERROR,
253
- model,
254
- timestamp: now(),
255
- message: error.message,
256
- error
257
- };
258
- return;
259
- }
260
- const usage = buildUsage(message.tokens);
261
- const finishReason = message.finish === "length" ? "length" : "stop";
262
- yield {
263
- type: EventType.RUN_FINISHED,
264
- runId,
265
- threadId,
266
- model,
267
- timestamp: now(),
268
- finishReason,
269
- ...usage !== void 0 && { usage }
270
- };
271
- }
272
- try {
273
- for await (const streamEvent of events) {
274
- ctx.onStreamEvent?.(streamEvent);
275
- if (streamEvent.kind === "session") {
276
- yield* startRun();
277
- yield {
278
- type: EventType.CUSTOM,
279
- model,
280
- timestamp: now(),
281
- name: SESSION_ID_EVENT,
282
- value: { sessionId: streamEvent.sessionId }
283
- };
284
- } else if (streamEvent.kind === "event") {
285
- yield* startRun();
286
- yield* handleEvent(streamEvent.event);
287
- } else {
288
- yield* finish(streamEvent.message);
289
- }
290
- }
291
- } catch (error) {
292
- yield* closeText();
293
- yield* closeReasoning();
294
- yield* synthesizeUnresolvedResults();
295
- throw error;
296
- }
63
+ const { model, runId, threadId, genId } = ctx;
64
+ const now = () => Date.now();
65
+ let runStarted = false;
66
+ /** Tool calls started but with no result yet, keyed by callID. */
67
+ const unresolvedToolCalls = /* @__PURE__ */ new Set();
68
+ /** Tool call ids that already emitted TOOL_CALL_START/ARGS/END. */
69
+ const openedToolCalls = /* @__PURE__ */ new Set();
70
+ /** Tool call ids that already emitted a TOOL_CALL_RESULT. */
71
+ const resolvedToolCalls = /* @__PURE__ */ new Set();
72
+ /** Accumulated text per text-part id, for delta derivation. */
73
+ const textAccumulators = /* @__PURE__ */ new Map();
74
+ let openTextId = null;
75
+ let openReasoningId = null;
76
+ function* startRun() {
77
+ if (runStarted) return;
78
+ runStarted = true;
79
+ yield {
80
+ type: EventType.RUN_STARTED,
81
+ runId,
82
+ threadId,
83
+ model,
84
+ timestamp: now(),
85
+ ...ctx.parentRunId !== void 0 && { parentRunId: ctx.parentRunId }
86
+ };
87
+ }
88
+ function* closeText() {
89
+ if (openTextId !== null) {
90
+ yield {
91
+ type: EventType.TEXT_MESSAGE_END,
92
+ messageId: openTextId,
93
+ model,
94
+ timestamp: now()
95
+ };
96
+ openTextId = null;
97
+ }
98
+ }
99
+ function* closeReasoning() {
100
+ if (openReasoningId !== null) {
101
+ yield {
102
+ type: EventType.REASONING_MESSAGE_END,
103
+ messageId: openReasoningId,
104
+ model,
105
+ timestamp: now()
106
+ };
107
+ yield {
108
+ type: EventType.REASONING_END,
109
+ messageId: openReasoningId,
110
+ model,
111
+ timestamp: now()
112
+ };
113
+ openReasoningId = null;
114
+ }
115
+ }
116
+ function* synthesizeUnresolvedResults() {
117
+ for (const toolCallId of unresolvedToolCalls) yield {
118
+ type: EventType.TOOL_CALL_RESULT,
119
+ toolCallId,
120
+ messageId: genId(),
121
+ model,
122
+ timestamp: now(),
123
+ content: JSON.stringify({ status: "interrupted" })
124
+ };
125
+ unresolvedToolCalls.clear();
126
+ }
127
+ function* handleTextPart(part, delta) {
128
+ yield* closeReasoning();
129
+ const prev = textAccumulators.get(part.id) ?? "";
130
+ let deltaText;
131
+ if (typeof delta === "string" && delta !== "") {
132
+ deltaText = delta;
133
+ textAccumulators.set(part.id, prev + delta);
134
+ } else {
135
+ const full = part.text;
136
+ deltaText = full.startsWith(prev) ? full.slice(prev.length) : full;
137
+ textAccumulators.set(part.id, full);
138
+ }
139
+ if (deltaText === "") return;
140
+ if (openTextId !== part.id) {
141
+ yield* closeText();
142
+ openTextId = part.id;
143
+ yield {
144
+ type: EventType.TEXT_MESSAGE_START,
145
+ messageId: part.id,
146
+ model,
147
+ timestamp: now(),
148
+ role: "assistant"
149
+ };
150
+ }
151
+ yield {
152
+ type: EventType.TEXT_MESSAGE_CONTENT,
153
+ messageId: part.id,
154
+ model,
155
+ timestamp: now(),
156
+ delta: deltaText,
157
+ content: textAccumulators.get(part.id) ?? deltaText
158
+ };
159
+ }
160
+ function* handleReasoningPart(part, delta) {
161
+ yield* closeText();
162
+ const prev = textAccumulators.get(part.id) ?? "";
163
+ let deltaText;
164
+ if (typeof delta === "string" && delta !== "") {
165
+ deltaText = delta;
166
+ textAccumulators.set(part.id, prev + delta);
167
+ } else {
168
+ const full = part.text;
169
+ deltaText = full.startsWith(prev) ? full.slice(prev.length) : full;
170
+ textAccumulators.set(part.id, full);
171
+ }
172
+ if (deltaText === "") return;
173
+ if (openReasoningId !== part.id) {
174
+ yield* closeReasoning();
175
+ openReasoningId = part.id;
176
+ yield {
177
+ type: EventType.REASONING_START,
178
+ messageId: part.id,
179
+ model,
180
+ timestamp: now()
181
+ };
182
+ yield {
183
+ type: EventType.REASONING_MESSAGE_START,
184
+ messageId: part.id,
185
+ role: "reasoning",
186
+ model,
187
+ timestamp: now()
188
+ };
189
+ }
190
+ yield {
191
+ type: EventType.REASONING_MESSAGE_CONTENT,
192
+ messageId: part.id,
193
+ delta: deltaText,
194
+ model,
195
+ timestamp: now()
196
+ };
197
+ }
198
+ function* openToolCall(part) {
199
+ if (openedToolCalls.has(part.callID)) return;
200
+ openedToolCalls.add(part.callID);
201
+ const toolCallName = resolveToolName(part.tool, ctx.bridgedToolNames);
202
+ const input = part.state.input ?? {};
203
+ const args = JSON.stringify(input);
204
+ yield {
205
+ type: EventType.TOOL_CALL_START,
206
+ toolCallId: part.callID,
207
+ toolCallName,
208
+ toolName: toolCallName,
209
+ model,
210
+ timestamp: now()
211
+ };
212
+ yield {
213
+ type: EventType.TOOL_CALL_ARGS,
214
+ toolCallId: part.callID,
215
+ model,
216
+ timestamp: now(),
217
+ delta: args,
218
+ args
219
+ };
220
+ yield {
221
+ type: EventType.TOOL_CALL_END,
222
+ toolCallId: part.callID,
223
+ toolCallName,
224
+ toolName: toolCallName,
225
+ model,
226
+ timestamp: now(),
227
+ input
228
+ };
229
+ unresolvedToolCalls.add(part.callID);
230
+ }
231
+ function* handleToolPart(part) {
232
+ yield* closeText();
233
+ yield* closeReasoning();
234
+ yield* openToolCall(part);
235
+ const state = part.state;
236
+ if (state.status !== "completed" && state.status !== "error") return;
237
+ if (resolvedToolCalls.has(part.callID)) return;
238
+ resolvedToolCalls.add(part.callID);
239
+ unresolvedToolCalls.delete(part.callID);
240
+ const isError = state.status === "error";
241
+ yield {
242
+ type: EventType.TOOL_CALL_RESULT,
243
+ toolCallId: part.callID,
244
+ messageId: genId(),
245
+ model,
246
+ timestamp: now(),
247
+ content: isError ? state.error : state.output,
248
+ ...isError && { state: "output-error" }
249
+ };
250
+ }
251
+ function* handleEvent(event) {
252
+ if (event.type === "message.part.updated") {
253
+ const { part, delta } = event.properties;
254
+ if (isTextPart(part)) yield* handleTextPart(part, delta);
255
+ else if (isReasoningPart(part)) yield* handleReasoningPart(part, delta);
256
+ else if (isToolPart(part)) yield* handleToolPart(part);
257
+ } else if (event.type === "todo.updated") yield {
258
+ type: EventType.CUSTOM,
259
+ model,
260
+ timestamp: now(),
261
+ name: TODO_EVENT,
262
+ value: { todos: event.properties.todos }
263
+ };
264
+ }
265
+ function* finish(message) {
266
+ yield* startRun();
267
+ yield* closeText();
268
+ yield* closeReasoning();
269
+ yield* synthesizeUnresolvedResults();
270
+ const error = messageError(message);
271
+ if (error) {
272
+ yield {
273
+ type: EventType.RUN_ERROR,
274
+ model,
275
+ timestamp: now(),
276
+ message: error.message,
277
+ error
278
+ };
279
+ return;
280
+ }
281
+ const usage = buildUsage(message.tokens);
282
+ const finishReason = message.finish === "length" ? "length" : "stop";
283
+ yield {
284
+ type: EventType.RUN_FINISHED,
285
+ runId,
286
+ threadId,
287
+ model,
288
+ timestamp: now(),
289
+ finishReason,
290
+ ...usage !== void 0 && { usage }
291
+ };
292
+ }
293
+ try {
294
+ for await (const streamEvent of events) {
295
+ ctx.onStreamEvent?.(streamEvent);
296
+ if (streamEvent.kind === "session") {
297
+ yield* startRun();
298
+ yield {
299
+ type: EventType.CUSTOM,
300
+ model,
301
+ timestamp: now(),
302
+ name: SESSION_ID_EVENT,
303
+ value: { sessionId: streamEvent.sessionId }
304
+ };
305
+ } else if (streamEvent.kind === "event") {
306
+ yield* startRun();
307
+ yield* handleEvent(streamEvent.event);
308
+ } else yield* finish(streamEvent.message);
309
+ }
310
+ } catch (error) {
311
+ yield* closeText();
312
+ yield* closeReasoning();
313
+ yield* synthesizeUnresolvedResults();
314
+ throw error;
315
+ }
297
316
  }
298
- export {
299
- BRIDGED_MCP_SERVER_NAME,
300
- SESSION_ID_EVENT,
301
- TODO_EVENT,
302
- resolveToolName,
303
- translateOpencodeStream
304
- };
305
- //# sourceMappingURL=translate.js.map
317
+ //#endregion
318
+ export { BRIDGED_MCP_SERVER_NAME, SESSION_ID_EVENT, TODO_EVENT, resolveToolName, translateOpencodeStream };
319
+
320
+ //# sourceMappingURL=translate.js.map