@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 +115 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +60 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/provider.d.ts +25 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +250 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/package.json +45 -0
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 @@
|
|
|
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 {};
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/provider.js
ADDED
|
@@ -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
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|