@molecule/api-ai-agents-llm 1.0.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/LICENSE ADDED
@@ -0,0 +1,115 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work.
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship.
43
+
44
+ "Contribution" shall mean any work of authorship, including the
45
+ original version of the Work and any modifications or additions
46
+ to that Work, that is intentionally submitted to the Licensor for
47
+ inclusion in the Work by the copyright owner or by an individual or
48
+ Legal Entity authorized to submit on behalf of the copyright owner.
49
+
50
+ "Contributor" shall mean Licensor and any individual or Legal Entity
51
+ on behalf of whom a Contribution has been received by the Licensor and
52
+ subsequently incorporated within the Work.
53
+
54
+ 2. Grant of Copyright License. Subject to the terms and conditions of
55
+ this License, each Contributor hereby grants to You a perpetual,
56
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
57
+ copyright license to reproduce, prepare Derivative Works of,
58
+ publicly display, publicly perform, sublicense, and distribute the
59
+ Work and such Derivative Works in Source or Object form.
60
+
61
+ 3. Grant of Patent License. Subject to the terms and conditions of
62
+ this License, each Contributor hereby grants to You a perpetual,
63
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
64
+ patent license to make, have made, use, offer to sell, sell, import,
65
+ and otherwise transfer the Work.
66
+
67
+ 4. Redistribution. You may reproduce and distribute copies of the
68
+ Work or Derivative Works thereof in any medium, with or without
69
+ modifications, and in Source or Object form, provided that You
70
+ meet the following conditions:
71
+
72
+ (a) You must give any other recipients of the Work or
73
+ Derivative Works a copy of this License; and
74
+
75
+ (b) You must cause any modified files to carry prominent notices
76
+ stating that You changed the files; and
77
+
78
+ (c) You must retain, in the Source form of any Derivative Works
79
+ that You distribute, all copyright, patent, trademark, and
80
+ attribution notices from the Source form of the Work,
81
+ excluding those notices that do not pertain to any part of
82
+ the Derivative Works; and
83
+
84
+ (d) If the Work includes a "NOTICE" text file as part of its
85
+ distribution, then any Derivative Works that You distribute must
86
+ include a readable copy of the attribution notices contained
87
+ within such NOTICE file.
88
+
89
+ 5. Submission of Contributions.
90
+
91
+ 6. Trademarks. This License does not grant permission to use the trade
92
+ names, trademarks, service marks, or product names of the Licensor.
93
+
94
+ 7. Disclaimer of Warranty. Unless required by applicable law or
95
+ agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
96
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
97
+
98
+ 8. Limitation of Liability. In no event and under no legal theory shall
99
+ any Contributor be liable to You for damages.
100
+
101
+ 9. Accepting Warranty or Additional Liability.
102
+
103
+ Copyright 2026 Molecule Dev, Inc.
104
+
105
+ Licensed under the Apache License, Version 2.0 (the "License");
106
+ you may not use this file except in compliance with the License.
107
+ You may obtain a copy of the License at
108
+
109
+ http://www.apache.org/licenses/LICENSE-2.0
110
+
111
+ Unless required by applicable law or agreed to in writing, software
112
+ distributed under the License is distributed on an "AS IS" BASIS,
113
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
114
+ See the License for the specific language governing permissions and
115
+ limitations under the License.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Browser guard — `@molecule/api-ai-agents-llm` is SERVER-ONLY.
3
+ *
4
+ * Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
5
+ * Evaluating a server package in a browser bundle is always an import-graph mistake
6
+ * (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
7
+ * ("Buffer is not defined") far from the culprit. Throwing here names the package and
8
+ * the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
9
+ * unaffected: the throw requires browser globals AND the absence of a node runtime.
10
+ */
11
+ const g = globalThis;
12
+ if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
13
+ throw new Error('@molecule/api-ai-agents-llm is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
14
+ 'from server code (a server route/function or your API), or dynamic-import it inside ' +
15
+ 'the server handler — never from components or shared client modules, and never ' +
16
+ 'polyfill Buffer/process to silence this.');
17
+ }
18
+ export {};
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Typed error for a failed agent run.
3
+ *
4
+ * @module
5
+ */
6
+ import type { TokenUsage } from '@molecule/api-ai';
7
+ import type { AgentStep } from '@molecule/api-ai-agents';
8
+ /**
9
+ * Thrown by `run()` when the agentic loop fails partway through — a provider
10
+ * API error (rate limit, auth, overload, network), an abort, or any other
11
+ * exception raised while draining a model turn or executing a tool.
12
+ *
13
+ * @remarks
14
+ * A plain rejected `run()` promise has no channel to deliver partial
15
+ * metering: the `Promise<AgentRunResult>` contract only carries `usage` on
16
+ * the RESOLVED path, so a long run that dies on step 8 used to silently
17
+ * under-meter the 7 turns of real, already-billed spend that came before it.
18
+ * `AgentRunError` carries the `usage` totaled across every turn that
19
+ * completed before the failure, plus the `steps` recorded so far, so a
20
+ * caller that meters spend can book the partial usage instead of losing it.
21
+ *
22
+ * `error.message` is unchanged from the plain `Error` this replaces —
23
+ * existing `catch (error) { … error.message … }` call sites keep working
24
+ * without any change. Only callers that want the partial usage/steps need to
25
+ * check `error instanceof AgentRunError`.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { AgentRunError } from '@molecule/api-ai-agents-llm'
30
+ *
31
+ * try {
32
+ * const result = await requireProvider().run({ task, tools })
33
+ * meterUsage(result.usage)
34
+ * } catch (error) {
35
+ * if (error instanceof AgentRunError) {
36
+ * meterUsage(error.usage) // book whatever was billed before the failure
37
+ * }
38
+ * throw error
39
+ * }
40
+ * ```
41
+ */
42
+ export declare class AgentRunError extends Error {
43
+ /** Token usage summed across every turn that completed before the failure. */
44
+ readonly usage: TokenUsage;
45
+ /** Every intermediate step (with its tool calls) recorded before the failure. */
46
+ readonly steps: AgentStep[];
47
+ /**
48
+ * Builds the error, carrying the partial run alongside the failure so a
49
+ * caller can report what the agent had already spent and done.
50
+ *
51
+ * @param message - The error message (same text a plain `Error` would carry).
52
+ * @param usage - Token usage accumulated across turns completed before the failure.
53
+ * @param steps - Steps recorded before the failure.
54
+ * @param options - Standard `Error` options — `cause` preserves the original thrown value.
55
+ */
56
+ constructor(message: string, usage: TokenUsage, steps: AgentStep[], options?: {
57
+ cause?: unknown;
58
+ });
59
+ }
60
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,CAAA;IAE3B;;;;;;;;OAQG;gBAED,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,SAAS,EAAE,EAClB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAOhC"}
package/dist/errors.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Typed error for a failed agent run.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * Thrown by `run()` when the agentic loop fails partway through — a provider
8
+ * API error (rate limit, auth, overload, network), an abort, or any other
9
+ * exception raised while draining a model turn or executing a tool.
10
+ *
11
+ * @remarks
12
+ * A plain rejected `run()` promise has no channel to deliver partial
13
+ * metering: the `Promise<AgentRunResult>` contract only carries `usage` on
14
+ * the RESOLVED path, so a long run that dies on step 8 used to silently
15
+ * under-meter the 7 turns of real, already-billed spend that came before it.
16
+ * `AgentRunError` carries the `usage` totaled across every turn that
17
+ * completed before the failure, plus the `steps` recorded so far, so a
18
+ * caller that meters spend can book the partial usage instead of losing it.
19
+ *
20
+ * `error.message` is unchanged from the plain `Error` this replaces —
21
+ * existing `catch (error) { … error.message … }` call sites keep working
22
+ * without any change. Only callers that want the partial usage/steps need to
23
+ * check `error instanceof AgentRunError`.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { AgentRunError } from '@molecule/api-ai-agents-llm'
28
+ *
29
+ * try {
30
+ * const result = await requireProvider().run({ task, tools })
31
+ * meterUsage(result.usage)
32
+ * } catch (error) {
33
+ * if (error instanceof AgentRunError) {
34
+ * meterUsage(error.usage) // book whatever was billed before the failure
35
+ * }
36
+ * throw error
37
+ * }
38
+ * ```
39
+ */
40
+ export class AgentRunError extends Error {
41
+ /** Token usage summed across every turn that completed before the failure. */
42
+ usage;
43
+ /** Every intermediate step (with its tool calls) recorded before the failure. */
44
+ steps;
45
+ /**
46
+ * Builds the error, carrying the partial run alongside the failure so a
47
+ * caller can report what the agent had already spent and done.
48
+ *
49
+ * @param message - The error message (same text a plain `Error` would carry).
50
+ * @param usage - Token usage accumulated across turns completed before the failure.
51
+ * @param steps - Steps recorded before the failure.
52
+ * @param options - Standard `Error` options — `cause` preserves the original thrown value.
53
+ */
54
+ constructor(message, usage, steps, options) {
55
+ super(message, options);
56
+ this.name = 'AgentRunError';
57
+ this.usage = usage;
58
+ this.steps = steps;
59
+ }
60
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * LLM-backed AI agents provider for molecule.dev — the batteries-included
3
+ * tool-calling loop that implements the `@molecule/api-ai-agents` core contract.
4
+ *
5
+ * Ships a default `provider` (`name: 'llm'`) that drives a model↔tool loop over
6
+ * the swappable `ai` chat bond (`@molecule/api-ai`): it calls the model,
7
+ * executes any tools the model requests via each `AITool.execute()`, feeds the
8
+ * results back, and repeats until the model stops calling tools (or the
9
+ * `maxSteps` budget is exhausted). Bond it once at startup, then drive it from
10
+ * anywhere via the `@molecule/api-ai-agents` accessor.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { bond } from '@molecule/api-bond'
15
+ * import { requireProvider } from '@molecule/api-ai-agents'
16
+ * import { provider as agents } from '@molecule/api-ai-agents-llm'
17
+ * import type { AITool } from '@molecule/api-ai'
18
+ *
19
+ * // Wire at startup (an `ai` provider must already be bonded).
20
+ * bond('ai-agents', agents)
21
+ *
22
+ * const myTool: AITool = {
23
+ * name: 'add',
24
+ * description: 'Add two numbers',
25
+ * parameters: {
26
+ * type: 'object',
27
+ * properties: { a: { type: 'number' }, b: { type: 'number' } },
28
+ * required: ['a', 'b'],
29
+ * },
30
+ * execute: async (input) => {
31
+ * const { a, b } = input as { a: number; b: number }
32
+ * return a + b
33
+ * },
34
+ * }
35
+ *
36
+ * const result = await requireProvider().run({
37
+ * task: 'What is 1 + 2? Use the add tool.',
38
+ * tools: [myTool],
39
+ * })
40
+ * console.log(result.output) // final assistant answer
41
+ * console.log(result.steps) // intermediate tool calls + results
42
+ * console.log(result.usage) // token usage summed across all model calls
43
+ * ```
44
+ *
45
+ * @remarks
46
+ * **Requires a bonded `ai` provider** (`bond('ai', provider)` or a named
47
+ * provider selected via `run({ provider: 'anthropic' })`) whose model supports
48
+ * tool use — this agent has no model of its own; it orchestrates the `ai` bond.
49
+ *
50
+ * The loop executes your tools: each model `tool_use` runs the matching
51
+ * `AITool.execute(input)` and the return value is fed back as a `tool_result`.
52
+ * A thrown tool error or an unknown tool name becomes an error `tool_result`
53
+ * (recorded with `isError: true`) instead of aborting the run, so the model can
54
+ * recover. `run()` requires exactly one of `task` (a single user turn) or
55
+ * `messages` (a full history); it throws if neither — or both — is supplied.
56
+ *
57
+ * Every model turn streams by default (`input.stream` — set `false` to force
58
+ * non-streaming) and, when `input.cacheControl` is set, is passed through to
59
+ * `ai.chat()` on every turn so a provider that supports prompt caching (e.g.
60
+ * `@molecule/api-ai-anthropic`) doesn't re-bill the identical `system` + `tools`
61
+ * prefix on each of up to `maxSteps` turns. Note `onEvent` is only an
62
+ * INCREMENTAL live hook while streaming — with `stream: false` it fires once
63
+ * per turn with the whole response.
64
+ *
65
+ * Failure semantics (how to tell failure modes apart):
66
+ * - **Tool failure** (throwing `execute()`, unknown tool name) — NON-fatal:
67
+ * recorded on the step with `isError: true` and fed back to the model.
68
+ * - **AI provider API failure** (rate limit, bad key, overload, network — the
69
+ * `ai` bond emits an in-band `error` ChatEvent), **an abort, or any other
70
+ * mid-run exception** — FATAL: `run()` rejects with an `AgentRunError`
71
+ * (message text unchanged from a plain `Error`) carrying the `usage` and
72
+ * `steps` accumulated across every turn that completed before the failure —
73
+ * a caller that meters spend can book that partial usage instead of losing
74
+ * it. `run()` never resolves with a silently empty `output`, so an empty
75
+ * `result.output` means the model genuinely produced no text, not that the
76
+ * API failed.
77
+ * - **Step budget exhausted** — resolves normally; `output` is the last
78
+ * assistant text or an explicit "step budget exhausted" note.
79
+ *
80
+ * Swappable like any bond: replace this LLM agent with your own
81
+ * `AIAgentsProvider` via `bond('ai-agents', myProvider)` — nothing else changes.
82
+ *
83
+ * @module
84
+ */
85
+ export * from './browser-guard.js';
86
+ export * from './errors.js';
87
+ export * from './provider.js';
88
+ export * from './types.js';
89
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmFG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * LLM-backed AI agents provider for molecule.dev — the batteries-included
3
+ * tool-calling loop that implements the `@molecule/api-ai-agents` core contract.
4
+ *
5
+ * Ships a default `provider` (`name: 'llm'`) that drives a model↔tool loop over
6
+ * the swappable `ai` chat bond (`@molecule/api-ai`): it calls the model,
7
+ * executes any tools the model requests via each `AITool.execute()`, feeds the
8
+ * results back, and repeats until the model stops calling tools (or the
9
+ * `maxSteps` budget is exhausted). Bond it once at startup, then drive it from
10
+ * anywhere via the `@molecule/api-ai-agents` accessor.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { bond } from '@molecule/api-bond'
15
+ * import { requireProvider } from '@molecule/api-ai-agents'
16
+ * import { provider as agents } from '@molecule/api-ai-agents-llm'
17
+ * import type { AITool } from '@molecule/api-ai'
18
+ *
19
+ * // Wire at startup (an `ai` provider must already be bonded).
20
+ * bond('ai-agents', agents)
21
+ *
22
+ * const myTool: AITool = {
23
+ * name: 'add',
24
+ * description: 'Add two numbers',
25
+ * parameters: {
26
+ * type: 'object',
27
+ * properties: { a: { type: 'number' }, b: { type: 'number' } },
28
+ * required: ['a', 'b'],
29
+ * },
30
+ * execute: async (input) => {
31
+ * const { a, b } = input as { a: number; b: number }
32
+ * return a + b
33
+ * },
34
+ * }
35
+ *
36
+ * const result = await requireProvider().run({
37
+ * task: 'What is 1 + 2? Use the add tool.',
38
+ * tools: [myTool],
39
+ * })
40
+ * console.log(result.output) // final assistant answer
41
+ * console.log(result.steps) // intermediate tool calls + results
42
+ * console.log(result.usage) // token usage summed across all model calls
43
+ * ```
44
+ *
45
+ * @remarks
46
+ * **Requires a bonded `ai` provider** (`bond('ai', provider)` or a named
47
+ * provider selected via `run({ provider: 'anthropic' })`) whose model supports
48
+ * tool use — this agent has no model of its own; it orchestrates the `ai` bond.
49
+ *
50
+ * The loop executes your tools: each model `tool_use` runs the matching
51
+ * `AITool.execute(input)` and the return value is fed back as a `tool_result`.
52
+ * A thrown tool error or an unknown tool name becomes an error `tool_result`
53
+ * (recorded with `isError: true`) instead of aborting the run, so the model can
54
+ * recover. `run()` requires exactly one of `task` (a single user turn) or
55
+ * `messages` (a full history); it throws if neither — or both — is supplied.
56
+ *
57
+ * Every model turn streams by default (`input.stream` — set `false` to force
58
+ * non-streaming) and, when `input.cacheControl` is set, is passed through to
59
+ * `ai.chat()` on every turn so a provider that supports prompt caching (e.g.
60
+ * `@molecule/api-ai-anthropic`) doesn't re-bill the identical `system` + `tools`
61
+ * prefix on each of up to `maxSteps` turns. Note `onEvent` is only an
62
+ * INCREMENTAL live hook while streaming — with `stream: false` it fires once
63
+ * per turn with the whole response.
64
+ *
65
+ * Failure semantics (how to tell failure modes apart):
66
+ * - **Tool failure** (throwing `execute()`, unknown tool name) — NON-fatal:
67
+ * recorded on the step with `isError: true` and fed back to the model.
68
+ * - **AI provider API failure** (rate limit, bad key, overload, network — the
69
+ * `ai` bond emits an in-band `error` ChatEvent), **an abort, or any other
70
+ * mid-run exception** — FATAL: `run()` rejects with an `AgentRunError`
71
+ * (message text unchanged from a plain `Error`) carrying the `usage` and
72
+ * `steps` accumulated across every turn that completed before the failure —
73
+ * a caller that meters spend can book that partial usage instead of losing
74
+ * it. `run()` never resolves with a silently empty `output`, so an empty
75
+ * `result.output` means the model genuinely produced no text, not that the
76
+ * API failed.
77
+ * - **Step budget exhausted** — resolves normally; `output` is the last
78
+ * assistant text or an explicit "step budget exhausted" note.
79
+ *
80
+ * Swappable like any bond: replace this LLM agent with your own
81
+ * `AIAgentsProvider` via `bond('ai-agents', myProvider)` — nothing else changes.
82
+ *
83
+ * @module
84
+ */
85
+ export * from './browser-guard.js';
86
+ export * from './errors.js';
87
+ export * from './provider.js';
88
+ export * from './types.js';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The default LLM-backed AI agents provider: a tool-calling loop over the
3
+ * bonded `ai` chat provider.
4
+ *
5
+ * This provider owns the agentic loop that used to live in the
6
+ * `@molecule/api-ai-agents` core. It composes the swappable `ai` chat bond
7
+ * (`@molecule/api-ai`): it calls the model, executes any tools the model
8
+ * requests, feeds the results back, and repeats until the model stops calling
9
+ * tools (or the `maxSteps` budget is exhausted). Living in a bond package (not
10
+ * the core) keeps the core interface-only.
11
+ *
12
+ * @module
13
+ */
14
+ import './types.js';
15
+ import type { AIAgentsProvider } from '@molecule/api-ai-agents';
16
+ /**
17
+ * The LLM-backed AI agents provider: a tool-calling loop over the bonded `ai`
18
+ * chat provider.
19
+ *
20
+ * Bond it with `bond('ai-agents', provider)` and drive it with
21
+ * `requireProvider().run({ task, tools })`. Requires a bonded `ai` provider
22
+ * whose model supports tool use.
23
+ */
24
+ export declare const provider: AIAgentsProvider;
25
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,YAAY,CAAA;AAOnB,OAAO,KAAK,EAKV,gBAAgB,EACjB,MAAM,yBAAyB,CAAA;AAkHhC;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,gBA2KtB,CAAA"}
@@ -0,0 +1,250 @@
1
+ /**
2
+ * The default LLM-backed AI agents provider: a tool-calling loop over the
3
+ * bonded `ai` chat provider.
4
+ *
5
+ * This provider owns the agentic loop that used to live in the
6
+ * `@molecule/api-ai-agents` core. It composes the swappable `ai` chat bond
7
+ * (`@molecule/api-ai`): it calls the model, executes any tools the model
8
+ * requests, feeds the results back, and repeats until the model stops calling
9
+ * tools (or the `maxSteps` budget is exhausted). Living in a bond package (not
10
+ * the core) keeps the core interface-only.
11
+ *
12
+ * @module
13
+ */
14
+ // Side-effect import: registers the `stream`/`cacheControl` module
15
+ // augmentation on `AgentRunInput` (see types.ts) so `input.stream` and
16
+ // `input.cacheControl` below are typed, not `any`.
17
+ import './types.js';
18
+ import { getProviderByName as getAiProviderByName, requireProvider as requireAiProvider, } from '@molecule/api-ai';
19
+ import { t } from '@molecule/api-i18n';
20
+ import { AgentRunError } from './errors.js';
21
+ /**
22
+ * Adds one usage delta into a running total (input/output summed, cache fields
23
+ * carried when present).
24
+ *
25
+ * @param total - The running total, mutated in place.
26
+ * @param delta - The usage to add.
27
+ */
28
+ function addUsage(total, delta) {
29
+ total.inputTokens += delta.inputTokens ?? 0;
30
+ total.outputTokens += delta.outputTokens ?? 0;
31
+ if (delta.cacheCreationInputTokens != null) {
32
+ total.cacheCreationInputTokens =
33
+ (total.cacheCreationInputTokens ?? 0) + delta.cacheCreationInputTokens;
34
+ }
35
+ if (delta.cacheReadInputTokens != null) {
36
+ total.cacheReadInputTokens = (total.cacheReadInputTokens ?? 0) + delta.cacheReadInputTokens;
37
+ }
38
+ }
39
+ /**
40
+ * Stringifies a tool result for a `tool_result` content block.
41
+ *
42
+ * @param result - The value returned by a tool's `execute()`.
43
+ * @returns A string representation suitable for the model.
44
+ */
45
+ function stringifyResult(result) {
46
+ if (typeof result === 'string')
47
+ return result;
48
+ try {
49
+ // JSON.stringify returns `undefined` for `undefined`/functions/symbols —
50
+ // fall back to String() so the block content is always a string.
51
+ return JSON.stringify(result) ?? String(result);
52
+ }
53
+ catch (_error) {
54
+ // Circular structures throw; a plain String() is the best available
55
+ // representation for the model. Intentionally not fatal to the run.
56
+ return String(result);
57
+ }
58
+ }
59
+ /**
60
+ * Drains a single chat stream, accumulating text and tool calls, forwarding
61
+ * every event to `onEvent`, and summing `done.usage`.
62
+ *
63
+ * @param stream - The async iterable of chat events from `ai.chat()`.
64
+ * @param onEvent - Optional hook invoked for every event.
65
+ * @returns The accumulated text, tool calls, and usage for this turn.
66
+ */
67
+ async function drainStream(stream, onEvent) {
68
+ let text = '';
69
+ const toolUses = [];
70
+ const usage = { inputTokens: 0, outputTokens: 0 };
71
+ let errorMessage;
72
+ let sawDone = false;
73
+ let lastSnapshot;
74
+ for await (const event of stream) {
75
+ onEvent?.(event);
76
+ switch (event.type) {
77
+ case 'text':
78
+ text += event.content;
79
+ break;
80
+ case 'tool_use':
81
+ toolUses.push({ id: event.id, name: event.name, input: event.input });
82
+ break;
83
+ case 'usage':
84
+ // Incremental snapshot (latest wins) — booked below if the stream ends
85
+ // without a `done` (metering contract in @molecule/api-ai ChatEvent).
86
+ lastSnapshot = event.usage;
87
+ break;
88
+ case 'error':
89
+ // Keep the FIRST error; providers emit one sanitized error then return.
90
+ errorMessage ??= event.message;
91
+ break;
92
+ case 'done':
93
+ sawDone = true;
94
+ addUsage(usage, event.usage);
95
+ break;
96
+ default:
97
+ break;
98
+ }
99
+ }
100
+ // METERING CONTRACT: a stream cut without a `done` (provider error event,
101
+ // upstream disconnect) still billed the tokens reported so far — book the
102
+ // latest snapshot instead of silently dropping the turn's spend.
103
+ if (!sawDone && lastSnapshot) {
104
+ addUsage(usage, lastSnapshot);
105
+ }
106
+ return { text, toolUses, usage, errorMessage };
107
+ }
108
+ /**
109
+ * The LLM-backed AI agents provider: a tool-calling loop over the bonded `ai`
110
+ * chat provider.
111
+ *
112
+ * Bond it with `bond('ai-agents', provider)` and drive it with
113
+ * `requireProvider().run({ task, tools })`. Requires a bonded `ai` provider
114
+ * whose model supports tool use.
115
+ */
116
+ export const provider = {
117
+ name: 'llm',
118
+ async run(input) {
119
+ if (!input.task && !(input.messages && input.messages.length > 0)) {
120
+ throw new Error(t('ai-agents.error.noTaskOrMessages', undefined, {
121
+ defaultValue: 'AgentRunInput requires a `task` string or a non-empty `messages` array.',
122
+ }));
123
+ }
124
+ // The contract is EXACTLY one of task/messages (see AgentRunInput). The old
125
+ // behavior silently discarded `task` when both were passed — an executor's
126
+ // task vanished with no signal. Fail loudly instead.
127
+ if (input.task && input.messages && input.messages.length > 0) {
128
+ throw new Error(t('ai-agents.error.bothTaskAndMessages', undefined, {
129
+ defaultValue: 'AgentRunInput accepts either `task` or `messages`, not both. Put the task in the `messages` history (as the last user message) or drop one of the two.',
130
+ }));
131
+ }
132
+ const ai = input.provider ? getAiProviderByName(input.provider) : requireAiProvider();
133
+ if (!ai) {
134
+ throw new Error(t('ai-agents.error.aiProviderNotFound', { name: input.provider ?? '' }, { defaultValue: `AI provider "${input.provider}" is not bonded.` }));
135
+ }
136
+ const messages = input.messages
137
+ ? [...input.messages]
138
+ : [{ role: 'user', content: input.task }];
139
+ const maxSteps = input.maxSteps ?? 10;
140
+ const steps = [];
141
+ const usage = { inputTokens: 0, outputTokens: 0 };
142
+ let lastText = '';
143
+ // Everything below can fail partway through a multi-turn run (a provider
144
+ // API error, an abort, or an unexpected throw from ai.chat() itself —
145
+ // e.g. a signal-aborted fetch). A plain rejection would silently drop
146
+ // every prior turn's `usage`/`steps`: a run that dies on step 8 would
147
+ // under-meter 7 turns of real billed spend. Wrap the whole loop so ANY
148
+ // failure re-throws as an AgentRunError carrying what was accumulated so
149
+ // far — `error.message` is unchanged, so existing catch sites keep
150
+ // working; only callers that want the partial usage/steps need to check
151
+ // `instanceof AgentRunError`.
152
+ try {
153
+ for (let step = 0; step < maxSteps; step++) {
154
+ const turn = await drainStream(ai.chat({
155
+ messages,
156
+ system: input.system,
157
+ tools: input.tools,
158
+ model: input.model,
159
+ maxTokens: input.maxTokens,
160
+ temperature: input.temperature,
161
+ // Defaults to streaming (see the `stream` augmentation in
162
+ // types.ts): onEvent is a LIVE hook, and non-streaming requests
163
+ // with large outputs (a big tool input at a raised maxTokens) are
164
+ // exactly what provider docs warn will time out over HTTP.
165
+ // drainStream already folds streamed events correctly, so there
166
+ // is no extra cost to defaulting streaming on. Set
167
+ // `input.stream: false` to force non-streaming.
168
+ stream: input.stream !== false,
169
+ // Prompt-cache breakpoint hint: the system+tools prefix is
170
+ // identical across turns, so passing this through avoids
171
+ // re-billing the full (growing) prompt on every one of up to
172
+ // maxSteps turns. Providers without cache support ignore it.
173
+ cacheControl: input.cacheControl,
174
+ signal: input.signal,
175
+ }), input.onEvent);
176
+ addUsage(usage, turn.usage);
177
+ // A provider API failure (rate limit, auth, overload, network) arrives
178
+ // as an in-band `error` ChatEvent — the stream ends without text or
179
+ // tools. Rejecting here disambiguates "the AI provider failed" from
180
+ // "the model legitimately answered nothing": the old fall-through
181
+ // returned { output: '', steps, usage } as if the run succeeded, and
182
+ // callers debugging the empty output had no signal that the API ever
183
+ // errored.
184
+ if (turn.errorMessage !== undefined) {
185
+ throw new Error(t('ai-agents.error.providerError', { message: turn.errorMessage }, { defaultValue: `AI provider error during agent run: ${turn.errorMessage}` }));
186
+ }
187
+ if (turn.text)
188
+ lastText = turn.text;
189
+ // Append the assistant turn: a text block (if any) + one tool_use block
190
+ // per requested tool call.
191
+ const assistantContent = [];
192
+ if (turn.text)
193
+ assistantContent.push({ type: 'text', text: turn.text });
194
+ for (const tu of turn.toolUses) {
195
+ assistantContent.push({ type: 'tool_use', id: tu.id, name: tu.name, input: tu.input });
196
+ }
197
+ messages.push({ role: 'assistant', content: assistantContent });
198
+ if (turn.toolUses.length === 0) {
199
+ // No tools requested — the model is done.
200
+ return { output: turn.text, steps, usage };
201
+ }
202
+ // Execute each requested tool and build the tool_result turn.
203
+ const resultBlocks = [];
204
+ const toolCalls = [];
205
+ for (const tu of turn.toolUses) {
206
+ if (input.signal?.aborted) {
207
+ throw new Error(t('ai-agents.error.aborted', undefined, {
208
+ defaultValue: 'Agent run aborted before tool execution completed.',
209
+ }));
210
+ }
211
+ const tool = input.tools?.find((candidate) => candidate.name === tu.name);
212
+ let result;
213
+ let isError = false;
214
+ if (!tool) {
215
+ result = `Error: unknown tool "${tu.name}".`;
216
+ isError = true;
217
+ }
218
+ else {
219
+ try {
220
+ result = await tool.execute(tu.input);
221
+ }
222
+ catch (error) {
223
+ // The model can recover from a failed tool call; surface the error
224
+ // back to it as a tool_result rather than aborting the whole run.
225
+ result = `Error: ${error instanceof Error ? error.message : String(error)}`;
226
+ isError = true;
227
+ }
228
+ }
229
+ toolCalls.push({ id: tu.id, name: tu.name, input: tu.input, result, isError });
230
+ resultBlocks.push({
231
+ type: 'tool_result',
232
+ tool_use_id: tu.id,
233
+ content: stringifyResult(result),
234
+ });
235
+ }
236
+ messages.push({ role: 'user', content: resultBlocks });
237
+ steps.push({ text: turn.text || undefined, toolCalls });
238
+ }
239
+ // Step budget exhausted while the model was still calling tools.
240
+ const output = lastText ||
241
+ t('ai-agents.stepBudgetExhausted', undefined, {
242
+ defaultValue: 'Agent stopped: step budget exhausted before a final answer.',
243
+ });
244
+ return { output, steps, usage };
245
+ }
246
+ catch (error) {
247
+ throw new AgentRunError(error instanceof Error ? error.message : String(error), usage, steps, { cause: error });
248
+ }
249
+ },
250
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Module augmentation: extends the core `@molecule/api-ai-agents` contract's
3
+ * `AgentRunInput` with two options this bond's `run()` honors.
4
+ *
5
+ * @remarks
6
+ * Both fields are ADDITIVE ONLY — they never widen or erase an existing
7
+ * `AgentRunInput` field — and are consumed exclusively by this bond's `run()`
8
+ * implementation; a different `AIAgentsProvider` bond is free to ignore them.
9
+ * Any file that imports `@molecule/api-ai-agents-llm` (directly, or
10
+ * transitively by bonding its `provider`) sees the merged `AgentRunInput`
11
+ * type, matching the shape `@molecule/api-ai`'s `ChatParams` already accepts
12
+ * for `stream` and `cacheControl`.
13
+ *
14
+ * @module
15
+ */
16
+ import '@molecule/api-ai-agents';
17
+ declare module '@molecule/api-ai-agents' {
18
+ interface AgentRunInput {
19
+ /**
20
+ * Whether each model turn streams its response (default `true`).
21
+ *
22
+ * The loop previously hardcoded `stream: false` on every turn even
23
+ * though `onEvent` exists specifically as a live hook — with streaming
24
+ * off, every provider bond parses the FULL response in one shot, so
25
+ * `onEvent` only ever fires once per turn instead of incrementally.
26
+ * That also risks HTTP timeouts on a large non-streamed turn (e.g. a
27
+ * big tool input at a raised `maxTokens`) — exactly the case provider
28
+ * docs recommend streaming to avoid. `drainStream` already handles
29
+ * streamed events correctly (text accumulation, `tool_use`, `done` all
30
+ * fold the same way), so there is no extra work to opt in. Set `false`
31
+ * to force non-streaming (e.g. a provider/proxy that cannot forward
32
+ * SSE) — `onEvent` then only fires once per turn.
33
+ */
34
+ stream?: boolean;
35
+ /**
36
+ * Prompt-cache breakpoint hint passed through to `ai.chat()` on every
37
+ * turn. The loop's `system` + `tools` prefix is identical across turns,
38
+ * so caching it avoids re-billing the full (growing) prompt on each of
39
+ * up to `maxSteps` turns. Only providers that support prompt caching
40
+ * (e.g. `@molecule/api-ai-anthropic`) act on it; others ignore it.
41
+ */
42
+ cacheControl?: {
43
+ type: 'ephemeral';
44
+ };
45
+ }
46
+ }
47
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,yBAAyB,CAAA;AAEhC,OAAO,QAAQ,yBAAyB,CAAC;IACvC,UAAU,aAAa;QACrB;;;;;;;;;;;;;;WAcG;QACH,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB;;;;;;WAMG;QACH,YAAY,CAAC,EAAE;YAAE,IAAI,EAAE,WAAW,CAAA;SAAE,CAAA;KACrC;CACF"}
package/dist/types.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Module augmentation: extends the core `@molecule/api-ai-agents` contract's
3
+ * `AgentRunInput` with two options this bond's `run()` honors.
4
+ *
5
+ * @remarks
6
+ * Both fields are ADDITIVE ONLY — they never widen or erase an existing
7
+ * `AgentRunInput` field — and are consumed exclusively by this bond's `run()`
8
+ * implementation; a different `AIAgentsProvider` bond is free to ignore them.
9
+ * Any file that imports `@molecule/api-ai-agents-llm` (directly, or
10
+ * transitively by bonding its `provider`) sees the merged `AgentRunInput`
11
+ * type, matching the shape `@molecule/api-ai`'s `ChatParams` already accepts
12
+ * for `stream` and `cacheControl`.
13
+ *
14
+ * @module
15
+ */
16
+ // Side-effect import: makes this file a module augmenting
17
+ // `@molecule/api-ai-agents` rather than a standalone global script.
18
+ import '@molecule/api-ai-agents';
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@molecule/api-ai-agents-llm",
3
+ "version": "1.0.0",
4
+ "description": "LLM-backed AI agents provider for molecule.dev — a tool-calling loop over the swappable @molecule/api-ai chat bond",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "molecule",
24
+ "ai",
25
+ "agents",
26
+ "tools",
27
+ "agentic",
28
+ "llm"
29
+ ],
30
+ "license": "Apache-2.0",
31
+ "peerDependencies": {
32
+ "@molecule/api-ai": "^1.0.0",
33
+ "@molecule/api-ai-agents": "^1.0.0",
34
+ "@molecule/api-i18n": "^1.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@molecule/api-ai": "1.0.0",
38
+ "@molecule/api-ai-agents": "1.0.0",
39
+ "@molecule/api-bond": "1.0.0",
40
+ "@molecule/api-i18n": "1.0.0",
41
+ "@types/node": "26.1.2",
42
+ "typescript": "6.0.3",
43
+ "vitest": "4.1.10"
44
+ }
45
+ }