@terminaluse/vercel-ai-sdk-provider 0.3.0 → 0.4.1

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/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { TerminalUse } from "@terminaluse/sdk";
2
1
  import { LanguageModel } from "ai";
2
+ import { TerminalUse } from "@terminaluse/sdk";
3
3
 
4
4
  //#region src/provider.d.ts
5
5
  interface TerminalUseProviderConfig {
@@ -19,6 +19,12 @@ interface TerminalUseProvider {
19
19
  interface TerminalUseProviderOptions {
20
20
  /** Task ID to send messages to (required) */
21
21
  taskId: string;
22
+ /** Skip task event dispatch and only stream task output */
23
+ skipSend?: boolean;
24
+ /** Optional absolute URL override for stream endpoint */
25
+ streamUrl?: string;
26
+ /** Optional headers for stream requests (e.g. bridge auth) */
27
+ streamHeaders?: Record<string, string>;
22
28
  /**
23
29
  * Optional override for the outbound task event. If omitted, provider sends
24
30
  * a text event derived from the latest prompt message (default behavior).
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { TerminalUseClient } from "@terminaluse/sdk";
2
1
  import { UnsupportedFunctionalityError } from "ai";
3
2
  import { createEventSourceResponseHandler, getFromApi } from "@ai-sdk/provider-utils";
4
3
  import { z } from "zod";
4
+ import { TerminalUseClient } from "@terminaluse/sdk";
5
5
 
6
6
  //#region src/ndjson-stream.ts
7
7
  /**
@@ -50,6 +50,14 @@ const finishPartSchema = z.object({
50
50
  totalUsage: streamUsageSchema,
51
51
  metadata: streamMetadataSchema
52
52
  });
53
+ const handlerCompletePartSchema = z.object({
54
+ type: z.literal("handler-complete"),
55
+ eventId: z.string(),
56
+ taskId: z.string(),
57
+ success: z.boolean(),
58
+ durationMs: z.number().int().nullable().optional(),
59
+ error: z.string().nullable().optional()
60
+ });
53
61
  const textStartPartSchema = z.object({
54
62
  type: z.literal("text-start"),
55
63
  id: z.string(),
@@ -127,6 +135,7 @@ const textStreamPartSchema = z.discriminatedUnion("type", [
127
135
  startStepPartSchema,
128
136
  finishStepPartSchema,
129
137
  finishPartSchema,
138
+ handlerCompletePartSchema,
130
139
  textStartPartSchema,
131
140
  textDeltaPartSchema,
132
141
  textEndPartSchema,
@@ -145,10 +154,14 @@ const textStreamPartSchema = z.discriminatedUnion("type", [
145
154
  * Uses @ai-sdk/provider-utils for SSE parsing.
146
155
  */
147
156
  async function* createTaskEventGenerator(config, taskId, options = {}) {
148
- const { signal } = options;
157
+ const { signal, streamUrl, streamHeaders } = options;
158
+ const headers = {
159
+ ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
160
+ ...streamHeaders ?? {}
161
+ };
149
162
  const { value: responseStream } = await getFromApi({
150
- url: `${config.baseURL}/tasks/${encodeURIComponent(taskId)}/stream`,
151
- headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
163
+ url: streamUrl ?? `${config.baseURL}/tasks/${encodeURIComponent(taskId)}/stream`,
164
+ headers,
152
165
  abortSignal: signal,
153
166
  successfulResponseHandler: createEventSourceResponseHandler(textStreamPartSchema),
154
167
  failedResponseHandler: async ({ response }) => {
@@ -175,9 +188,16 @@ async function* createTaskEventGenerator(config, taskId, options = {}) {
175
188
  * Creates a ReadableStream that transforms v2 TextStreamPart events to AI SDK v3 format.
176
189
  * Implements a thin passthrough adapter with minimal transformation logic.
177
190
  */
178
- function createTerminalUseTransformStream(config, taskId, signal) {
179
- const eventGenerator = createTaskEventGenerator(config, taskId, { signal });
191
+ function createTerminalUseTransformStream(config, taskId, signal, arg4, arg5) {
192
+ const { streamOverride, closeConfig } = resolveStreamArgs(arg4, arg5);
193
+ const eventGenerator = createTaskEventGenerator(config, taskId, {
194
+ signal,
195
+ streamUrl: streamOverride?.streamUrl,
196
+ streamHeaders: streamOverride?.streamHeaders
197
+ });
180
198
  let emittedStreamStart = false;
199
+ const pendingParts = [];
200
+ const trackedParts = createStreamedPartTracker();
181
201
  return new ReadableStream({
182
202
  async pull(controller) {
183
203
  if (!emittedStreamStart) {
@@ -188,6 +208,13 @@ function createTerminalUseTransformStream(config, taskId, signal) {
188
208
  });
189
209
  return;
190
210
  }
211
+ if (pendingParts.length > 0) {
212
+ const pendingPart = pendingParts.shift();
213
+ if (pendingPart) {
214
+ controller.enqueue(pendingPart);
215
+ return;
216
+ }
217
+ }
191
218
  try {
192
219
  while (true) {
193
220
  const { value, done } = await eventGenerator.next();
@@ -196,14 +223,16 @@ function createTerminalUseTransformStream(config, taskId, signal) {
196
223
  return;
197
224
  }
198
225
  const event = value;
199
- const transformed = transformEvent(event, taskId);
200
- if (event.type === "finish" || event.type === "error") {
201
- if (transformed) controller.enqueue(transformed);
226
+ const transformedParts = transformEvent(event, taskId, trackedParts);
227
+ if (shouldCloseStream(event, closeConfig)) {
228
+ for (const part of transformedParts) controller.enqueue(part);
202
229
  controller.close();
203
230
  return;
204
231
  }
205
- if (transformed) {
206
- controller.enqueue(transformed);
232
+ if (transformedParts.length > 0) {
233
+ const [firstPart, ...restParts] = transformedParts;
234
+ controller.enqueue(firstPart);
235
+ if (restParts.length > 0) pendingParts.push(...restParts);
207
236
  return;
208
237
  }
209
238
  }
@@ -219,19 +248,19 @@ function createTerminalUseTransformStream(config, taskId, signal) {
219
248
  }
220
249
  /**
221
250
  * Transforms a single v2 TextStreamPart event to AI SDK v3 LanguageModelV3StreamPart.
222
- * Returns null for events that should be skipped (e.g., finish-step, start).
251
+ * Returns an array to support synthesizing start parts for orphan deltas.
223
252
  */
224
- function transformEvent(event, taskId) {
253
+ function transformEvent(event, taskId, trackedParts) {
225
254
  switch (event.type) {
226
- case "start": return null;
227
- case "start-step": return {
255
+ case "start": return [];
256
+ case "start-step": return [{
228
257
  type: "response-metadata",
229
258
  id: taskId,
230
259
  timestamp: /* @__PURE__ */ new Date(),
231
260
  modelId: "terminaluse-agent"
232
- };
233
- case "finish-step": return null;
234
- case "finish": return {
261
+ }];
262
+ case "finish-step": return [];
263
+ case "finish": return [{
235
264
  type: "finish",
236
265
  finishReason: {
237
266
  unified: event.finishReason,
@@ -250,76 +279,145 @@ function transformEvent(event, taskId) {
250
279
  reasoning: void 0
251
280
  }
252
281
  }
253
- };
254
- case "text-start": return {
255
- type: "text-start",
256
- id: event.id
257
- };
258
- case "text-delta": return {
259
- type: "text-delta",
260
- id: event.id,
261
- delta: event.text
262
- };
263
- case "text-end": return {
264
- type: "text-end",
265
- id: event.id
266
- };
267
- case "reasoning-start": return {
268
- type: "reasoning-start",
269
- id: event.id
270
- };
271
- case "reasoning-delta": return {
272
- type: "reasoning-delta",
273
- id: event.id,
274
- delta: event.text
275
- };
276
- case "reasoning-end": return {
277
- type: "reasoning-end",
278
- id: event.id
279
- };
280
- case "tool-input-start": return {
281
- type: "tool-input-start",
282
- id: event.id,
283
- toolName: event.toolName,
284
- providerExecuted: true,
285
- dynamic: true
286
- };
287
- case "tool-input-delta": return {
288
- type: "tool-input-delta",
289
- id: event.id,
290
- delta: event.delta
291
- };
292
- case "tool-input-end": return {
293
- type: "tool-input-end",
294
- id: event.id
295
- };
296
- case "tool-call": return {
282
+ }];
283
+ case "handler-complete": return [];
284
+ case "text-start":
285
+ if (trackedParts.text.open.has(event.id)) return [];
286
+ trackedParts.text.open.add(event.id);
287
+ return [{
288
+ type: "text-start",
289
+ id: event.id
290
+ }];
291
+ case "text-delta": {
292
+ const deltaPart = {
293
+ type: "text-delta",
294
+ id: event.id,
295
+ delta: event.text
296
+ };
297
+ if (trackedParts.text.open.has(event.id)) return [deltaPart];
298
+ if (trackedParts.text.open.size > 0) return [];
299
+ trackedParts.text.open.add(event.id);
300
+ return [{
301
+ type: "text-start",
302
+ id: event.id
303
+ }, deltaPart];
304
+ }
305
+ case "text-end":
306
+ if (!trackedParts.text.open.has(event.id)) return [];
307
+ trackedParts.text.open.delete(event.id);
308
+ return [{
309
+ type: "text-end",
310
+ id: event.id
311
+ }];
312
+ case "reasoning-start":
313
+ if (trackedParts.reasoning.open.has(event.id)) return [];
314
+ trackedParts.reasoning.open.add(event.id);
315
+ return [{
316
+ type: "reasoning-start",
317
+ id: event.id
318
+ }];
319
+ case "reasoning-delta": {
320
+ const deltaPart = {
321
+ type: "reasoning-delta",
322
+ id: event.id,
323
+ delta: event.text
324
+ };
325
+ if (trackedParts.reasoning.open.has(event.id)) return [deltaPart];
326
+ if (trackedParts.reasoning.open.size > 0) return [];
327
+ trackedParts.reasoning.open.add(event.id);
328
+ return [{
329
+ type: "reasoning-start",
330
+ id: event.id
331
+ }, deltaPart];
332
+ }
333
+ case "reasoning-end":
334
+ if (!trackedParts.reasoning.open.has(event.id)) return [];
335
+ trackedParts.reasoning.open.delete(event.id);
336
+ return [{
337
+ type: "reasoning-end",
338
+ id: event.id
339
+ }];
340
+ case "tool-input-start":
341
+ if (trackedParts.toolInput.open.has(event.id)) return [];
342
+ trackedParts.toolInput.open.add(event.id);
343
+ return [{
344
+ type: "tool-input-start",
345
+ id: event.id,
346
+ toolName: event.toolName,
347
+ providerExecuted: true,
348
+ dynamic: true
349
+ }];
350
+ case "tool-input-delta":
351
+ if (!trackedParts.toolInput.open.has(event.id)) return [];
352
+ return [{
353
+ type: "tool-input-delta",
354
+ id: event.id,
355
+ delta: event.delta
356
+ }];
357
+ case "tool-input-end":
358
+ if (!trackedParts.toolInput.open.has(event.id)) return [];
359
+ trackedParts.toolInput.open.delete(event.id);
360
+ return [{
361
+ type: "tool-input-end",
362
+ id: event.id
363
+ }];
364
+ case "tool-call": return [{
297
365
  type: "tool-call",
298
366
  toolCallId: event.toolCallId,
299
367
  toolName: event.toolName,
300
368
  input: JSON.stringify(event.input),
301
369
  providerExecuted: true,
302
370
  dynamic: true
303
- };
371
+ }];
304
372
  case "tool-result": {
305
373
  const result = event.output ?? "";
306
- return {
374
+ return [{
307
375
  type: "tool-result",
308
376
  toolCallId: event.toolCallId,
309
377
  toolName: event.toolName,
310
378
  result
311
- };
379
+ }];
312
380
  }
313
- case "error": return {
381
+ case "error": return [{
314
382
  type: "error",
315
383
  error: event.error
316
- };
317
- default: return null;
384
+ }];
385
+ default: return [];
318
386
  }
319
387
  }
388
+ function createStreamedPartTracker() {
389
+ return {
390
+ text: createSegmentTracker(),
391
+ reasoning: createSegmentTracker(),
392
+ toolInput: createSegmentTracker()
393
+ };
394
+ }
395
+ function createSegmentTracker() {
396
+ return { open: /* @__PURE__ */ new Set() };
397
+ }
398
+ function shouldCloseStream(event, closeConfig) {
399
+ if (event.type === "error") return true;
400
+ if (closeConfig.closeMode === "legacy") return event.type === "finish";
401
+ return event.type === "handler-complete" && event.eventId === closeConfig.eventId;
402
+ }
403
+ function resolveStreamArgs(arg4, arg5) {
404
+ if (isStreamCloseConfig(arg4)) return {
405
+ streamOverride: void 0,
406
+ closeConfig: arg4
407
+ };
408
+ return {
409
+ streamOverride: arg4,
410
+ closeConfig: arg5 ?? { closeMode: "legacy" }
411
+ };
412
+ }
413
+ function isStreamCloseConfig(value) {
414
+ if (!value || typeof value !== "object") return false;
415
+ return "closeMode" in value;
416
+ }
320
417
 
321
418
  //#endregion
322
419
  //#region src/provider.ts
420
+ const HANDLER_COMPLETE_CLOSE_CAPABILITY = "handler-complete-close-v1";
323
421
  /**
324
422
  * Creates a custom AI SDK provider for TerminalUse agents.
325
423
  *
@@ -372,7 +470,7 @@ function createTerminalUseProvider(config) {
372
470
  const { prompt, providerOptions, abortSignal } = options;
373
471
  const tuOptions = providerOptions?.terminaluse;
374
472
  if (!tuOptions?.taskId) throw new Error("taskId is required. Create a task via /api/tasks first.");
375
- const { taskId } = tuOptions;
473
+ const { taskId, skipSend = false } = tuOptions;
376
474
  const explicitEvent = tuOptions.event;
377
475
  const defaultEventContent = (() => {
378
476
  const userContent = prompt.at(-1)?.content;
@@ -386,22 +484,54 @@ function createTerminalUseProvider(config) {
386
484
  text: textContent || ""
387
485
  };
388
486
  })();
389
- const requestBody = {
390
- task_id: taskId,
391
- content: explicitEvent?.content ?? defaultEventContent,
392
- ...explicitEvent?.idempotencyKey ? { idempotency_key: explicitEvent.idempotencyKey } : {},
393
- ...typeof explicitEvent?.persistMessage === "boolean" ? { persist_message: explicitEvent.persistMessage } : {}
394
- };
395
- try {
396
- await client.tasks.sendEvent(requestBody);
397
- } catch (error) {
398
- throw new Error(`Failed to send message: ${error instanceof Error ? error.message : String(error)}`);
487
+ if (!skipSend) {
488
+ const requestBody = {
489
+ task_id: taskId,
490
+ content: explicitEvent?.content ?? defaultEventContent,
491
+ ...explicitEvent?.idempotencyKey ? { idempotency_key: explicitEvent.idempotencyKey } : {},
492
+ ...typeof explicitEvent?.persistMessage === "boolean" ? { persist_message: explicitEvent.persistMessage } : {}
493
+ };
494
+ let sendEventResponse;
495
+ try {
496
+ sendEventResponse = await client.tasks.sendEvent(requestBody);
497
+ } catch (error) {
498
+ throw new Error(`Failed to send message: ${error instanceof Error ? error.message : String(error)}`);
499
+ }
500
+ const closeConfig = resolveStreamCloseConfig(sendEventResponse);
501
+ return { stream: typeof tuOptions.streamUrl === "string" || tuOptions.streamHeaders && Object.keys(tuOptions.streamHeaders).length > 0 ? createTerminalUseTransformStream(tuConfig, taskId, abortSignal, {
502
+ streamUrl: tuOptions.streamUrl,
503
+ streamHeaders: tuOptions.streamHeaders
504
+ }, closeConfig) : createTerminalUseTransformStream(tuConfig, taskId, abortSignal, closeConfig) };
399
505
  }
400
- return { stream: createTerminalUseTransformStream(tuConfig, taskId, abortSignal) };
506
+ return { stream: typeof tuOptions.streamUrl === "string" || tuOptions.streamHeaders && Object.keys(tuOptions.streamHeaders).length > 0 ? createTerminalUseTransformStream(tuConfig, taskId, abortSignal, {
507
+ streamUrl: tuOptions.streamUrl,
508
+ streamHeaders: tuOptions.streamHeaders
509
+ }) : createTerminalUseTransformStream(tuConfig, taskId, abortSignal) };
401
510
  }
402
511
  };
403
512
  } };
404
513
  }
514
+ function resolveStreamCloseConfig(response) {
515
+ const eventId = getStringProperty(response, "id");
516
+ const streamCapabilities = getStringArrayProperty(response, "streamCapabilities") ?? getStringArrayProperty(response, "stream_capabilities");
517
+ if (eventId && streamCapabilities?.includes(HANDLER_COMPLETE_CLOSE_CAPABILITY)) return {
518
+ closeMode: "handler-complete",
519
+ eventId
520
+ };
521
+ return { closeMode: "legacy" };
522
+ }
523
+ function getStringProperty(value, key) {
524
+ if (!value || typeof value !== "object") return;
525
+ const maybeValue = value[key];
526
+ return typeof maybeValue === "string" ? maybeValue : void 0;
527
+ }
528
+ function getStringArrayProperty(value, key) {
529
+ if (!value || typeof value !== "object") return;
530
+ const maybeArray = value[key];
531
+ if (!Array.isArray(maybeArray)) return;
532
+ if (!maybeArray.every((entry) => typeof entry === "string")) return;
533
+ return maybeArray;
534
+ }
405
535
 
406
536
  //#endregion
407
537
  export { createTerminalUseProvider };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terminaluse/vercel-ai-sdk-provider",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Vercel AI SDK provider for TerminalUse agents",
5
5
  "repository": {
6
6
  "type": "git",