@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +21 -0
  3. package/README.md +147 -0
  4. package/dist/engine/agent-loop.d.ts +52 -0
  5. package/dist/engine/agent-loop.js +260 -0
  6. package/dist/engine/agent.d.ts +129 -0
  7. package/dist/engine/agent.js +283 -0
  8. package/dist/engine/message-manager.d.ts +26 -0
  9. package/dist/engine/message-manager.js +39 -0
  10. package/dist/engine/steering.d.ts +27 -0
  11. package/dist/engine/steering.js +51 -0
  12. package/dist/engine/watchdog.d.ts +24 -0
  13. package/dist/engine/watchdog.js +50 -0
  14. package/dist/index.d.ts +46 -0
  15. package/dist/index.js +63 -0
  16. package/dist/llm/model-registry.d.ts +51 -0
  17. package/dist/llm/model-registry.js +277 -0
  18. package/dist/llm/provider.d.ts +72 -0
  19. package/dist/llm/provider.js +77 -0
  20. package/dist/llm/providers/anthropic.d.ts +23 -0
  21. package/dist/llm/providers/anthropic.js +420 -0
  22. package/dist/llm/providers/google.d.ts +22 -0
  23. package/dist/llm/providers/google.js +336 -0
  24. package/dist/llm/providers/index.d.ts +67 -0
  25. package/dist/llm/providers/index.js +127 -0
  26. package/dist/llm/providers/openai.d.ts +22 -0
  27. package/dist/llm/providers/openai.js +353 -0
  28. package/dist/llm/stream-utils.d.ts +33 -0
  29. package/dist/llm/stream-utils.js +132 -0
  30. package/dist/llm/types.d.ts +129 -0
  31. package/dist/llm/types.js +17 -0
  32. package/dist/sandbox/console-capture.d.ts +29 -0
  33. package/dist/sandbox/console-capture.js +56 -0
  34. package/dist/sandbox/http-bridge.d.ts +47 -0
  35. package/dist/sandbox/http-bridge.js +143 -0
  36. package/dist/sandbox/isolated-vm-sandbox.d.ts +50 -0
  37. package/dist/sandbox/isolated-vm-sandbox.js +226 -0
  38. package/dist/sandbox/tool-bridge.d.ts +42 -0
  39. package/dist/sandbox/tool-bridge.js +98 -0
  40. package/dist/sandbox/types.d.ts +84 -0
  41. package/dist/sandbox/types.js +8 -0
  42. package/dist/skills/constants.d.ts +122 -0
  43. package/dist/skills/constants.js +179 -0
  44. package/dist/skills/disclosure.d.ts +146 -0
  45. package/dist/skills/disclosure.js +788 -0
  46. package/dist/skills/frontmatter.d.ts +27 -0
  47. package/dist/skills/frontmatter.js +177 -0
  48. package/dist/skills/index.d.ts +21 -0
  49. package/dist/skills/index.js +18 -0
  50. package/dist/skills/skill-find-tool.d.ts +41 -0
  51. package/dist/skills/skill-find-tool.js +117 -0
  52. package/dist/skills/skill-registry.d.ts +96 -0
  53. package/dist/skills/skill-registry.js +208 -0
  54. package/dist/skills/skill-tool.d.ts +44 -0
  55. package/dist/skills/skill-tool.js +96 -0
  56. package/dist/skills/tool-policy.d.ts +91 -0
  57. package/dist/skills/tool-policy.js +272 -0
  58. package/dist/skills/types.d.ts +271 -0
  59. package/dist/skills/types.js +8 -0
  60. package/dist/skills/validate.d.ts +37 -0
  61. package/dist/skills/validate.js +264 -0
  62. package/dist/tools/executor.d.ts +54 -0
  63. package/dist/tools/executor.js +206 -0
  64. package/dist/tools/helpers.d.ts +69 -0
  65. package/dist/tools/helpers.js +87 -0
  66. package/dist/tools/registry.d.ts +58 -0
  67. package/dist/tools/registry.js +119 -0
  68. package/dist/tools/types.d.ts +140 -0
  69. package/dist/tools/types.js +7 -0
  70. package/dist/tools/validator.d.ts +46 -0
  71. package/dist/tools/validator.js +210 -0
  72. package/dist/types.d.ts +56 -0
  73. package/dist/types.js +7 -0
  74. 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
+ }