langfx.js 0.1.0-alpha.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.
Files changed (104) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +107 -0
  4. package/dist/agentic.d.ts +128 -0
  5. package/dist/agentic.js +265 -0
  6. package/dist/agentic.js.map +1 -0
  7. package/dist/cache.d.ts +32 -0
  8. package/dist/cache.js +135 -0
  9. package/dist/cache.js.map +1 -0
  10. package/dist/cancellation.d.ts +9 -0
  11. package/dist/cancellation.js +70 -0
  12. package/dist/cancellation.js.map +1 -0
  13. package/dist/errors.d.ts +40 -0
  14. package/dist/errors.js +44 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/index.d.ts +18 -0
  17. package/dist/index.js +15 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/json-stream.d.ts +73 -0
  20. package/dist/json-stream.js +222 -0
  21. package/dist/json-stream.js.map +1 -0
  22. package/dist/langfunc.d.ts +20 -0
  23. package/dist/langfunc.js +28 -0
  24. package/dist/langfunc.js.map +1 -0
  25. package/dist/language-model.d.ts +78 -0
  26. package/dist/language-model.js +218 -0
  27. package/dist/language-model.js.map +1 -0
  28. package/dist/llms/anthropic.d.ts +30 -0
  29. package/dist/llms/anthropic.js +365 -0
  30. package/dist/llms/anthropic.js.map +1 -0
  31. package/dist/llms/gemini.d.ts +34 -0
  32. package/dist/llms/gemini.js +380 -0
  33. package/dist/llms/gemini.js.map +1 -0
  34. package/dist/llms/images.d.ts +10 -0
  35. package/dist/llms/images.js +43 -0
  36. package/dist/llms/images.js.map +1 -0
  37. package/dist/llms/index.d.ts +7 -0
  38. package/dist/llms/index.js +4 -0
  39. package/dist/llms/index.js.map +1 -0
  40. package/dist/llms/openai.d.ts +30 -0
  41. package/dist/llms/openai.js +398 -0
  42. package/dist/llms/openai.js.map +1 -0
  43. package/dist/llms/transport.d.ts +4 -0
  44. package/dist/llms/transport.js +90 -0
  45. package/dist/llms/transport.js.map +1 -0
  46. package/dist/mapping.d.ts +71 -0
  47. package/dist/mapping.js +190 -0
  48. package/dist/mapping.js.map +1 -0
  49. package/dist/message.d.ts +56 -0
  50. package/dist/message.js +86 -0
  51. package/dist/message.js.map +1 -0
  52. package/dist/python-preview.d.ts +24 -0
  53. package/dist/python-preview.js +368 -0
  54. package/dist/python-preview.js.map +1 -0
  55. package/dist/python-stream.d.ts +15 -0
  56. package/dist/python-stream.js +29 -0
  57. package/dist/python-stream.js.map +1 -0
  58. package/dist/python.d.ts +85 -0
  59. package/dist/python.js +728 -0
  60. package/dist/python.js.map +1 -0
  61. package/dist/query.d.ts +31 -0
  62. package/dist/query.js +151 -0
  63. package/dist/query.js.map +1 -0
  64. package/dist/retry.d.ts +12 -0
  65. package/dist/retry.js +56 -0
  66. package/dist/retry.js.map +1 -0
  67. package/dist/schema/zod.d.ts +4 -0
  68. package/dist/schema/zod.js +7 -0
  69. package/dist/schema/zod.js.map +1 -0
  70. package/dist/schema.d.ts +10 -0
  71. package/dist/schema.js +7 -0
  72. package/dist/schema.js.map +1 -0
  73. package/dist/template.d.ts +14 -0
  74. package/dist/template.js +75 -0
  75. package/dist/template.js.map +1 -0
  76. package/dist/testing/index.d.ts +33 -0
  77. package/dist/testing/index.js +56 -0
  78. package/dist/testing/index.js.map +1 -0
  79. package/dist/tool-call.d.ts +27 -0
  80. package/dist/tool-call.js +16 -0
  81. package/dist/tool-call.js.map +1 -0
  82. package/dist/tools.d.ts +43 -0
  83. package/dist/tools.js +155 -0
  84. package/dist/tools.js.map +1 -0
  85. package/docs/ANTHROPIC.md +43 -0
  86. package/docs/API_DESIGN.md +200 -0
  87. package/docs/GEMINI.md +68 -0
  88. package/docs/IMPLEMENTATION_STATUS.md +77 -0
  89. package/docs/LIVE_TESTING.md +24 -0
  90. package/docs/MAPPING.md +54 -0
  91. package/docs/OPENAI.md +40 -0
  92. package/docs/PORTING_PLAN.md +135 -0
  93. package/docs/PROMPT_PARITY.md +379 -0
  94. package/docs/PYTHON_PROTOCOL.md +109 -0
  95. package/docs/PYTHON_PROTOCOL_PARITY.md +964 -0
  96. package/docs/PYTHON_SCHEMA_EVALUATION.md +93 -0
  97. package/docs/PYTHON_STREAMING_PARITY.md +59 -0
  98. package/docs/RELEASING.md +25 -0
  99. package/docs/RETRIES_AND_CACHE.md +56 -0
  100. package/docs/SESSION_EVENTS.md +40 -0
  101. package/docs/SOURCE_AUDIT.md +133 -0
  102. package/docs/STREAMING.md +76 -0
  103. package/docs/TOOL_STREAMING.md +31 -0
  104. package/package.json +95 -0
@@ -0,0 +1,27 @@
1
+ /** A complete model-requested call. Arguments still require tool validation. */
2
+ export declare class ToolCall {
3
+ readonly id: string;
4
+ readonly name: string;
5
+ readonly args: Readonly<Record<string, unknown>>;
6
+ constructor(id: string, name: string, args: Readonly<Record<string, unknown>>);
7
+ }
8
+ export interface ToolDeclaration {
9
+ readonly name: string;
10
+ readonly description: string;
11
+ readonly inputSchema: Readonly<Record<string, unknown>>;
12
+ }
13
+ /** Display-only events. Completed arguments still require registered-tool validation.
14
+ * Wait for the successful final stream chunk before executing any calls.
15
+ */
16
+ export type ToolCallStreamEvent = {
17
+ readonly type: 'toolCallStart';
18
+ readonly id: string;
19
+ readonly name: string;
20
+ } | {
21
+ readonly type: 'toolCallDelta';
22
+ readonly id: string;
23
+ readonly delta: string;
24
+ } | {
25
+ readonly type: 'toolCallEnd';
26
+ readonly call: ToolCall;
27
+ };
@@ -0,0 +1,16 @@
1
+ /** A complete model-requested call. Arguments still require tool validation. */
2
+ export class ToolCall {
3
+ id;
4
+ name;
5
+ args;
6
+ constructor(id, name, args) {
7
+ this.id = id;
8
+ this.name = name;
9
+ this.args = args;
10
+ if (!id || !name)
11
+ throw new TypeError('Tool calls require an ID and name.');
12
+ if (!args || typeof args !== 'object' || Array.isArray(args))
13
+ throw new TypeError('Tool arguments must be an object.');
14
+ }
15
+ }
16
+ //# sourceMappingURL=tool-call.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-call.js","sourceRoot":"","sources":["../src/tool-call.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,OAAO,QAAQ;IAER,EAAE;IACF,IAAI;IACJ,IAAI;IAHf,YACW,EAAU,EACV,IAAY,EACZ,IAAuC;kBAFvC,EAAE;oBACF,IAAI;oBACJ,IAAI;QAEb,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,SAAS,CAAC,oCAAoC,CAAC,CAAC;QAC5E,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,mCAAmC,CAAC,CAAC;IACzH,CAAC;CACF"}
@@ -0,0 +1,43 @@
1
+ import { Action, Session } from './agentic.js';
2
+ import type { RunOptions } from './agentic.js';
3
+ import type { Schema } from './schema.js';
4
+ import type { ToolCall, ToolDeclaration } from './tool-call.js';
5
+ export interface ToolOptions<I, O> {
6
+ readonly name: string;
7
+ readonly description: string;
8
+ readonly input: Schema<I>;
9
+ /** Agent sends validated outputs as JSON; use plain objects/arrays, not Set, Map or class instances. */
10
+ readonly output: Schema<O>;
11
+ readonly execute: (input: I, session: Session) => Promise<O>;
12
+ }
13
+ export interface RegisteredTool {
14
+ readonly declaration: ToolDeclaration;
15
+ prepare(args: unknown): (session: Session) => Promise<unknown>;
16
+ }
17
+ export declare class Tool<I, O> implements RegisteredTool {
18
+ private readonly options;
19
+ readonly declaration: ToolDeclaration;
20
+ constructor(options: ToolOptions<I, O>);
21
+ /** Validate before any app function runs; returned closure holds validated args. */
22
+ prepare(args: unknown): (session: Session) => Promise<O>;
23
+ }
24
+ export interface AgentOptions {
25
+ readonly prompt: string;
26
+ readonly instructions?: string;
27
+ readonly tools?: readonly RegisteredTool[];
28
+ readonly limits?: {
29
+ readonly maxSteps?: number;
30
+ readonly maxToolCalls?: number;
31
+ readonly maxExecutionTime?: number;
32
+ };
33
+ readonly allowTool?: (call: ToolCall, session: Session) => boolean | Promise<boolean>;
34
+ }
35
+ export declare class Agent extends Action<string> {
36
+ private readonly options;
37
+ private readonly tools;
38
+ private readonly maxSteps;
39
+ private readonly maxToolCalls;
40
+ constructor(options: AgentOptions);
41
+ invoke(session?: Session, options?: RunOptions): Promise<string>;
42
+ call(session: Session): Promise<string>;
43
+ }
package/dist/tools.js ADDED
@@ -0,0 +1,155 @@
1
+ import { Action, Session } from './agentic.js';
2
+ import { checkCancelled } from './cancellation.js';
3
+ import { BudgetExceededError, EmptyGenerationError, ToolDeniedError, ToolExecutionError } from './errors.js';
4
+ import { ComplexMessage, SystemMessage, ToolMessage, UserMessage } from './message.js';
5
+ export class Tool {
6
+ options;
7
+ declaration;
8
+ constructor(options) {
9
+ this.options = options;
10
+ if (!/^[A-Za-z_][A-Za-z0-9_-]{0,63}$/.test(options.name))
11
+ throw new TypeError('Invalid tool name.');
12
+ this.declaration = Object.freeze({ name: options.name, description: options.description, inputSchema: options.input.jsonSchema });
13
+ }
14
+ /** Validate before any app function runs; returned closure holds validated args. */
15
+ prepare(args) {
16
+ let input;
17
+ try {
18
+ input = this.options.input.parse(args);
19
+ }
20
+ catch (cause) {
21
+ throw new ToolExecutionError(`Invalid arguments for tool ${this.declaration.name}.`, { cause });
22
+ }
23
+ return async (session) => {
24
+ checkCancelled(session.signal);
25
+ try {
26
+ const output = this.options.output.parse(await this.options.execute(input, session));
27
+ checkCancelled(session.signal);
28
+ return output;
29
+ }
30
+ catch (cause) {
31
+ checkCancelled(session.signal);
32
+ throw new ToolExecutionError(`Tool ${this.declaration.name} failed.`, { cause });
33
+ }
34
+ };
35
+ }
36
+ }
37
+ class ExecuteTool extends Action {
38
+ toolName;
39
+ execute;
40
+ constructor(toolName, execute) {
41
+ super();
42
+ this.toolName = toolName;
43
+ this.execute = execute;
44
+ }
45
+ get name() { return `Tool:${this.toolName}`; }
46
+ call(session) { return this.execute(session); }
47
+ }
48
+ export class Agent extends Action {
49
+ options;
50
+ tools;
51
+ maxSteps;
52
+ maxToolCalls;
53
+ constructor(options) {
54
+ super();
55
+ this.options = options;
56
+ this.maxSteps = options.limits?.maxSteps ?? 8;
57
+ this.maxToolCalls = options.limits?.maxToolCalls ?? 12;
58
+ if (!Number.isInteger(this.maxSteps) || this.maxSteps < 1 || !Number.isInteger(this.maxToolCalls) || this.maxToolCalls < 0) {
59
+ throw new RangeError('Agent limits require positive maxSteps and non-negative maxToolCalls.');
60
+ }
61
+ const tools = options.tools ?? [];
62
+ this.tools = new Map(tools.map(tool => [tool.declaration.name, tool]));
63
+ if (this.tools.size !== tools.length)
64
+ throw new TypeError('Tool names must be unique.');
65
+ }
66
+ invoke(session, options = {}) {
67
+ const configured = this.options.limits?.maxExecutionTime ?? 60;
68
+ const seconds = Math.min(configured, options.maxExecutionTime ?? configured);
69
+ return super.invoke(session, { ...options, maxExecutionTime: seconds });
70
+ }
71
+ async call(session) {
72
+ const messages = [];
73
+ if (this.options.instructions)
74
+ messages.push(new SystemMessage(this.options.instructions));
75
+ messages.push(new UserMessage(this.options.prompt));
76
+ const usedIds = new Set();
77
+ let calls = 0;
78
+ for (let step = 0; step < this.maxSteps; step++) {
79
+ checkCancelled(session.signal);
80
+ const response = await session.query(new ComplexMessage(messages), {
81
+ tools: [...this.tools.values()].map(tool => tool.declaration), returnsMessage: true,
82
+ });
83
+ messages.push(response);
84
+ if (!response.toolCalls.length) {
85
+ if (!response.text)
86
+ throw new EmptyGenerationError('Agent received neither text nor tool calls.');
87
+ checkCancelled(session.signal);
88
+ return response.text;
89
+ }
90
+ // Reserve the entire batch and validate it before allowing side effects.
91
+ if (step + 1 >= this.maxSteps)
92
+ throw new BudgetExceededError('No model step remains to consume tool results.');
93
+ if (calls + response.toolCalls.length > this.maxToolCalls)
94
+ throw new BudgetExceededError('Agent tool-call limit exceeded.');
95
+ const prepared = response.toolCalls.map(call => {
96
+ if (usedIds.has(call.id))
97
+ throw new ToolExecutionError(`Duplicate tool-call ID: ${call.id}`);
98
+ usedIds.add(call.id);
99
+ const tool = this.tools.get(call.name);
100
+ if (!tool)
101
+ throw new ToolExecutionError(`Unknown tool: ${call.name}`);
102
+ return { call, execute: tool.prepare(call.args) };
103
+ });
104
+ for (const { call } of prepared) {
105
+ if (this.options.allowTool && !await this.options.allowTool(call, session))
106
+ throw new ToolDeniedError(`Tool denied: ${call.name}`);
107
+ checkCancelled(session.signal);
108
+ }
109
+ calls += prepared.length;
110
+ for (const { call, execute } of prepared) {
111
+ const output = await new ExecuteTool(call.name, execute).invoke(session);
112
+ let text;
113
+ try {
114
+ validateJsonOutput(output);
115
+ text = JSON.stringify(output, (_key, value) => {
116
+ if (value === undefined || typeof value === 'function' || typeof value === 'symbol'
117
+ || typeof value === 'bigint' || typeof value === 'number' && !Number.isFinite(value)) {
118
+ throw new TypeError('Tool output contains a non-JSON value.');
119
+ }
120
+ return value;
121
+ });
122
+ }
123
+ catch (cause) {
124
+ throw new ToolExecutionError('Tool output is not JSON serializable.', { cause });
125
+ }
126
+ if (text === undefined)
127
+ throw new ToolExecutionError('Tool output must be JSON serializable.');
128
+ messages.push(new ToolMessage(text, call.id, { metadata: { toolName: call.name } }));
129
+ }
130
+ }
131
+ throw new BudgetExceededError('Agent model-step limit exceeded.');
132
+ }
133
+ }
134
+ /** Reject containers that JSON.stringify would silently erase or transform. */
135
+ function validateJsonOutput(value, ancestors = new Set()) {
136
+ if (value === null || typeof value === 'string' || typeof value === 'boolean'
137
+ || typeof value === 'number' && Number.isFinite(value))
138
+ return;
139
+ if (typeof value !== 'object')
140
+ throw new TypeError('Tool output contains a non-JSON value.');
141
+ const prototype = Object.getPrototypeOf(value);
142
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
143
+ throw new TypeError('Tool output requires plain JSON objects and arrays.');
144
+ }
145
+ if (ancestors.has(value))
146
+ throw new TypeError('Tool output contains a cycle.');
147
+ if (Object.getOwnPropertySymbols(value).some(key => Object.prototype.propertyIsEnumerable.call(value, key))) {
148
+ throw new TypeError('Tool output contains a symbol key.');
149
+ }
150
+ ancestors.add(value);
151
+ for (const child of Array.isArray(value) ? value : Object.values(value))
152
+ validateJsonOutput(child, ancestors);
153
+ ancestors.delete(value);
154
+ }
155
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ # Anthropic Messages adapter
2
+
3
+ `Anthropic extends LanguageModel` is available through `lf.llms.Anthropic`, `langfx.js/llms`, and `langfx.js/llms/anthropic`. It implements text calls, complete client tools/results, and text/tool SSE streaming. Tests and browser demos use mocked HTTP, not live inference.
4
+
5
+ ```ts
6
+ import { Anthropic } from 'langfx.js/llms/anthropic';
7
+
8
+ const lm = new Anthropic({
9
+ model: 'your-model-id',
10
+ apiKey: async signal => obtainAppCredential(signal), // application-owned
11
+ maxTokens: 4096,
12
+ });
13
+ const message = await lm.call('Explain closures.');
14
+ for await (const chunk of lm.stream('Explain generators.')) {
15
+ console.log(chunk.delta.text);
16
+ }
17
+ ```
18
+
19
+ The default endpoint is `https://api.anthropic.com/v1/messages`. `baseUrl` overrides the version root and `transport` accepts the same injected fetch-like interface as Gemini. No environment variables or global credentials are read. A supplied API key is sent in `x-api-key`; the version header is `2023-06-01`. `maxTokens` defaults to 4096. Omitting a key supports a host-authenticated gateway/transport. Direct browser requests depend on the endpoint's CORS/auth configuration; this adapter does not enable a browser-access override header automatically. The fixture browser demo makes no external requests. See Anthropic's [API overview](https://platform.claude.com/docs/en/api/overview).
20
+
21
+ Leading system sections become top-level `system` blocks. Adjacent equal-role turns are merged. Client tools use `input_schema`, `tool_use`, and immediately following user `tool_result` blocks. IDs must correlate and every pending call must have a result before another conversation turn. Tool choice supports auto, any, none, or a registered name. Raw supported response blocks are retained in scoped metadata so their ordering survives caching and application-owned transcripts. Reuse of continuation data across a different model or endpoint rejects. This follows the [tool-result protocol](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls).
22
+
23
+ SSE parsing handles fragmented UTF-8 and line boundaries. The adapter checks message/block event order, processes cumulative usage, and requires a complete message_stop before final success. Unknown top-level event types are ignored for forward compatibility; unsupported block/delta types reject. Early exit and failures close the reader. The base model retries only eligible failures before any chunk is emitted. See [stream event definitions](https://platform.claude.com/docs/en/build-with-claude/streaming).
24
+
25
+ Authentication errors, rate limits, and transient server errors map to the shared error classes. Truncation rejects with IncompleteGenerationError; refusal rejects with ContentFilteredError. Provider error bodies are not exposed in error messages. In-band overload/rate errors retain classification. Generic transport errors remain unclassified and do not automatically retry.
26
+
27
+ Usage totals include input_tokens, cache_creation_input_tokens, and cache_read_input_tokens, plus output_tokens. Missing totals stay unknown. These counts do not imply a single token price or include unreported failed-attempt usage. See Anthropic's [prompt-cache usage fields](https://platform.claude.com/docs/en/build-with-claude/prompt-caching).
28
+
29
+ ## Current limits
30
+
31
+ - Images are supported only in user messages; other modalities reject before transport.
32
+ - Prompt-based JSON queries and structured JSON field streaming work through the existing Mapping/parser. Native structured output is not yet implemented and rejects explicitly.
33
+ - Complete client tool calls work with Agent and explicit conversation transcripts. Tool streaming emits start, argument-delta, and completion events; the final aggregated message preserves complete tool calls and continuation blocks. See [tool streaming](TOOL_STREAMING.md).
34
+ - Thinking/redacted-thinking blocks, citations deltas, server tools, managed agents, automatic continuation on pause_turn, and cloud-platform authentication are not implemented. Unsupported response blocks never silently degrade to text.
35
+ - Model availability, live CORS/auth behavior, and model-specific option compatibility remain unverified. No model ID is hardcoded as a default.
36
+
37
+ Run `npm run check` for fixture contracts and `npm run serve:example` for the window/worker mocked tool-loop demo. Live-provider testing remains a separate step requiring application-provided credentials.
38
+
39
+ ## Image inputs
40
+
41
+ User messages accept ordered text and `Image` parts, including templates and `LfQuery`. HTTP(S) URI images become `image` blocks with URL sources; bytes and Blob/File images use base64 sources with explicit media types. The adapter does not download URL images. See the [Anthropic vision protocol](https://platform.claude.com/docs/en/build-with-claude/vision).
42
+
43
+ Inline images require PNG, JPEG, GIF, or WebP MIME types and 1 byte–5 MiB per image (a conservative local adapter bound, not a model limit). Blob reads observe cancellation. Other modalities and images in system, assistant, or tool messages reject. Image contents are not decoded or validated locally; model support and provider limits still apply. Image inputs also work with text output streaming. Tests use mocked transport, not live inference.
@@ -0,0 +1,200 @@
1
+ # Proposed TypeScript API
2
+
3
+ This document describes the target API. The core, Gemini adapter, bounded Agent loop, structured JSON field streams, opt-in retries, and an in-memory cache are implemented; other providers and other future examples remain design sketches. See [implementation status](IMPLEMENTATION_STATUS.md) and the runnable examples in the README. **Python API consistency is a primary design goal.** Keep the same public concepts, class hierarchy, construction style, method responsibilities, and default return values wherever JavaScript permits. Classes are the primary API, not a later convenience layer.
4
+
5
+ The implementation can use plain data, explicit schemas, and invocation-local state internally without exposing a different client/factory programming model. No separate `createClient` is required: a `LanguageModel` object is already the model abstraction.
6
+
7
+ ## Python-to-TypeScript mapping
8
+
9
+ | Python | Proposed TypeScript | Reason for any difference |
10
+ | --- | --- | --- |
11
+ | `lf.llms.Gemini(model=...)` | `new lf.llms.Gemini({ model })` | JavaScript constructor/options syntax |
12
+ | `lf.LanguageModel` subclass | `class CustomModel extends lf.LanguageModel` | Preserve inheritance and custom-provider extension |
13
+ | `lf.query(prompt, Schema, lm=lm)` | `await lf.query(prompt, Schema, { lm })` | Same positional schema and direct typed result; async I/O |
14
+ | `lf.query(prompt, lm=lm)` | `await lf.query(prompt, { lm })` | Unstructured overload returns text |
15
+ | `returns_message=True` | `returnsMessage: true` | Optional message/result metadata, not a mandatory envelope |
16
+ | `lf.Template("Hello {{name}}")` | `new lf.Template("Hello {{name}}")` | Keep basic template syntax; full Jinja is out of scope |
17
+ | `lf.LangFunc(...)` | `new lf.LangFunc(...)` / subclass | Keep the prompt → model → transform pipeline |
18
+ | `lm(prompt)` / `langfunc(...)` | `await lm.call(prompt)` / `await langfunc.call(...)` | Ordinary JavaScript class instances are not callable |
19
+ | `lm.sample(prompts)` / `lm.stream(prompt)` | `await lm.sample(prompts)` / `lm.stream(prompt)` | Promise results and async iteration |
20
+ | `lf.query_stream(...)` | `lf.queryStream(...)` | Mechanical snake_case → camelCase naming |
21
+ | `lf.Action` with `async call(session, ...)` | `class X extends lf.Action<T>` with `async call(session, ...)` | Preserve the implementation hook |
22
+ | `await action(session, ...)` | `await action.invoke(session, ...)` | Separate public lifecycle wrapper from the overridden `call` hook |
23
+ | `lf.Session(...)` | `new lf.Session(...)` | Preserve session construction, queries, events, and traces |
24
+ | `lf.UserMessage(...)`, `lf.Image.from_uri(...)` | `new lf.UserMessage(...)`, `lf.Image.fromUri(...)` | Preserve public message/modality classes |
25
+ | `lf.context(...)` | Model/session defaults and explicit overrides | Deliberate browser concurrency boundary; no mutable ambient context |
26
+ | `pg.Object` schema inferred from annotations | Explicit `lf.Schema<T>` or class with declared schema | TypeScript annotations alone supply no runtime validator |
27
+
28
+ Use camelCase consistently for multiword methods/options (`maxTokens`, `cacheSeed`, `systemMessage`). Keep `lm`, `schema`, and existing class names. One async API replaces Python's sync/async pairs; synchronous wrappers and `a`-prefixed duplicates are deliberately excluded. These naming and context choices are design decisions, not requirements of TypeScript.
29
+
30
+ ## Execution model: one async API for I/O
31
+
32
+ **Design decision:** keep Python's familiar unprefixed names, with asynchronous semantics for operations that may perform I/O. Do not port synchronous wrappers, blocking bridges, or duplicate `a`-prefixed methods/functions. This applies to public APIs and protected provider hooks.
33
+
34
+ | Python pair | TypeScript API | Return contract |
35
+ | --- | --- | --- |
36
+ | `LanguageModel.__call__` / `acall` | `lm.call(...)` | `Promise<Message>` |
37
+ | `sample` / `asample` | `lm.sample(...)` | `Promise<SamplingResult[]>` |
38
+ | `stream` / `astream` | `lm.stream(...)` | `AsyncIterable<StreamChunk>` |
39
+ | `lf.query` / `lf.aquery` | `lf.query(...)` | `Promise<T>` (or text/message according to the overload) |
40
+ | `lf.query_stream` / `lf.aquery_stream` | `lf.queryStream(...)` | `AsyncIterable` of text or structured events |
41
+ | `LangFunc.__call__` / `acall` | `langFunc.call(...)` | `Promise<Message>` |
42
+ | `Session.query` / `Session.aquery` | `session.query(...)` | Same Promise overloads as `lf.query` |
43
+
44
+ Call Promise-returning methods with `await`; consume streams with `for await...of`. Stream methods return the async iterable directly, not a Promise of an iterable. Model calls retain these contracts even for a cache hit or a fake/local model. Protected `_sample` returns a Promise and `_stream` returns an async iterable; neither has a separate synchronous or `a`-prefixed counterpart.
45
+
46
+ JavaScript local computation is still synchronous. Constructors, message manipulation, `Template.render`, local JSON parsing, and `Schema.parse` return ordinary values and throw ordinary errors. In contrast, Langfx's LLM-assisted `parse` helper will be async when ported, because it calls a model. Media loading and persistent storage are async where they perform I/O; creating a URI descriptor is synchronous.
47
+
48
+ `Action.invoke` and its `call` hook, tool execution, and future embedding/MCP/evaluation operations follow the same async I/O rule. Do not introduce a second naming family for local implementations of those interfaces. Async does not imply parallel execution; concurrency remains explicitly controlled by the caller or session.
49
+
50
+ ## Model construction and extension
51
+
52
+ ```ts
53
+ import * as lf from 'langfx.js';
54
+
55
+ // modelId and transport are supplied by the application.
56
+ const lm = new lf.llms.Gemini({ model: modelId, transport });
57
+ const answer = await lf.query('What is 1 + 1?', { lm }); // string
58
+ const response = await lm.call('What is 1 + 1?'); // Message
59
+ console.log(answer, response.text);
60
+ ```
61
+
62
+ `OpenAI`, `Anthropic`, and `Gemini` extend `LanguageModel`. The base owns normalization, cache lookup, retries, usage, and cancellation. Providers implement protected async `_sample` and `_stream` hooks; callers use public `call`, `sample`, and `stream`. The initial retry/cache options and deliberate scope differences are documented in [retries and cache](RETRIES_AND_CACHE.md). This mirrors Python's base-class/template-method organization while removing synchronous bridging. A minimal custom model implements `_sample`; unsupported streaming is reported explicitly unless one-event emulation is requested.
63
+
64
+ Keep `SamplingOptions`, `SamplingResult`, `Sample`, `StreamChunk`, `ToolCall`, and the error hierarchy recognizable. Provider option types extend the common sampling options. Constructors accept an options object; flat sampling options may be routed to a typed options value just as in Python. Call-time overrides never mutate shared model defaults.
65
+
66
+ `sample` retains a list-of-prompts input and list-of-results output. P0 can limit each prompt to one candidate while preserving the result structure. Capabilities describe supported modalities, tools, schemas, and streaming. Unsupported requested capabilities fail clearly.
67
+
68
+ Offer `lf.llms` namespace access and direct imports such as `langfx.js/llms/gemini`. Root namespace exports must stay free of registration/network side effects and pass a single-provider tree-shaking check. Heavy optional/native adapters remain separate imports. Named model conveniences can be thin subclasses later; omitting a large historical model catalog does not require omitting provider classes. Defer `LanguageModel.get` until explicit provider registration/resolution is designed.
69
+
70
+ ## Typed queries and runtime schemas
71
+
72
+ ```ts
73
+ import * as lf from 'langfx.js';
74
+ import { fromZod } from 'langfx.js/schema/zod';
75
+ import { z } from 'zod';
76
+
77
+ const ImageDescription = fromZod(z.object({
78
+ items: z.array(z.object({ name: z.string(), color: z.string() })),
79
+ }));
80
+
81
+ const image = lf.Image.fromFile(file);
82
+ const desc = await lf.query(
83
+ 'Describe objects in {{image}} from top to bottom.',
84
+ ImageDescription,
85
+ { lm, vars: { image }, signal: controller.signal },
86
+ );
87
+ renderItems(desc.items); // Direct typed value, like Python lf.query.
88
+
89
+ const message = await lf.query('Describe {{image}}.', ImageDescription, {
90
+ lm, vars: { image }, returnsMessage: true,
91
+ });
92
+ renderItems(message.result.items);
93
+ ```
94
+
95
+ The optional Zod adapter is implemented through the separate `langfx.js/schema/zod` entry point. `Schema<T>` pairs a model-facing JSON Schema with `parse(value: unknown): T`. It is an adapter contract, not a new schema language. `query(prompt, schema, options)` returns `Promise<T>`; without a schema it returns `Promise<string>`. The `returnsMessage: true` overload returns `Promise<Message<T>>` with a validated `result`, provenance, and available usage metadata. Detailed invocation tracking is available through Session; ordinary queries do not require `.value` access.
96
+
97
+ A schema-bearing class can be accepted through an explicit `static schema: Schema<MyClass>` whose validator constructs a class instance. This preserves object-oriented output where desired without inferring field schemas from erased annotations, requiring decorators, or importing a symbolic object system. Generic `query<MyClass>(...)` alone does not establish validation. Dynamic MCP schemas produce `unknown` until validated/refined by application code.
98
+
99
+ Both native structured output and prompt-JSON output use the same final validator. Retain Python's `nativeStructuredOutput` option: false selects prompt-based output (Python by default, JSON with `protocol: 'json'`), true requires provider-native support. An unsupported native request fails before sending; it does not silently retry another mode. Python is the default for prompt-based structured queries; `protocol: 'python'` supports a restricted constructor-data grammar with explicit factories. See [Python protocol](PYTHON_PROTOCOL.md). Bounded repair remains P1.
100
+
101
+ ## Template, LangFunc, and messages
102
+
103
+ ```ts
104
+ const greeting = new lf.Template('Hello {{name}}!', { name: 'world' });
105
+ const rendered = greeting.render({ name: 'Ada' }); // Message
106
+
107
+ const explain = new lf.LangFunc('Explain {{topic}}.', { lm });
108
+ const response = await explain.call({ vars: { topic: 'rainbows' } });
109
+ console.log(response.text);
110
+ ```
111
+
112
+ Port `Template.render`, composition, bound variables, `fromRawStr`, and basic `{{name}}`/property-path substitution. Interpolated Template, Message, and Modality objects retain their structure. Do not evaluate arbitrary expressions or claim full Jinja compatibility: filters, statements, loops, inheritance, and partial unresolved expressions remain outside P0. JavaScript prompt functions or an optional tagged-template helper cover more elaborate composition.
113
+
114
+ For consistency with Python, query string inputs use this Template subset. Wrap literal text in `UserMessage` or use `Template.fromRawStr` when braces must not be interpreted. Separate `vars` from model/run options to prevent variable names from colliding with configuration keys. Missing variables raise a rendering error in P0.
115
+
116
+ `LangFunc extends Template` and retains `transformInput`/`transformOutput` subclass hooks. `call` returns `Promise<Message>`, resolving to the output Message as in Python; `stream` returns `AsyncIterable<StreamChunk>`. Store per-call input/output in invocation records rather than shared last-call fields, so a reusable instance is concurrency-safe.
117
+
118
+ Retain `Message`, `UserMessage`, `AIMessage`, `SystemMessage`, `ToolMessage`, and `ComplexMessage` as public classes. `ComplexMessage` can compose role-scoped turns; normalize to an ordered message array at the provider boundary. Ordered content parts, not marker-bearing text, are the internal source of truth. Keep text/result/metadata/provenance access and explicit conversion methods. Python symbolic mutation APIs are not required.
119
+
120
+ Retain `Modality`, `Mime`, `Image`, `Audio`, `Video`, and `PDF` concepts. `Image.fromUri` constructs a descriptor without fetching; browser-specific `fromFile`/`fromBytes` complement it. Filesystem `fromPath` belongs to a host adapter. Tool calls/results preserve correlation IDs; provider continuation data remains opaque and scoped to its originating provider. Cache annotations stay separate from visible text.
121
+
122
+ ## Action and Session
123
+
124
+ ```ts
125
+ class Summarize extends lf.Action<string> {
126
+ constructor(readonly text: string) { super(); }
127
+
128
+ override async call(session: lf.Session): Promise<string> {
129
+ return session.query('Summarize {{text}}.', {
130
+ vars: { text: this.text },
131
+ });
132
+ }
133
+ }
134
+
135
+ class Compare extends lf.Action<string[]> {
136
+ constructor(readonly documents: readonly string[]) { super(); }
137
+
138
+ override async call(session: lf.Session): Promise<string[]> {
139
+ return session.concurrentMap(
140
+ this.documents,
141
+ (text, branch) => new Summarize(text).invoke(branch),
142
+ { maxConcurrency: 3 },
143
+ );
144
+ }
145
+ }
146
+
147
+ const session = new lf.Session({ lm });
148
+ const unsubscribe = session.subscribe(event => updateTrace(event));
149
+ try {
150
+ const summaries = await new Compare(documents).invoke(session, {
151
+ signal: controller.signal,
152
+ maxExecutionTime: 30, // Seconds, preserving Python's option semantics.
153
+ });
154
+ renderSummaries(summaries);
155
+ inspectTrace(session.root);
156
+ } finally {
157
+ unsubscribe();
158
+ await session.dispose();
159
+ }
160
+ ```
161
+
162
+ `Action.call` is the author-implemented hook. `Action.invoke` performs lifecycle tracking, deadlines, cancellation, and error recording before calling that hook. Users invoke actions through `invoke`, never directly through `call`. This explicit wrapper replaces Python `__call__`; making a class instance itself callable would require unnecessary function/proxy machinery. Omitting the session creates an owned session, and `invoke(undefined, { lm })` supplies its model.
163
+
164
+ The hook receives an invocation-bound Session view. It shares the root trace store and budget but carries immutable parent ancestry and effective settings. `session.query`, nested `invoke(session)`, and `session.concurrentMap` therefore attribute work correctly without a process-global current action. A view is not a new independent conversation or root session. A view cannot be used to start work after its owning invocation ends.
165
+
166
+ `Session.query` matches `lf.query` overloads and direct return values, with session defaults and trace attribution. Keep `root`, `allQueries`, `allActions`, `allLogs`, `usageSummary`, metadata, progress, and logging concepts. `concurrentMap` passes a bound branch view, limits concurrency, and cancels/settles cooperative siblings according to its error policy.
167
+
168
+ An action object holds configuration; each invocation owns mutable state/result/error. Override precedence is model defaults → session/invocation defaults → explicit call options. Children cannot extend parent deadlines or increase the remaining shared budget. Avoid ambiguous `action.result` or `action.invocation` access during concurrent reuse; inspect per-run trace records.
169
+
170
+ Session observers are UI notifications backed by a bounded trace store. A slow/throwing observer must not corrupt action execution. Disposal releases owned work/listeners/transports without closing a shared externally owned model. No JavaScript `with` or implicit async-context emulation is required.
171
+
172
+ ## Tools and bounded agents
173
+
174
+ Keep Python's `ToolCall`, tool declarations, tool choice, and result messages. Add a small explicit `Tool<Input, Output>` class holding a name, input/output schemas, and an app-owned async execution function; it replaces Python schema reflection, not LanguageModel or Action.
175
+
176
+ The proposed convenience loop is an `Agent extends Action<string>` with instructions, registered tools, and run limits. Construct it with `new Agent(...)` and execute through `invoke(session, options)`. `Tool` and `Agent` are additions, not claims of existing Python classes. Low-level Actions and tool calls remain independently usable.
177
+
178
+ The loop performs request → complete tool calls → schema validation → policy/budget check → registered function execution → correlated result messages → next request. It returns the final answer directly; denial, limit exhaustion, cancellation, and failure have distinct errors/trace outcomes. Tools execute sequentially by default; parallel execution is opt-in. Never execute partial streamed arguments or automatically replay side-effectful tools.
179
+
180
+ Model-selected Action classes require explicit declared schemas and an allowlisted mapping from discriminator/name to constructors. A model-provided class name must not trigger dynamic imports or arbitrary construction.
181
+
182
+ ## Streaming, persistence, and release checks
183
+
184
+ Keep StreamChunk's delta, aggregated message, candidate index, final marker, usage, and finish reason. `queryStream` with a schema retains ObjectStart, FieldStart, StringFieldDelta, FieldEnd, ObjectEnd, GenerationComplete, and ParsingFailed semantics; replace Python path/schema/class objects with portable equivalents. Add a distinct cancellation terminal outcome. Partial data is never typed as validated `T`. The implemented discriminated event union uses camelCase `type` values, adds `generationFailed` for provider/transport failures, and represents paths as arrays of string keys and numeric indices. See [structured streaming](STREAMING.md) for the concrete contract.
185
+
186
+ Close/abort upstream readers in `finally`, including early consumer exit. A fully consumed structured query stream reports one terminal outcome (early consumer exit closes it without a delivered terminal event); raw model/text streams retain their chunk-and-thrown-error contract; non-streaming calls reject on failure. Invalid setup may throw before iteration begins. Preserve classified errors and sanitized provider details. Test escape/UTF-8 fragmentation, stream termination, tool correlation, and usage aggregation.
187
+
188
+ Serialize versioned message/history/trace data, not executable class definitions or closures. Public classes can implement explicit serialization without reproducing the PyGlove registry. Saved conversations start new runs; durable resume and exactly-once execution remain separate concerns.
189
+
190
+ Before stabilizing the API, pair each supported Python example with a TypeScript example: provider construction and substitution, custom LanguageModel subclass, typed/direct query return, returnsMessage, Template composition, LangFunc transforms, nested Action/Session traces, and streaming. Type-check these examples during implementation. Every divergence must be either a mechanical language mapping or a documented scope/runtime decision.
191
+
192
+ ## Mapping classes
193
+
194
+ Prompt-based structured queries use `LfQuery<T> → Mapping<T> → LangFunc → Template`, following Python's reusable mapping pipeline. `query` is the convenience wrapper; direct `Mapping.call` returns `Message<T>`, with the typed value in `result`. `MappingExample` describes request/example input, optional output/schema/context/metadata. Few-shot examples require JSON-compatible parser inputs in JSON mode, or class instances supported by the Python codec in Python mode.
195
+
196
+ `render`, `transformInput`, `postprocessResponse`, `parseResult`, and `postprocessResult` provide synchronous customization hooks; model execution remains async. Native structured output bypasses Mapping, as in Python. Mapping supports JSON and a restricted Python-style data protocol; it excludes arbitrary protocol registration, Python code evaluation, automatic repair, defaults on failure, and PyGlove contextual behavior. See [Mapping](MAPPING.md) for concrete supported behavior and streaming boundaries.
197
+
198
+ ## Conversation history scope
199
+
200
+ Memory, ConversationHistory, and a memory namespace are out of scope for now. Applications own any cross-invocation conversation history and can supply explicit Message/ComplexMessage transcripts to models. Agent transcripts remain local to each invocation.
package/docs/GEMINI.md ADDED
@@ -0,0 +1,68 @@
1
+ # Gemini adapter and tool loop
2
+
3
+ `Gemini` is the first concrete LanguageModel adapter. `GoogleGenAI` is also exported as a subclass for users familiar with Python's Developer API naming. This implementation does not include Vertex AI authentication or its endpoint layout.
4
+
5
+ ```ts
6
+ import * as lf from 'langfx.js';
7
+ // Or: import { Gemini } from 'langfx.js/llms/gemini';
8
+
9
+ const lm = new lf.llms.Gemini({ model: modelId, apiKey });
10
+ const answer = await lf.query('Explain rainbows.', { lm });
11
+
12
+ for await (const chunk of lm.stream('Explain rainbows.')) {
13
+ renderText(chunk.aggregated.text);
14
+ }
15
+ ```
16
+
17
+ The application supplies `modelId` and credentials. No default model, environment-variable lookup, SDK initialization, or network request occurs during import or construction. An async `apiKey(signal)` resolver, fetch-compatible `transport(url, init)`, and `baseUrl` support host authentication and compatible gateways. `baseUrl` is the API-version prefix; the adapter appends `/models/{model}:generateContent` or the SSE equivalent. Browser endpoint access still depends on CORS/auth; do not embed a shared organization credential in a public application.
18
+
19
+ ## Supported and tested boundary
20
+
21
+ Text and image input; generated text; complete function calls and correlated results; native structured JSON requests; sampling temperature/maxTokens; tool choice; reported usage; text SSE; cancellation; HTTP error classification. Images use explicit MIME types and inline bytes/Blob or provider-readable file URIs. The adapter does not upload or download URI media for the application.
22
+
23
+ The transport uses the Developer API's generateContent and streamGenerateContent methods. Structured output uses responseJsonSchema, tool declarations use parametersJsonSchema, and same-model follow-up turns retain original provider parts, including opaque signatures. [Gemini API reference](https://ai.google.dev/api/generate-content)
24
+
25
+ The adapter deliberately accepts a conservative schema subset: basic types, object properties/required/additionalProperties, array items/count bounds, numeric bounds, titles/descriptions, string/number enum or const, and anyOf. Unsupported keywords fail before the request. The `$schema` dialect annotation is omitted from the wire; const is represented as a singleton enum. Local validation still runs for completed typed queries. Recursive references, formats, patterns, and other unsupported constraints can use prompt-JSON generation plus the local validator instead of native schema enforcement.
26
+
27
+ Complete function calls are supported during streaming, with start/end display events and final continuation metadata. Partial function arguments, generated media, multiple candidates, and provider-side hosted tools are not implemented. Retry and local response-cache support come from LanguageModel. See [tool streaming](TOOL_STREAMING.md). They are not silently emulated. Truncated or filtered generations fail rather than being treated as successful answers. Token usage remains unknown if the provider omits required counters. Usage totals are passed through, not reconstructed from visible text.
28
+
29
+ ## Registered application tools
30
+
31
+ ```ts
32
+ import * as lf from 'langfx.js';
33
+ import { fromZod } from 'langfx.js/schema/zod';
34
+ import { z } from 'zod';
35
+
36
+ const add = new lf.Tool({
37
+ name: 'add',
38
+ description: 'Add two numbers.',
39
+ input: fromZod(z.object({ a: z.number(), b: z.number() })),
40
+ output: fromZod(z.number()),
41
+ async execute({ a, b }, session) {
42
+ // Pass session.signal to application I/O when needed.
43
+ return a + b;
44
+ },
45
+ });
46
+
47
+ const session = new lf.Session({ lm });
48
+ try {
49
+ const answer = await new lf.Agent({
50
+ prompt: 'What is 2 + 3?',
51
+ tools: [add],
52
+ limits: { maxSteps: 4, maxToolCalls: 2, maxExecutionTime: 30 },
53
+ }).invoke(session);
54
+ console.log(answer, session.allActions);
55
+ } finally {
56
+ await session.dispose();
57
+ }
58
+ ```
59
+
60
+ An Agent is an Action with prompt/configuration fields. Limits default to 8 model requests, 12 tool calls, and 60 seconds per invocation. Explicit shorter run/parent deadlines still apply. Each concurrent invocation owns its transcript, call IDs, and counters. These are loop-local limits, not a global allowance for arbitrary nested agents or model calls inside app tools. Exact token/cost budgeting remains future work.
61
+
62
+ Before any batch executes, every call must resolve to a registered tool, have a unique ID, pass input validation and the optional `allowTool(call, session)` policy, and fit the remaining call budget. At least one model step must remain to consume the results. Tools execute sequentially as named trace actions. Output validation and JSON serialization happen before adding the result message. Errors/denials stop the run; side effects are never automatically replayed or rolled back. A host policy may await its own approval UI; the library does not supply that UI.
63
+
64
+ ## Verification
65
+
66
+ `npm run example:agent` executes the actual Gemini encoder/decoder and a local tool against deterministic HTTP response fixtures. `npm run serve:example` runs the same loop in a browser and module worker. Both Safari checks passed. The tests also cover signatures, missing/provider call IDs, multipart tool results, UTF-8/SSE fragmentation, abort/reader cleanup, error paths, and loop isolation/budgets.
67
+
68
+ The seven-request live smoke suite passed on `gemini-3.7-flash` on 2026-09-16, including JSON/Python queries, text/Python streaming, and a streamed local-tool round trip. See [live testing](LIVE_TESTING.md). This verifies those cases in Node; browser CORS, media, native structured output, and other model configurations remain unverified.