@terminaluse/vercel-ai-sdk-provider 0.2.0 → 0.4.0

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 CHANGED
@@ -36,9 +36,39 @@ const result = await streamText({
36
36
  | Option | Type | Description |
37
37
  |--------|------|-------------|
38
38
  | `taskId` | `string` | Task ID to send messages to (required) |
39
+ | `event` | `object` | Optional explicit task event override (`content`, `persistMessage`, `idempotencyKey`) |
39
40
 
40
41
  Tasks must be created separately using the [`@terminaluse/sdk`](https://www.npmjs.com/package/@terminaluse/sdk).
41
42
 
43
+ ### Sending data events
44
+
45
+ By default, the provider sends a text event derived from the last prompt message.
46
+ If you need to send a structured task event (for example, AskUserQuestion answers),
47
+ pass `providerOptions.terminaluse.event`:
48
+
49
+ ```typescript
50
+ await streamText({
51
+ model: terminaluse.agent('namespace/agent-name'),
52
+ messages: [],
53
+ providerOptions: {
54
+ terminaluse: {
55
+ taskId,
56
+ event: {
57
+ content: {
58
+ type: 'data',
59
+ data: {
60
+ type: 'ask_user_answer',
61
+ answers: { 'Question?': 'Answer' },
62
+ },
63
+ },
64
+ persistMessage: false,
65
+ idempotencyKey: 'optional-idempotency-key',
66
+ },
67
+ },
68
+ },
69
+ });
70
+ ```
71
+
42
72
  ## Conversation Continuity
43
73
 
44
74
  To maintain conversation history across multiple interactions, create a task once
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { LanguageModel } from "ai";
2
+ import { TerminalUse } from "@terminaluse/sdk";
2
3
 
3
4
  //#region src/provider.d.ts
4
5
  interface TerminalUseProviderConfig {
@@ -18,6 +19,31 @@ interface TerminalUseProvider {
18
19
  interface TerminalUseProviderOptions {
19
20
  /** Task ID to send messages to (required) */
20
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>;
28
+ /**
29
+ * Optional override for the outbound task event. If omitted, provider sends
30
+ * a text event derived from the latest prompt message (default behavior).
31
+ */
32
+ event?: TerminalUseProviderTaskEventOptions;
33
+ }
34
+ interface TerminalUseProviderTaskEventOptions {
35
+ /**
36
+ * Explicit event content to send. Supports text and data task events.
37
+ */
38
+ content: NonNullable<TerminalUse.CreateTaskEventRequest['content']>;
39
+ /**
40
+ * Whether TerminalUse should persist this event as a message.
41
+ */
42
+ persistMessage?: boolean;
43
+ /**
44
+ * Optional idempotency key for retry-safe event delivery.
45
+ */
46
+ idempotencyKey?: string;
21
47
  }
22
48
  /**
23
49
  * Creates a custom AI SDK provider for TerminalUse agents.
@@ -48,4 +74,4 @@ interface TerminalUseProviderOptions {
48
74
  */
49
75
  declare function createTerminalUseProvider(config: TerminalUseProviderConfig): TerminalUseProvider;
50
76
  //#endregion
51
- export { type TerminalUseProvider, type TerminalUseProviderConfig, type TerminalUseProviderOptions, createTerminalUseProvider };
77
+ export { type TerminalUseProvider, type TerminalUseProviderConfig, type TerminalUseProviderOptions, type TerminalUseProviderTaskEventOptions, createTerminalUseProvider };
package/dist/index.mjs CHANGED
@@ -1,22 +1,8 @@
1
1
  import { UnsupportedFunctionalityError } from "ai";
2
2
  import { createEventSourceResponseHandler, getFromApi } from "@ai-sdk/provider-utils";
3
3
  import { z } from "zod";
4
+ import { TerminalUseClient } from "@terminaluse/sdk";
4
5
 
5
- //#region src/http.ts
6
- /**
7
- * Fetch wrapper with TerminalUse authentication headers.
8
- */
9
- async function terminaluseFetch(config, path, options = {}) {
10
- const headers = new Headers(options.headers);
11
- if (config.apiKey) headers.set("Authorization", `Bearer ${config.apiKey}`);
12
- if (!headers.has("Content-Type") && options.body) headers.set("Content-Type", "application/json");
13
- return fetch(`${config.baseURL}${path}`, {
14
- ...options,
15
- headers
16
- });
17
- }
18
-
19
- //#endregion
20
6
  //#region src/ndjson-stream.ts
21
7
  /**
22
8
  * SSE streaming utility for TerminalUse task events.
@@ -64,6 +50,14 @@ const finishPartSchema = z.object({
64
50
  totalUsage: streamUsageSchema,
65
51
  metadata: streamMetadataSchema
66
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
+ });
67
61
  const textStartPartSchema = z.object({
68
62
  type: z.literal("text-start"),
69
63
  id: z.string(),
@@ -141,6 +135,7 @@ const textStreamPartSchema = z.discriminatedUnion("type", [
141
135
  startStepPartSchema,
142
136
  finishStepPartSchema,
143
137
  finishPartSchema,
138
+ handlerCompletePartSchema,
144
139
  textStartPartSchema,
145
140
  textDeltaPartSchema,
146
141
  textEndPartSchema,
@@ -159,10 +154,14 @@ const textStreamPartSchema = z.discriminatedUnion("type", [
159
154
  * Uses @ai-sdk/provider-utils for SSE parsing.
160
155
  */
161
156
  async function* createTaskEventGenerator(config, taskId, options = {}) {
162
- const { signal } = options;
157
+ const { signal, streamUrl, streamHeaders } = options;
158
+ const headers = {
159
+ ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
160
+ ...streamHeaders ?? {}
161
+ };
163
162
  const { value: responseStream } = await getFromApi({
164
- url: `${config.baseURL}/tasks/${encodeURIComponent(taskId)}/stream`,
165
- headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
163
+ url: streamUrl ?? `${config.baseURL}/tasks/${encodeURIComponent(taskId)}/stream`,
164
+ headers,
166
165
  abortSignal: signal,
167
166
  successfulResponseHandler: createEventSourceResponseHandler(textStreamPartSchema),
168
167
  failedResponseHandler: async ({ response }) => {
@@ -189,8 +188,13 @@ async function* createTaskEventGenerator(config, taskId, options = {}) {
189
188
  * Creates a ReadableStream that transforms v2 TextStreamPart events to AI SDK v3 format.
190
189
  * Implements a thin passthrough adapter with minimal transformation logic.
191
190
  */
192
- function createTerminalUseTransformStream(config, taskId, signal) {
193
- 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
+ });
194
198
  let emittedStreamStart = false;
195
199
  return new ReadableStream({
196
200
  async pull(controller) {
@@ -211,7 +215,7 @@ function createTerminalUseTransformStream(config, taskId, signal) {
211
215
  }
212
216
  const event = value;
213
217
  const transformed = transformEvent(event, taskId);
214
- if (event.type === "finish" || event.type === "error") {
218
+ if (shouldCloseStream(event, closeConfig)) {
215
219
  if (transformed) controller.enqueue(transformed);
216
220
  controller.close();
217
221
  return;
@@ -265,6 +269,7 @@ function transformEvent(event, taskId) {
265
269
  }
266
270
  }
267
271
  };
272
+ case "handler-complete": return null;
268
273
  case "text-start": return {
269
274
  type: "text-start",
270
275
  id: event.id
@@ -331,9 +336,29 @@ function transformEvent(event, taskId) {
331
336
  default: return null;
332
337
  }
333
338
  }
339
+ function shouldCloseStream(event, closeConfig) {
340
+ if (event.type === "error") return true;
341
+ if (closeConfig.closeMode === "legacy") return event.type === "finish";
342
+ return event.type === "handler-complete" && event.eventId === closeConfig.eventId;
343
+ }
344
+ function resolveStreamArgs(arg4, arg5) {
345
+ if (isStreamCloseConfig(arg4)) return {
346
+ streamOverride: void 0,
347
+ closeConfig: arg4
348
+ };
349
+ return {
350
+ streamOverride: arg4,
351
+ closeConfig: arg5 ?? { closeMode: "legacy" }
352
+ };
353
+ }
354
+ function isStreamCloseConfig(value) {
355
+ if (!value || typeof value !== "object") return false;
356
+ return "closeMode" in value;
357
+ }
334
358
 
335
359
  //#endregion
336
360
  //#region src/provider.ts
361
+ const HANDLER_COMPLETE_CLOSE_CAPABILITY = "handler-complete-close-v1";
337
362
  /**
338
363
  * Creates a custom AI SDK provider for TerminalUse agents.
339
364
  *
@@ -366,6 +391,10 @@ function createTerminalUseProvider(config) {
366
391
  baseURL: config.baseURL,
367
392
  apiKey: config.apiKey
368
393
  };
394
+ const client = new TerminalUseClient({
395
+ environment: config.baseURL,
396
+ bearerAuth: { token: config.apiKey }
397
+ });
369
398
  return { agent(agentName) {
370
399
  return {
371
400
  specificationVersion: "v3",
@@ -382,29 +411,68 @@ function createTerminalUseProvider(config) {
382
411
  const { prompt, providerOptions, abortSignal } = options;
383
412
  const tuOptions = providerOptions?.terminaluse;
384
413
  if (!tuOptions?.taskId) throw new Error("taskId is required. Create a task via /api/tasks first.");
385
- const { taskId } = tuOptions;
386
- const userContent = prompt.at(-1)?.content;
387
- let textContent;
388
- if (Array.isArray(userContent)) {
389
- const textPart = userContent.find((c) => c.type === "text");
390
- if (textPart && textPart.type === "text") textContent = textPart.text;
391
- } else if (typeof userContent === "string") textContent = userContent;
392
- const sendResponse = await terminaluseFetch(tuConfig, `/tasks/${encodeURIComponent(taskId)}/events`, {
393
- method: "POST",
394
- body: JSON.stringify({ content: {
414
+ const { taskId, skipSend = false } = tuOptions;
415
+ const explicitEvent = tuOptions.event;
416
+ const defaultEventContent = (() => {
417
+ const userContent = prompt.at(-1)?.content;
418
+ let textContent;
419
+ if (Array.isArray(userContent)) {
420
+ const textPart = userContent.find((c) => c.type === "text");
421
+ if (textPart && textPart.type === "text") textContent = textPart.text;
422
+ } else if (typeof userContent === "string") textContent = userContent;
423
+ return {
395
424
  type: "text",
396
425
  text: textContent || ""
397
- } })
398
- });
399
- if (!sendResponse.ok) {
400
- const errorText = await sendResponse.text();
401
- throw new Error(`Failed to send message: ${sendResponse.status} - ${errorText}`);
426
+ };
427
+ })();
428
+ if (!skipSend) {
429
+ const requestBody = {
430
+ task_id: taskId,
431
+ content: explicitEvent?.content ?? defaultEventContent,
432
+ ...explicitEvent?.idempotencyKey ? { idempotency_key: explicitEvent.idempotencyKey } : {},
433
+ ...typeof explicitEvent?.persistMessage === "boolean" ? { persist_message: explicitEvent.persistMessage } : {}
434
+ };
435
+ let sendEventResponse;
436
+ try {
437
+ sendEventResponse = await client.tasks.sendEvent(requestBody);
438
+ } catch (error) {
439
+ throw new Error(`Failed to send message: ${error instanceof Error ? error.message : String(error)}`);
440
+ }
441
+ const closeConfig = resolveStreamCloseConfig(sendEventResponse);
442
+ return { stream: typeof tuOptions.streamUrl === "string" || tuOptions.streamHeaders && Object.keys(tuOptions.streamHeaders).length > 0 ? createTerminalUseTransformStream(tuConfig, taskId, abortSignal, {
443
+ streamUrl: tuOptions.streamUrl,
444
+ streamHeaders: tuOptions.streamHeaders
445
+ }, closeConfig) : createTerminalUseTransformStream(tuConfig, taskId, abortSignal, closeConfig) };
402
446
  }
403
- return { stream: createTerminalUseTransformStream(tuConfig, taskId, abortSignal) };
447
+ return { stream: typeof tuOptions.streamUrl === "string" || tuOptions.streamHeaders && Object.keys(tuOptions.streamHeaders).length > 0 ? createTerminalUseTransformStream(tuConfig, taskId, abortSignal, {
448
+ streamUrl: tuOptions.streamUrl,
449
+ streamHeaders: tuOptions.streamHeaders
450
+ }) : createTerminalUseTransformStream(tuConfig, taskId, abortSignal) };
404
451
  }
405
452
  };
406
453
  } };
407
454
  }
455
+ function resolveStreamCloseConfig(response) {
456
+ const eventId = getStringProperty(response, "id");
457
+ const streamCapabilities = getStringArrayProperty(response, "streamCapabilities") ?? getStringArrayProperty(response, "stream_capabilities");
458
+ if (eventId && streamCapabilities?.includes(HANDLER_COMPLETE_CLOSE_CAPABILITY)) return {
459
+ closeMode: "handler-complete",
460
+ eventId
461
+ };
462
+ return { closeMode: "legacy" };
463
+ }
464
+ function getStringProperty(value, key) {
465
+ if (!value || typeof value !== "object") return;
466
+ const maybeValue = value[key];
467
+ return typeof maybeValue === "string" ? maybeValue : void 0;
468
+ }
469
+ function getStringArrayProperty(value, key) {
470
+ if (!value || typeof value !== "object") return;
471
+ const maybeArray = value[key];
472
+ if (!Array.isArray(maybeArray)) return;
473
+ if (!maybeArray.every((entry) => typeof entry === "string")) return;
474
+ return maybeArray;
475
+ }
408
476
 
409
477
  //#endregion
410
478
  export { createTerminalUseProvider };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terminaluse/vercel-ai-sdk-provider",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Vercel AI SDK provider for TerminalUse agents",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,11 +31,13 @@
31
31
  "lint": "biome lint .",
32
32
  "format": "biome format --write .",
33
33
  "check": "biome check --write .",
34
- "prepublishOnly": "bun run build"
34
+ "check:publish-manifest": "node ./scripts/check-publish-manifest.mjs",
35
+ "prepublishOnly": "bun run check:publish-manifest && bun run build"
35
36
  },
36
37
  "dependencies": {
37
38
  "@ai-sdk/provider": "^3.0.0",
38
39
  "@ai-sdk/provider-utils": "^4.0.0",
40
+ "@terminaluse/sdk": "^0.7.0",
39
41
  "zod": "^3.24.0"
40
42
  },
41
43
  "peerDependencies": {