@aragon-agent/core 0.1.3
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/CHANGELOG.md +126 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/engine/agent-loop.d.ts +52 -0
- package/dist/engine/agent-loop.js +260 -0
- package/dist/engine/agent.d.ts +129 -0
- package/dist/engine/agent.js +283 -0
- package/dist/engine/message-manager.d.ts +26 -0
- package/dist/engine/message-manager.js +39 -0
- package/dist/engine/steering.d.ts +27 -0
- package/dist/engine/steering.js +51 -0
- package/dist/engine/watchdog.d.ts +24 -0
- package/dist/engine/watchdog.js +50 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +63 -0
- package/dist/llm/model-registry.d.ts +51 -0
- package/dist/llm/model-registry.js +277 -0
- package/dist/llm/provider.d.ts +72 -0
- package/dist/llm/provider.js +77 -0
- package/dist/llm/providers/anthropic.d.ts +23 -0
- package/dist/llm/providers/anthropic.js +420 -0
- package/dist/llm/providers/google.d.ts +22 -0
- package/dist/llm/providers/google.js +336 -0
- package/dist/llm/providers/index.d.ts +67 -0
- package/dist/llm/providers/index.js +127 -0
- package/dist/llm/providers/openai.d.ts +22 -0
- package/dist/llm/providers/openai.js +353 -0
- package/dist/llm/stream-utils.d.ts +33 -0
- package/dist/llm/stream-utils.js +132 -0
- package/dist/llm/types.d.ts +129 -0
- package/dist/llm/types.js +17 -0
- package/dist/sandbox/console-capture.d.ts +29 -0
- package/dist/sandbox/console-capture.js +56 -0
- package/dist/sandbox/http-bridge.d.ts +47 -0
- package/dist/sandbox/http-bridge.js +143 -0
- package/dist/sandbox/isolated-vm-sandbox.d.ts +50 -0
- package/dist/sandbox/isolated-vm-sandbox.js +226 -0
- package/dist/sandbox/tool-bridge.d.ts +42 -0
- package/dist/sandbox/tool-bridge.js +98 -0
- package/dist/sandbox/types.d.ts +84 -0
- package/dist/sandbox/types.js +8 -0
- package/dist/skills/constants.d.ts +122 -0
- package/dist/skills/constants.js +179 -0
- package/dist/skills/disclosure.d.ts +146 -0
- package/dist/skills/disclosure.js +788 -0
- package/dist/skills/frontmatter.d.ts +27 -0
- package/dist/skills/frontmatter.js +177 -0
- package/dist/skills/index.d.ts +21 -0
- package/dist/skills/index.js +18 -0
- package/dist/skills/skill-find-tool.d.ts +41 -0
- package/dist/skills/skill-find-tool.js +117 -0
- package/dist/skills/skill-registry.d.ts +96 -0
- package/dist/skills/skill-registry.js +208 -0
- package/dist/skills/skill-tool.d.ts +44 -0
- package/dist/skills/skill-tool.js +96 -0
- package/dist/skills/tool-policy.d.ts +91 -0
- package/dist/skills/tool-policy.js +272 -0
- package/dist/skills/types.d.ts +271 -0
- package/dist/skills/types.js +8 -0
- package/dist/skills/validate.d.ts +37 -0
- package/dist/skills/validate.js +264 -0
- package/dist/tools/executor.d.ts +54 -0
- package/dist/tools/executor.js +206 -0
- package/dist/tools/helpers.d.ts +69 -0
- package/dist/tools/helpers.js +87 -0
- package/dist/tools/registry.d.ts +58 -0
- package/dist/tools/registry.js +119 -0
- package/dist/tools/types.d.ts +140 -0
- package/dist/tools/types.js +7 -0
- package/dist/tools/validator.d.ts +46 -0
- package/dist/tools/validator.js +210 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +7 -0
- package/package.json +115 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@aragon-agent/core` are documented here. Entries at
|
|
4
|
+
`0.1.x` and earlier refer to the package by its former name, `@argon-agent/core`
|
|
5
|
+
— those releases really were published under that name, so they are left as they
|
|
6
|
+
were written.
|
|
7
|
+
|
|
8
|
+
## 0.2.0
|
|
9
|
+
|
|
10
|
+
### Breaking
|
|
11
|
+
|
|
12
|
+
- **The package is renamed `@argon-agent/core` → `@aragon-agent/core`.** The
|
|
13
|
+
brand was always meant to be *Aragon*, matching AragonMesh; the missing `a`
|
|
14
|
+
was a typo that reached the registry. `@argon-agent/core` is deprecated and
|
|
15
|
+
receives no further releases.
|
|
16
|
+
|
|
17
|
+
Every subpath keeps its shape — only the scope changes:
|
|
18
|
+
|
|
19
|
+
```diff
|
|
20
|
+
- import { Agent } from '@argon-agent/core';
|
|
21
|
+
+ import { Agent } from '@aragon-agent/core';
|
|
22
|
+
- import type { LlmMessage } from '@argon-agent/core/llm/types';
|
|
23
|
+
+ import type { LlmMessage } from '@aragon-agent/core/llm/types';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **The exported alias `ArgonAgent` is now `AragonAgent`.** It remains an alias
|
|
27
|
+
of `Agent`, which is unchanged — code importing `Agent` needs no edit.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Skill system (pure-logic half).** New `src/skills/` module and a `./skills`
|
|
32
|
+
subpath export: `SkillRegistry`, `parseFrontmatter`,
|
|
33
|
+
`validateSkillFrontmatter`, `validateStagedSkill`, `renderSkillCatalog`,
|
|
34
|
+
`renderSkillBody`, `renderSkillInvocation`, `applySkillArguments`,
|
|
35
|
+
`suggestSkillNames`, `sanitizeForPromptBlock`, `createSkillTool`, and the
|
|
36
|
+
`SKILL_*` budget constants. The root runtime surface grows from 33 to 56
|
|
37
|
+
value exports; see `API.md`.
|
|
38
|
+
- **`rankCatalogRecords(records, { usage, now })`** — orders the Level 1 catalog
|
|
39
|
+
by scope, then by usage recency and frequency, using integer buckets so the
|
|
40
|
+
result stays byte-reproducible. Invariant: called without a usage map it is
|
|
41
|
+
element-for-element identical to `catalogRecords()`, which is what keeps every
|
|
42
|
+
existing catalog snapshot valid.
|
|
43
|
+
- **`createSkillFindTool(deps)` / `renderSkillFindResults(query, matched, opts)`**
|
|
44
|
+
— the `skill_find` tool and its rendering, for reaching skills that did not fit
|
|
45
|
+
in the catalog. Searches only the injected registry: no network, no
|
|
46
|
+
`activation: manual` skills, and an explicit refusal to invent an install
|
|
47
|
+
source when nothing matches. Same sanitisation and byte budget discipline as
|
|
48
|
+
the catalog (`SKILL_FIND_MAX_BYTES`).
|
|
49
|
+
- Types `SkillIntegrity`, `SkillUsageStat`, `SkillUsageMap`, `SkillFindOptions`;
|
|
50
|
+
`SkillRecord.integrity`; optional `SkillManifestSource.resolvedRef` and
|
|
51
|
+
`SkillManifest.updatedAt` / `previousVersion` (additive — the manifest schema
|
|
52
|
+
version is deliberately unchanged so an older CLI still reads them).
|
|
53
|
+
- All filesystem access goes through the injected `SkillHost` port, so the
|
|
54
|
+
package still imports no `node:*` builtin outside the one grandfathered
|
|
55
|
+
`node:crypto` in `llm/providers/google.ts`.
|
|
56
|
+
- **Release tooling for the scope change.** `npm run verify:brand` derives the
|
|
57
|
+
list of files that would enter the tarball from `npm pack --dry-run --json`
|
|
58
|
+
and refuses the release if any of them still mentions the old brand — README
|
|
59
|
+
included, with only the four documented exceptions. It runs inside
|
|
60
|
+
`publish-latest.ps1` and cannot be skipped. `npm run preflight` answers "would
|
|
61
|
+
a real publish succeed?" before any version file is touched, and
|
|
62
|
+
`npm run verify:published` checks afterwards that the registry really serves
|
|
63
|
+
the renamed package, README and repository link included.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- `no-host-coupling.test.ts` now also asserts that **no** file under `src/`
|
|
68
|
+
imports a `node:*` builtin, with a single documented allowlist entry and a
|
|
69
|
+
self-check so the assertion cannot pass vacuously. This guarantee was
|
|
70
|
+
previously assumed but never actually checked.
|
|
71
|
+
- Added a repository-level PowerShell release workflow that versions, verifies,
|
|
72
|
+
and publishes Core before the dependent CLI, with dry-run rollback and
|
|
73
|
+
partial-release resume support.
|
|
74
|
+
|
|
75
|
+
### Security
|
|
76
|
+
|
|
77
|
+
- Every span of third-party text is passed through `sanitizeForPromptBlock()`
|
|
78
|
+
before it is concatenated into a tagged prompt block. Without it a skill
|
|
79
|
+
description containing `</available_skills>` could close the block and have
|
|
80
|
+
the text after it read as top-level system-prompt instructions.
|
|
81
|
+
- All skill budgets are measured in UTF-8 **bytes**. A character-based budget
|
|
82
|
+
is ~3x under-counted for CJK text, which pushed results past `ToolExecutor`'s
|
|
83
|
+
100 000-byte ceiling and truncated the closing tag off the block.
|
|
84
|
+
|
|
85
|
+
## 0.1.0
|
|
86
|
+
|
|
87
|
+
Initial extraction of the ArgonAgent engine from the AragonMesh code base
|
|
88
|
+
(`server/agent-core/`) into a standalone, publishable package.
|
|
89
|
+
|
|
90
|
+
- Zero-coupling agent engine: LLM provider adapters (Anthropic, OpenAI,
|
|
91
|
+
Google), streaming, provider registry, model registry.
|
|
92
|
+
- Tool system: typed tool definitions, registry, JSON-Schema validator (with
|
|
93
|
+
built-in fallback), executor.
|
|
94
|
+
- Engine: multi-turn agent loop, message manager, steering queue, idle
|
|
95
|
+
watchdog.
|
|
96
|
+
- Optional isolated-vm CodeAct sandbox (lazy-loaded).
|
|
97
|
+
- Adds the `ArgonAgent` export as an alias of the `Agent` class.
|
|
98
|
+
|
|
99
|
+
Publish-readiness (still part of the unreleased `0.1.0`):
|
|
100
|
+
|
|
101
|
+
- Added `publishConfig.access:"public"` (required for scoped public publish),
|
|
102
|
+
a `prepublishOnly` build guard, and a package-directory `LICENSE`.
|
|
103
|
+
- `files` now ships `CHANGELOG.md` alongside `dist`.
|
|
104
|
+
- Added `typesVersions` so consumers on the legacy `moduleResolution:"node"`
|
|
105
|
+
can resolve the subpath type declarations.
|
|
106
|
+
- Declared `sideEffects:false` (the barrel is pure re-exports) to aid
|
|
107
|
+
downstream tree-shaking.
|
|
108
|
+
- Added discovery metadata: `keywords` / `author` / `repository` / `homepage`
|
|
109
|
+
/ `bugs`.
|
|
110
|
+
- Added a compile-checked `examples/minimal.ts` usage sample (not shipped in the
|
|
111
|
+
tarball).
|
|
112
|
+
|
|
113
|
+
Reuse-verification harness (still part of the unreleased `0.1.0`):
|
|
114
|
+
|
|
115
|
+
- Added `examples/consumer-smoke/run.mjs` + the `verify:dist` script: runs the
|
|
116
|
+
built `dist/` like an external consumer (Node package self-referencing →
|
|
117
|
+
`exports` map → `dist/`), driving `ArgonAgent` end-to-end with an offline stub
|
|
118
|
+
provider and asserting every `exports` subpath resolves at runtime.
|
|
119
|
+
- Added a public-API contract snapshot (`src/__tests__/public-api.test.ts` +
|
|
120
|
+
`API.md`) that freezes the runtime export surface against silent drift.
|
|
121
|
+
- Added a `no-host-coupling` guard test that forbids host-repo imports
|
|
122
|
+
(`@server/`, `@shared/`, `@/`, out-of-package `../../../`) and legacy-brand
|
|
123
|
+
leakage on the public surface.
|
|
124
|
+
- Added a publish guard: `scripts/check-publishable.mjs` (wired into
|
|
125
|
+
`prepublishOnly`) hard-blocks publishing while the `<ORG>` placeholder remains
|
|
126
|
+
in `package.json`, and `scripts/stamp-org.mjs` replaces it in one command.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AragonAgent contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# @aragon-agent/core
|
|
2
|
+
|
|
3
|
+
The publishable **AragonAgent** engine: a zero-coupling, dependency-injected
|
|
4
|
+
TypeScript agent runtime.
|
|
5
|
+
|
|
6
|
+
## Requirements
|
|
7
|
+
|
|
8
|
+
- **Node.js ≥ 18** — the engine relies on the global `fetch` / `AbortController`
|
|
9
|
+
built into Node 18+.
|
|
10
|
+
- **ESM-only** — this package ships as native ES modules (`"type": "module"`).
|
|
11
|
+
Consume it with `import`; it cannot be `require()`-d from CommonJS.
|
|
12
|
+
- **Optional native deps** — `ajv` and `isolated-vm` are `optionalDependencies`:
|
|
13
|
+
the package works without them (a built-in validator fallback is used, and the
|
|
14
|
+
sandbox modules are lazy-loaded). Install `isolated-vm` only if you use the
|
|
15
|
+
`@aragon-agent/core/sandbox/*` CodeAct sandbox.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @aragon-agent/core
|
|
21
|
+
# optional — only if you use the isolated-vm CodeAct sandbox:
|
|
22
|
+
npm install isolated-vm
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`ajv` and `isolated-vm` are **optional dependencies**:
|
|
26
|
+
|
|
27
|
+
- `ajv` — used by the tool param validator when present; a built-in fallback
|
|
28
|
+
validator is used otherwise.
|
|
29
|
+
- `isolated-vm` — used only by the `@aragon-agent/core/sandbox/*` modules, which
|
|
30
|
+
are lazy-loaded and never imported from the package root (`isolated-vm` is
|
|
31
|
+
incompatible with some embedded V8 runtimes such as Electron's).
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { AragonAgent, getProviderRegistry, initProviders } from '@aragon-agent/core';
|
|
37
|
+
|
|
38
|
+
initProviders();
|
|
39
|
+
|
|
40
|
+
const agent = new AragonAgent({
|
|
41
|
+
systemPrompt: 'You are a coding agent.',
|
|
42
|
+
model: { providerId: 'anthropic', modelId: 'claude-...', baseUrl: '...' },
|
|
43
|
+
tools: [],
|
|
44
|
+
providerRegistry: getProviderRegistry(),
|
|
45
|
+
getApiKey: () => process.env.ANTHROPIC_API_KEY,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await agent.prompt('Hello');
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`AragonAgent` is an alias of the engine's `Agent` class.
|
|
52
|
+
|
|
53
|
+
A complete, type-checked version of this snippet (a dummy tool + provider setup
|
|
54
|
+
+ agent construction) lives in [`examples/minimal.ts`](./examples/minimal.ts).
|
|
55
|
+
It is compile-only (never hits the network) and is not part of the published
|
|
56
|
+
tarball.
|
|
57
|
+
|
|
58
|
+
## Entry points
|
|
59
|
+
|
|
60
|
+
| Import path | Contents |
|
|
61
|
+
| -------------------------------- | ----------------------------------------- |
|
|
62
|
+
| `@aragon-agent/core` | Full barrel (engine, LLM, tools, types). |
|
|
63
|
+
| `@aragon-agent/core/llm/types` | LLM message / streaming / model types. |
|
|
64
|
+
| `@aragon-agent/core/llm/providers`| Provider registry + `streamLLM`/`completeLLM`. |
|
|
65
|
+
| `@aragon-agent/core/tools/types` | Tool system types. |
|
|
66
|
+
| `@aragon-agent/core/tools/helpers`| `textResult` / `errorResult` / `defineTool`. |
|
|
67
|
+
| `@aragon-agent/core/sandbox/*` | Optional isolated-vm CodeAct sandbox. |
|
|
68
|
+
|
|
69
|
+
## Build
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm run build # tsc → dist/*.js + *.d.ts
|
|
73
|
+
npm test # vitest (smoke + public-api contract + no-host-coupling guard)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Verifying reuse
|
|
77
|
+
|
|
78
|
+
`npm test` and `examples/minimal.ts` only prove the package **compiles**. To
|
|
79
|
+
prove the built `dist/` can actually be **run** by an external consumer:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run verify:dist # build dist/, then run examples/consumer-smoke/run.mjs
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`run.mjs` imports this package **by its own name** (`@aragon-agent/core`) via
|
|
86
|
+
Node's *package self-referencing*, so Node resolves it through
|
|
87
|
+
`package.json#exports` → `./dist/index.js` — the same `(exports map + NodeNext +
|
|
88
|
+
.js extension)` path that an external project's `npm install @aragon-agent/core`
|
|
89
|
+
hits, **without** placing the package in `node_modules`. It injects an offline
|
|
90
|
+
stub LLM provider, runs one `AragonAgent.prompt()` turn end-to-end (zero network),
|
|
91
|
+
and asserts the `exports` subpaths all resolve at runtime. Success prints
|
|
92
|
+
`[consumer-smoke] OK`.
|
|
93
|
+
|
|
94
|
+
For the unambiguous "another project installs and reuses it" check (which also
|
|
95
|
+
covers running **without** the optional `ajv` / `isolated-vm` deps), see the
|
|
96
|
+
pack-into-a-fresh-project golden verification in
|
|
97
|
+
[`examples/consumer-smoke/README.md`](./examples/consumer-smoke/README.md).
|
|
98
|
+
|
|
99
|
+
The runtime public-API surface is frozen by `src/__tests__/public-api.test.ts`
|
|
100
|
+
and documented for humans in [`API.md`](./API.md); a `no-host-coupling` test
|
|
101
|
+
keeps the package free of host-repo imports and legacy brand leakage.
|
|
102
|
+
|
|
103
|
+
## Publishing
|
|
104
|
+
|
|
105
|
+
This package is published to the public npm registry. Run the release from the
|
|
106
|
+
**package directory** so the package-level `prepublishOnly` hook fires and a
|
|
107
|
+
fresh `dist/` is built:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cd packages/core
|
|
111
|
+
npm run build # produce dist/
|
|
112
|
+
npm pack --dry-run --json # inspect the tarball file list (no upload)
|
|
113
|
+
npm publish # publishConfig.access:"public" is baked in
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
(From the repo root you can equivalently run `npm publish -w packages/core`.)
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
|
|
120
|
+
- **`publishConfig.access` is set to `"public"`**, so no `--access public` flag
|
|
121
|
+
is needed — scoped packages would otherwise default to restricted/private.
|
|
122
|
+
- **The publishing account must own the `@aragon-agent` npm scope/org** before
|
|
123
|
+
the first publish. Even with `access:"public"`, publishing under a scope you
|
|
124
|
+
do not own fails with 403/404. Create the `@aragon-agent` org on npmjs (or
|
|
125
|
+
switch to a scope your account owns and rename the package accordingly).
|
|
126
|
+
- **`files` is the load-bearing whitelist.** The repo's `.gitignore` ignores
|
|
127
|
+
`dist/`, but npm's `files: ["dist", "CHANGELOG.md"]` takes precedence and
|
|
128
|
+
ships the built output. Do not "simplify" the `files` field away, or the
|
|
129
|
+
published tarball will be empty. `npm pack --dry-run --json` should always
|
|
130
|
+
list `dist/index.js`.
|
|
131
|
+
- Repository metadata points to
|
|
132
|
+
`https://github.com/xdrshjr/AragonAgent`.
|
|
133
|
+
|
|
134
|
+
If the repository moves, use the helper script or update
|
|
135
|
+
`repository` / `homepage` / `bugs` before publishing:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
node scripts/stamp-org.mjs <org> # or: ARAGON_REPO_ORG=<org> node scripts/stamp-org.mjs
|
|
139
|
+
node scripts/check-publishable.mjs # exits non-zero if the placeholder remains in package.json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`prepublishOnly` runs `check-publishable.mjs` before the build, so any publish
|
|
143
|
+
attempt with an unstamped placeholder is hard-blocked automatically.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Agent Loop — the heart of the engine.
|
|
3
|
+
*
|
|
4
|
+
* Implements the LLM call -> stream consumption -> tool execution -> loop
|
|
5
|
+
* cycle, with steering/follow-up checkpoints and optional CodeAct code
|
|
6
|
+
* block detection and execution.
|
|
7
|
+
*/
|
|
8
|
+
import type { ThinkingLevel } from '../llm/types.js';
|
|
9
|
+
import type { ProviderRegistry } from '../llm/providers/index.js';
|
|
10
|
+
import type { ToolExecutor } from '../tools/executor.js';
|
|
11
|
+
import type { ToolRegistry } from '../tools/registry.js';
|
|
12
|
+
import type { AgentEvent } from '../types.js';
|
|
13
|
+
import type { MessageManager } from './message-manager.js';
|
|
14
|
+
import type { MessageQueueManager } from './steering.js';
|
|
15
|
+
import type { CodeActSandbox } from '../sandbox/types.js';
|
|
16
|
+
export type { CodeActSandbox };
|
|
17
|
+
export type { ExecuteResult as CodeActResult } from '../sandbox/types.js';
|
|
18
|
+
export interface ModelRef {
|
|
19
|
+
providerId: string;
|
|
20
|
+
modelId: string;
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AgentLoopContext {
|
|
24
|
+
providerRegistry: ProviderRegistry;
|
|
25
|
+
toolRegistry: ToolRegistry;
|
|
26
|
+
toolExecutor: ToolExecutor;
|
|
27
|
+
messageManager: MessageManager;
|
|
28
|
+
messageQueueManager: MessageQueueManager;
|
|
29
|
+
getApiKey: (providerId: string) => string | undefined;
|
|
30
|
+
emit: (event: AgentEvent) => void;
|
|
31
|
+
sandbox?: CodeActSandbox;
|
|
32
|
+
model: ModelRef;
|
|
33
|
+
systemPrompt: string;
|
|
34
|
+
thinkingLevel?: ThinkingLevel;
|
|
35
|
+
/** Maximum output tokens per LLM call. `undefined` = use provider default. */
|
|
36
|
+
maxTokens?: number;
|
|
37
|
+
signal: AbortSignal;
|
|
38
|
+
timeouts: {
|
|
39
|
+
llmCallTimeout: number;
|
|
40
|
+
toolTimeout: number;
|
|
41
|
+
codeTimeout: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Run the Agent loop until completion, steering exhaustion, or abort.
|
|
46
|
+
*
|
|
47
|
+
* This function is invoked by `Agent.prompt()` and `Agent.continue()`.
|
|
48
|
+
* It mutates the messageManager and emits events through the provided
|
|
49
|
+
* context. All errors are caught — the caller (Agent) is responsible
|
|
50
|
+
* for emitting `agent_end` in its own finally block.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runAgentLoop(ctx: AgentLoopContext): Promise<void>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Agent Loop — the heart of the engine.
|
|
3
|
+
*
|
|
4
|
+
* Implements the LLM call -> stream consumption -> tool execution -> loop
|
|
5
|
+
* cycle, with steering/follow-up checkpoints and optional CodeAct code
|
|
6
|
+
* block detection and execution.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// CodeAct regex for extracting ```execute-js blocks
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const CODEACT_REGEX = /```execute-js\n([\s\S]*?)```/g;
|
|
12
|
+
/**
|
|
13
|
+
* Extract all execute-js code blocks from assistant text.
|
|
14
|
+
*/
|
|
15
|
+
function extractCodeBlocks(text) {
|
|
16
|
+
const blocks = [];
|
|
17
|
+
let match;
|
|
18
|
+
// Reset lastIndex for safety when reusing the regex.
|
|
19
|
+
CODEACT_REGEX.lastIndex = 0;
|
|
20
|
+
while ((match = CODEACT_REGEX.exec(text)) !== null) {
|
|
21
|
+
const code = match[1];
|
|
22
|
+
if (code && code.trim().length > 0) {
|
|
23
|
+
blocks.push(code);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return blocks;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Collect all text content from an AssistantMessage's content blocks.
|
|
30
|
+
*/
|
|
31
|
+
function getAssistantText(message) {
|
|
32
|
+
return message.content
|
|
33
|
+
.filter((b) => b.type === 'text')
|
|
34
|
+
.map((b) => b.text)
|
|
35
|
+
.join('\n');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Collect tool call blocks from an AssistantMessage.
|
|
39
|
+
*/
|
|
40
|
+
function getToolCalls(message) {
|
|
41
|
+
return message.content.filter((b) => b.type === 'tool_call');
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// runAgentLoop
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Run the Agent loop until completion, steering exhaustion, or abort.
|
|
48
|
+
*
|
|
49
|
+
* This function is invoked by `Agent.prompt()` and `Agent.continue()`.
|
|
50
|
+
* It mutates the messageManager and emits events through the provided
|
|
51
|
+
* context. All errors are caught — the caller (Agent) is responsible
|
|
52
|
+
* for emitting `agent_end` in its own finally block.
|
|
53
|
+
*/
|
|
54
|
+
export async function runAgentLoop(ctx) {
|
|
55
|
+
while (!ctx.signal.aborted) {
|
|
56
|
+
// ----- Steering checkpoint (before LLM call) -----
|
|
57
|
+
if (ctx.messageQueueManager.hasSteering()) {
|
|
58
|
+
const steeringMessages = ctx.messageQueueManager.drainSteering();
|
|
59
|
+
for (const text of steeringMessages) {
|
|
60
|
+
const userMsg = { role: 'user', content: text, timestamp: Date.now() };
|
|
61
|
+
ctx.messageManager.push(userMsg);
|
|
62
|
+
}
|
|
63
|
+
// Continue to LLM call with injected steering messages.
|
|
64
|
+
}
|
|
65
|
+
// ----- Turn start -----
|
|
66
|
+
ctx.emit({ type: 'turn_start' });
|
|
67
|
+
// ----- Build LLM request -----
|
|
68
|
+
const apiKey = ctx.getApiKey(ctx.model.providerId);
|
|
69
|
+
if (!apiKey) {
|
|
70
|
+
throw new Error(`No API key available for provider "${ctx.model.providerId}". ` +
|
|
71
|
+
'Please configure the API key in Settings.');
|
|
72
|
+
}
|
|
73
|
+
const toolDefs = ctx.toolRegistry.toToolDefinitions();
|
|
74
|
+
const messages = ctx.messageManager.getAll();
|
|
75
|
+
// ----- Stream LLM response -----
|
|
76
|
+
let assistantMessage;
|
|
77
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
78
|
+
const stream = ctx.providerRegistry.stream(ctx.model.providerId, {
|
|
79
|
+
model: ctx.model.modelId,
|
|
80
|
+
messages,
|
|
81
|
+
systemPrompt: ctx.systemPrompt,
|
|
82
|
+
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
83
|
+
apiKey,
|
|
84
|
+
baseUrl: ctx.model.baseUrl,
|
|
85
|
+
signal: ctx.signal,
|
|
86
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
87
|
+
maxTokens: ctx.maxTokens,
|
|
88
|
+
});
|
|
89
|
+
for await (const event of stream) {
|
|
90
|
+
if (ctx.signal.aborted)
|
|
91
|
+
break;
|
|
92
|
+
// Forward every stream event to subscribers.
|
|
93
|
+
ctx.emit({ type: 'message_update', streamEvent: event });
|
|
94
|
+
if (event.type === 'done') {
|
|
95
|
+
assistantMessage = event.message;
|
|
96
|
+
usage = event.usage;
|
|
97
|
+
}
|
|
98
|
+
else if (event.type === 'error') {
|
|
99
|
+
throw event.error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (ctx.signal.aborted)
|
|
103
|
+
break;
|
|
104
|
+
if (!assistantMessage) {
|
|
105
|
+
// Stream ended without a 'done' event — treat as an error.
|
|
106
|
+
throw new Error('LLM stream ended without producing a done event');
|
|
107
|
+
}
|
|
108
|
+
// ----- Turn end -----
|
|
109
|
+
ctx.emit({ type: 'turn_end', message: assistantMessage, usage });
|
|
110
|
+
ctx.messageManager.push(assistantMessage);
|
|
111
|
+
// ----- Process tool calls -----
|
|
112
|
+
const toolCalls = getToolCalls(assistantMessage);
|
|
113
|
+
const hasToolCalls = toolCalls.length > 0;
|
|
114
|
+
let steeringInterrupted = false;
|
|
115
|
+
if (hasToolCalls) {
|
|
116
|
+
// Steering checkpoint: if steering arrived during the LLM call,
|
|
117
|
+
// skip tool execution and inject steering messages instead.
|
|
118
|
+
if (ctx.messageQueueManager.hasSteering()) {
|
|
119
|
+
const steeringMessages = ctx.messageQueueManager.drainSteering();
|
|
120
|
+
for (const text of steeringMessages) {
|
|
121
|
+
const userMsg = { role: 'user', content: text, timestamp: Date.now() };
|
|
122
|
+
ctx.messageManager.push(userMsg);
|
|
123
|
+
}
|
|
124
|
+
// Loop continues — LLM will see the steering messages.
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Execute each tool call sequentially.
|
|
128
|
+
for (const toolCall of toolCalls) {
|
|
129
|
+
if (ctx.signal.aborted)
|
|
130
|
+
break;
|
|
131
|
+
// Check steering between individual tool executions.
|
|
132
|
+
if (ctx.messageQueueManager.hasSteering()) {
|
|
133
|
+
// Inject remaining tool results as errors so LLM knows they were skipped.
|
|
134
|
+
const remainingCalls = toolCalls.slice(toolCalls.indexOf(toolCall));
|
|
135
|
+
for (const skipped of remainingCalls) {
|
|
136
|
+
ctx.messageManager.push({
|
|
137
|
+
role: 'tool_result',
|
|
138
|
+
toolCallId: skipped.toolCallId,
|
|
139
|
+
content: 'Tool execution skipped due to steering interrupt.',
|
|
140
|
+
isError: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Inject steering messages.
|
|
144
|
+
const steeringMessages = ctx.messageQueueManager.drainSteering();
|
|
145
|
+
for (const text of steeringMessages) {
|
|
146
|
+
const userMsg = { role: 'user', content: text, timestamp: Date.now() };
|
|
147
|
+
ctx.messageManager.push(userMsg);
|
|
148
|
+
}
|
|
149
|
+
steeringInterrupted = true;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
ctx.emit({
|
|
153
|
+
type: 'tool_execution_start',
|
|
154
|
+
toolCallId: toolCall.toolCallId,
|
|
155
|
+
toolName: toolCall.toolName,
|
|
156
|
+
args: toolCall.args,
|
|
157
|
+
});
|
|
158
|
+
const execResult = await ctx.toolExecutor.execute(toolCall.toolCallId, toolCall.toolName, toolCall.args, ctx.signal);
|
|
159
|
+
ctx.emit({
|
|
160
|
+
type: 'tool_execution_end',
|
|
161
|
+
toolCallId: toolCall.toolCallId,
|
|
162
|
+
toolName: toolCall.toolName,
|
|
163
|
+
result: execResult.result,
|
|
164
|
+
isError: execResult.isError,
|
|
165
|
+
duration: execResult.duration,
|
|
166
|
+
});
|
|
167
|
+
// Build and push tool result message.
|
|
168
|
+
const resultText = execResult.result.content
|
|
169
|
+
.map((c) => (c.type === 'text' ? c.text : `[image: ${c.mediaType}]`))
|
|
170
|
+
.join('\n');
|
|
171
|
+
ctx.messageManager.push({
|
|
172
|
+
role: 'tool_result',
|
|
173
|
+
toolCallId: toolCall.toolCallId,
|
|
174
|
+
content: resultText,
|
|
175
|
+
isError: execResult.isError,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (ctx.signal.aborted)
|
|
179
|
+
break;
|
|
180
|
+
// If steering interrupted tool execution, skip CodeAct and continue
|
|
181
|
+
// to the next loop iteration where the LLM sees the steering messages.
|
|
182
|
+
if (steeringInterrupted)
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// ----- CodeAct code block detection -----
|
|
186
|
+
const assistantText = getAssistantText(assistantMessage);
|
|
187
|
+
const codeBlocks = extractCodeBlocks(assistantText);
|
|
188
|
+
let hasCodeBlocks = codeBlocks.length > 0 && ctx.sandbox != null;
|
|
189
|
+
if (hasCodeBlocks && ctx.sandbox) {
|
|
190
|
+
for (const code of codeBlocks) {
|
|
191
|
+
if (ctx.signal.aborted)
|
|
192
|
+
break;
|
|
193
|
+
ctx.emit({ type: 'code_execution_start', code, language: 'javascript' });
|
|
194
|
+
const startTime = Date.now();
|
|
195
|
+
let output = '';
|
|
196
|
+
let error;
|
|
197
|
+
try {
|
|
198
|
+
const result = await ctx.sandbox.execute(code, {
|
|
199
|
+
timeout: ctx.timeouts.codeTimeout,
|
|
200
|
+
signal: ctx.signal,
|
|
201
|
+
});
|
|
202
|
+
// Build output from console entries and return value.
|
|
203
|
+
const parts = [];
|
|
204
|
+
for (const entry of result.consoleOutput) {
|
|
205
|
+
const prefix = entry.level === 'log' ? '' : `[${entry.level}] `;
|
|
206
|
+
parts.push(`${prefix}${entry.args.join(' ')}`);
|
|
207
|
+
}
|
|
208
|
+
if (result.returnValue !== undefined) {
|
|
209
|
+
parts.push(`=> ${result.returnValue}`);
|
|
210
|
+
}
|
|
211
|
+
output = parts.join('\n');
|
|
212
|
+
error = result.error?.message;
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
error = err instanceof Error ? err.message : String(err);
|
|
216
|
+
}
|
|
217
|
+
const duration = Date.now() - startTime;
|
|
218
|
+
ctx.emit({ type: 'code_execution_end', output, error, duration });
|
|
219
|
+
// Inject code execution result as a user message.
|
|
220
|
+
let resultContent = '[Code Execution Result]\n';
|
|
221
|
+
if (output) {
|
|
222
|
+
resultContent += `Output: ${output}\n`;
|
|
223
|
+
}
|
|
224
|
+
if (error) {
|
|
225
|
+
resultContent += `---\nError: ${error}\n`;
|
|
226
|
+
}
|
|
227
|
+
if (!output && !error) {
|
|
228
|
+
resultContent += 'Output: (no output)\n';
|
|
229
|
+
}
|
|
230
|
+
const codeResultMsg = {
|
|
231
|
+
role: 'user',
|
|
232
|
+
content: resultContent,
|
|
233
|
+
timestamp: Date.now(),
|
|
234
|
+
};
|
|
235
|
+
ctx.messageManager.push(codeResultMsg);
|
|
236
|
+
}
|
|
237
|
+
if (ctx.signal.aborted)
|
|
238
|
+
break;
|
|
239
|
+
// After code execution, continue the loop so LLM can react to results.
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// ----- Exit or follow-up checkpoint -----
|
|
243
|
+
if (!hasToolCalls && !hasCodeBlocks) {
|
|
244
|
+
// No tools and no code blocks — check for follow-up messages.
|
|
245
|
+
if (ctx.messageQueueManager.hasFollowUp()) {
|
|
246
|
+
const followUpMessages = ctx.messageQueueManager.drainFollowUp();
|
|
247
|
+
for (const text of followUpMessages) {
|
|
248
|
+
const userMsg = { role: 'user', content: text, timestamp: Date.now() };
|
|
249
|
+
ctx.messageManager.push(userMsg);
|
|
250
|
+
}
|
|
251
|
+
// Continue loop with follow-up messages.
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
// No more work — exit the loop.
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
// If we had tool calls (and they were executed), continue the loop
|
|
258
|
+
// so the LLM can process tool results.
|
|
259
|
+
}
|
|
260
|
+
}
|