tina4-nodejs 3.13.112 → 3.13.114

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.
@@ -21,10 +21,92 @@ export interface ChatResponse {
21
21
  finishReason: string | null;
22
22
  raw: Record<string, unknown>;
23
23
  }
24
- export interface AiMessage {
24
+ /**
25
+ * A multimodal content part. `text` carries plain UTF-8 prose; `image`
26
+ * carries a `data:<media_type>;base64,<payload>` URI or an https:// URL
27
+ * (the client translates to each provider's shape, ADR-0060). `tool_result`
28
+ * carries the Anthropic-style return of a locally-executed tool call
29
+ * (ADR-0061); the client translates it to OpenAI's `{role: "tool", ...}`
30
+ * turn on non-Anthropic providers.
31
+ */
32
+ export type ContentPart = {
33
+ type: "text";
34
+ text: string;
35
+ } | {
36
+ type: "image";
37
+ source: string;
38
+ } | {
39
+ type: "tool_result";
40
+ tool_use_id: string;
41
+ content: string;
42
+ };
43
+ /** The value a caller may pass for `message.content`. ADR-0060. */
44
+ export type AiMessageContent = string | ContentPart[];
45
+ /**
46
+ * One conversation turn. The three "chat" roles carry a string OR a
47
+ * content-parts array (ADR-0060). The `tool` role is the OpenAI-style
48
+ * return of a tool call (ADR-0061); the client translates it to the
49
+ * Anthropic user-turn form when the current provider is Anthropic.
50
+ */
51
+ export type AiMessage = {
25
52
  role: "system" | "user" | "assistant";
53
+ content: AiMessageContent;
54
+ } | {
55
+ role: "tool";
56
+ tool_call_id: string;
26
57
  content: string;
58
+ };
59
+ /**
60
+ * A tool declaration the model may call (named `AiToolDeclaration` to
61
+ * stay out of the way of {@link ./ai.ts}'s existing `AiTool` interface
62
+ * for AI-coding-tool context installation). `parameters` is a JSON
63
+ * Schema object; it is passed to the provider unchanged (ADR-0061
64
+ * `parameters-passthrough`).
65
+ */
66
+ export interface AiToolDeclaration {
67
+ name: string;
68
+ description: string;
69
+ parameters: Record<string, unknown>;
27
70
  }
71
+ /**
72
+ * How the model picks a tool. Four Tina4 values that span the useful cases
73
+ * across providers (ADR-0061 wire-translation table):
74
+ * 'auto' — model may call any tool or answer with text
75
+ * 'none' — model must not call a tool (Anthropic omits `tools`)
76
+ * 'required' — model must call some tool
77
+ * {name: 'x'} — model must call tool 'x'
78
+ */
79
+ export type AiToolChoice = "auto" | "none" | "required" | {
80
+ name: string;
81
+ };
82
+ /**
83
+ * One event yielded by `Ai.chat(stream: true)`. The four variants
84
+ * discriminated by `type`. Text deltas arrive per chunk (typewriter UX);
85
+ * `tool_call` fires once per call, aggregated from provider fragments;
86
+ * `done` fires exactly once after all deltas; `error` replaces `done` on
87
+ * mid-stream failure. ADR-0060.
88
+ */
89
+ export type AiEvent = {
90
+ type: "text_delta";
91
+ text: string;
92
+ } | {
93
+ type: "tool_call";
94
+ id: string;
95
+ name: string;
96
+ args: Record<string, unknown>;
97
+ } | {
98
+ type: "done";
99
+ finishReason: string;
100
+ usage?: {
101
+ promptTokens: number;
102
+ completionTokens: number;
103
+ totalTokens: number;
104
+ };
105
+ } | {
106
+ type: "error";
107
+ message: string;
108
+ code?: string;
109
+ };
28
110
  export interface AiChatOptions {
29
111
  model?: string;
30
112
  temperature?: number;
@@ -32,6 +114,14 @@ export interface AiChatOptions {
32
114
  stream?: boolean;
33
115
  timeout?: number;
34
116
  provider?: "local" | "openai" | "anthropic";
117
+ /** Tools the model may call. ADR-0061 — translated per provider. */
118
+ tools?: AiToolDeclaration[];
119
+ /**
120
+ * How the model picks a tool. ADR-0061 — translated per provider. If
121
+ * `'none'` on Anthropic (which has no "none" mode), `tools` is omitted
122
+ * from the outbound body entirely.
123
+ */
124
+ toolChoice?: AiToolChoice;
35
125
  }
36
126
  export interface AiEmbedOptions {
37
127
  model?: string;
@@ -41,26 +131,89 @@ export interface AiEmbedOptions {
41
131
  export declare class Ai {
42
132
  static chat(messages: AiMessage[], options: AiChatOptions & {
43
133
  stream: true;
44
- }): AsyncGenerator<string>;
134
+ }): AsyncGenerator<AiEvent>;
45
135
  static chat(messages: AiMessage[], options?: AiChatOptions & {
46
136
  stream?: false;
47
137
  }): Promise<ChatResponse>;
48
138
  static complete(prompt: string, options?: Omit<AiChatOptions, "stream">): Promise<string>;
49
139
  static embed(textOrTexts: string | string[], options?: AiEmbedOptions): Promise<number[] | number[][]>;
140
+ /**
141
+ * Validate role + content shape. Content may be a string OR a non-empty
142
+ * list of {type:'text'|'image'|'tool_result', ...} parts (ADR-0060 +
143
+ * ADR-0061). The `tool` role is the OpenAI-style tool-result turn
144
+ * (ADR-0061). Malformed parts fail fast with AiConfigError, never
145
+ * reaching the wire.
146
+ */
50
147
  private static validateMessages;
148
+ private static validateContent;
149
+ /**
150
+ * Validate the outbound tool declarations (ADR-0061). Each tool needs a
151
+ * non-empty `name`, a string `description`, and a JSON-Schema-shaped
152
+ * `parameters` object. Malformed tools fail fast with AiConfigError,
153
+ * never reaching the wire.
154
+ */
155
+ private static validateTools;
156
+ /**
157
+ * Validate the outbound tool_choice value (ADR-0061). The four accepted
158
+ * shapes are 'auto', 'none', 'required', and {name: 'x'}.
159
+ */
160
+ private static validateToolChoice;
51
161
  private static number;
52
162
  private static config;
53
163
  private static endpoint;
54
164
  private static headers;
165
+ /**
166
+ * Build the provider-specific request body from a Tina4-shaped message
167
+ * list plus optional tool declarations (ADR-0060 + ADR-0061).
168
+ *
169
+ * Content parts translate per provider:
170
+ * - OpenAI/local: image → {type:'image_url', image_url:{url}}
171
+ * - Anthropic: image → {type:'image', source:{type:'base64'|'url', ...}}
172
+ * String content is preserved verbatim in the OpenAI/local shape and
173
+ * likewise for Anthropic (both accept a bare string).
174
+ *
175
+ * Tool-result turns are normalised to the current provider's expected
176
+ * shape (either the OpenAI `{role:"tool", tool_call_id, content}` turn or
177
+ * the Anthropic `{role:"user", content:[{type:"tool_result", ...}]}`
178
+ * turn), so an agent-loop written against Tina4 never has to fork on
179
+ * TINA4_AI_PROVIDER (ADR-0061 wire translation).
180
+ */
55
181
  private static chatBody;
182
+ /**
183
+ * Normalise the Tina4-shaped messages into the provider's on-wire shape.
184
+ * The `tool` role and the `tool_result` content part are translated
185
+ * between the OpenAI and Anthropic forms so either input works against
186
+ * either provider (ADR-0061 return-path table).
187
+ */
188
+ private static normalizeMessagesForProvider;
189
+ /**
190
+ * Attach the outbound `tools` and `tool_choice` (ADR-0061 outbound
191
+ * translation tables) to the body in place. When toolChoice is 'none'
192
+ * on Anthropic (Anthropic has no "none" mode) the tools list is omitted
193
+ * entirely — the model cannot call what it cannot see.
194
+ */
195
+ private static applyTools;
196
+ /**
197
+ * Translate one message content value into the provider's on-wire shape.
198
+ * A plain string is passed through (both providers accept a string
199
+ * content). A parts array becomes provider-native content blocks.
200
+ */
201
+ private static translateContent;
202
+ private static parseDataUri;
203
+ private static contentToPlainText;
56
204
  private static open;
57
205
  private static readBody;
58
206
  private static retryDelay;
59
207
  private static requestJson;
60
208
  private static normalizeChat;
61
209
  private static chatResponse;
62
- private static streamDelta;
63
- private static streamData;
64
- private static streamError;
210
+ /**
211
+ * Stream the response through the shared {@link parseSseStream} framer
212
+ * (ADR-0060 rule 5). Translates each SSE data payload into 0..N
213
+ * {@link AiEvent}s: text_delta per chunk, tool_call aggregated per
214
+ * index / block, exactly one done (or error) at the end.
215
+ */
65
216
  private static streamRequest;
217
+ private static responseChunks;
218
+ private static streamError;
66
219
  }
@@ -4,6 +4,38 @@ export interface ApiResult {
4
4
  headers: Record<string, string>;
5
5
  error: string | null;
6
6
  }
7
+ /**
8
+ * Options for the streaming primitives ({@link Api.streamBytes},
9
+ * {@link Api.streamLines}, {@link Api.streamSse}). All fields optional.
10
+ * `timeout` bounds the WHOLE stream (headers + body), matching
11
+ * `TINA4_API_TIMEOUT`; `connectTimeout` bounds only the connection +
12
+ * headers-arrival phase, matching `TINA4_API_CONNECT_TIMEOUT`.
13
+ */
14
+ export interface StreamOptions {
15
+ method?: string;
16
+ body?: unknown;
17
+ headers?: Record<string, string>;
18
+ contentType?: string;
19
+ timeout?: number;
20
+ connectTimeout?: number;
21
+ }
22
+ /**
23
+ * One SSE event yielded by {@link Api.streamSse}. `data` is always present
24
+ * (multi-line `data:` fields are concatenated with `\n`). `event`, `id`,
25
+ * `retry` are set only when the corresponding SSE field appeared. `retry`
26
+ * is a number (milliseconds) per the SSE spec.
27
+ */
28
+ export interface SseEvent {
29
+ data: string;
30
+ event?: string;
31
+ id?: string;
32
+ retry?: number;
33
+ }
34
+ /** Raised by the streaming primitives on a non-2xx status. */
35
+ export declare class ApiStreamError extends Error {
36
+ readonly status: number | null;
37
+ constructor(message: string, status?: number | null);
38
+ }
7
39
  /**
8
40
  * Result of {@link Api.download}. There is no `body` field — the response
9
41
  * body went to disk. `path` is the destination on success and `null` on any
@@ -90,6 +122,28 @@ export interface ApiOptions {
90
122
  */
91
123
  cookies?: boolean;
92
124
  }
125
+ /**
126
+ * Split an async byte iterable into UTF-8 lines. Handles LF and CRLF; a
127
+ * multibyte codepoint that lands across a chunk boundary buffers across the
128
+ * split (TextDecoder({stream: true})). A trailing line without a terminator
129
+ * yields on EOF.
130
+ *
131
+ * Exported so {@link Api} instance methods AND `Ai.chat` streaming share
132
+ * one framer — ADR-0060's "no duplicate framing code" rule.
133
+ */
134
+ export declare function parseLineStream(chunks: AsyncIterable<Uint8Array>): AsyncGenerator<string>;
135
+ /**
136
+ * Parse SSE (Server-Sent Events) framing from a line iterable. Yields
137
+ * one {@link SseEvent} per event boundary (blank line) or on EOF for a
138
+ * final trailing event. `:` comment lines are ignored. Fields are
139
+ * `data` (multi-line concatenated with `\n`), `event`, `id`, `retry`.
140
+ *
141
+ * Follows the WHATWG SSE parsing algorithm closely enough for every LLM
142
+ * provider (OpenAI, Anthropic, local): one leading space after the colon
143
+ * is stripped, unknown fields are ignored, malformed `retry:` values are
144
+ * ignored.
145
+ */
146
+ export declare function parseSseStream(lines: AsyncIterable<string>): AsyncGenerator<SseEvent>;
93
147
  export declare class Api {
94
148
  private baseUrl;
95
149
  private headers;
@@ -209,6 +263,54 @@ export declare class Api {
209
263
  * not written on error.
210
264
  */
211
265
  download(path: string, destPath: string, params?: Record<string, string>): Promise<DownloadResult>;
266
+ /**
267
+ * Stream a response body as raw bytes. Yields the chunks the transport
268
+ * delivers, in order, never buffered whole. Ends cleanly on EOF and
269
+ * throws on a transport failure or a non-2xx status (body drained
270
+ * first). No JSON decoding, no line splitting, no framing —
271
+ * {@link streamLines} and {@link streamSse} build on this primitive.
272
+ *
273
+ * Closing the iterator before EOF (a `break` out of a `for await`)
274
+ * destroys the underlying socket, so a caller who takes only the
275
+ * first few chunks never leaks the connection.
276
+ *
277
+ * `opts.timeout` bounds the whole stream duration (default
278
+ * `TINA4_API_TIMEOUT` or the client `timeout`); `opts.connectTimeout`
279
+ * bounds just the connect + headers phase (default
280
+ * `TINA4_API_CONNECT_TIMEOUT` or 10s).
281
+ */
282
+ streamBytes(path: string, opts?: StreamOptions): AsyncGenerator<Uint8Array>;
283
+ /**
284
+ * Stream the response body as UTF-8 lines. Splits on LF or CRLF;
285
+ * buffers a multibyte codepoint that lands across a chunk boundary;
286
+ * yields a trailing line without a terminator on EOF. Built on
287
+ * {@link streamBytes} plus the shared {@link parseLineStream}.
288
+ */
289
+ streamLines(path: string, opts?: StreamOptions): AsyncGenerator<string>;
290
+ /**
291
+ * Stream the response as SSE (Server-Sent Events). Yields one
292
+ * {@link SseEvent} per event boundary (blank line) or on EOF for a
293
+ * trailing event. `data:[DONE]` is delivered as an ordinary event
294
+ * with `data === "[DONE]"` and the iterator ends on the next EOF.
295
+ * Built on {@link streamLines} plus the shared {@link parseSseStream}.
296
+ */
297
+ streamSse(path: string, opts?: StreamOptions): AsyncGenerator<SseEvent>;
298
+ /**
299
+ * Resolve a stream duration from (in order): explicit `opts` field,
300
+ * the named env var, then the fallback. Zero disables. A non-numeric
301
+ * or negative env value warns via a fallback rather than throwing —
302
+ * a bad env var must not brick every streaming call.
303
+ */
304
+ private streamSeconds;
305
+ /**
306
+ * Open a streaming HTTP request. Returns the raw
307
+ * {@link http.IncomingMessage} once headers arrive. Redirects are NOT
308
+ * followed on streams (a caller who needs a redirect should do a
309
+ * regular GET first). Connect phase is bounded by `connectSec`;
310
+ * body-phase timeout is applied by the caller (streamBytes) via
311
+ * `res.destroy()`.
312
+ */
313
+ private openStreamRequest;
212
314
  private buildUrl;
213
315
  /**
214
316
  * Build the request headers (default User-Agent + auth + cookie jar +
@@ -39,8 +39,8 @@ export { ServiceRunner, Tina4Service, matchCronField, matchesCron } from "./serv
39
39
  export type { ServiceOptions, ServiceContext, ServiceHandler, ServiceInfo } from "./service.js";
40
40
  export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, sweep, createBackend, _resetBackend } from "./cache.js";
41
41
  export type { ResponseCacheConfig, CacheBackend } from "./cache.js";
42
- export { Api } from "./api.js";
43
- export type { ApiResult, ApiOptions, ApiTransport, DownloadResult, UploadOptions } from "./api.js";
42
+ export { Api, ApiStreamError, parseLineStream, parseSseStream } from "./api.js";
43
+ export type { ApiResult, ApiOptions, ApiTransport, DownloadResult, UploadOptions, StreamOptions, SseEvent } from "./api.js";
44
44
  export { Context, defaultContext, existingContext, fts5Supported, _sharedContexts } from "./context/index.js";
45
45
  export type { SearchHit } from "./context/index.js";
46
46
  export { Events } from "./events.js";
@@ -58,7 +58,7 @@ export type { AiTool } from "./ai.js";
58
58
  export { Sso, SSO, SsoError } from "./sso.js";
59
59
  export type { SsoOptions } from "./sso.js";
60
60
  export { Ai, AiError, AiConfigError, AiHTTPError, AiTimeoutError, AiParseError } from "./aiClient.js";
61
- export type { ChatResponse, AiMessage, AiChatOptions, AiEmbedOptions } from "./aiClient.js";
61
+ export type { ChatResponse, AiMessage, AiChatOptions, AiEmbedOptions, AiEvent, ContentPart, AiMessageContent, AiToolDeclaration, AiToolChoice } from "./aiClient.js";
62
62
  export type { ImapMessage, ImapFullMessage, ImapAttachment } from "./messenger.js";
63
63
  export { LiteBackend } from "./queueBackends/liteBackend.js";
64
64
  export { RabbitMQBackend, parseAmqpUrl } from "./queueBackends/rabbitmqBackend.js";