loopengine 0.0.10 → 0.0.16
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/README.md +306 -423
- package/dist/agent-as-tool.d.ts +7 -0
- package/dist/agent-as-tool.js +32 -0
- package/dist/agent-config.d.ts +139 -9
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +174 -0
- package/dist/discover-agents.d.ts +18 -10
- package/dist/discover-agents.js +87 -26
- package/dist/index.d.ts +11 -10
- package/dist/index.js +6 -5
- package/dist/{anthropic-model-call.d.ts → model-calls/anthropic-model-call.d.ts} +1 -1
- package/dist/{anthropic-model-call.js → model-calls/anthropic-model-call.js} +8 -6
- package/dist/{deepseek-model-call.d.ts → model-calls/deepseek-model-call.d.ts} +1 -1
- package/dist/{openai-model-call.d.ts → model-calls/openai-model-call.d.ts} +1 -1
- package/dist/run-agent.d.ts +31 -1
- package/dist/run-agent.js +219 -8
- package/dist/session-store.d.ts +1 -1
- package/package.json +21 -10
- /package/dist/{deepseek-model-call.js → model-calls/deepseek-model-call.js} +0 -0
- /package/dist/{openai-model-call.js → model-calls/openai-model-call.js} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
import type { ModelCall } from '
|
|
2
|
+
import type { ModelCall } from '#run-agent.js';
|
|
3
3
|
export interface AnthropicModelCallOptions {
|
|
4
4
|
/** Defaults to the ANTHROPIC_API_KEY env var, same as the SDK itself. */
|
|
5
5
|
apiKey?: string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// One of the real ModelCall implementations loopengine ships (see also
|
|
2
|
+
// openai-model-call.ts, deepseek-model-call.ts — the latter is what
|
|
3
|
+
// agents/customer-service/index.ts actually uses). file-agent.ts and
|
|
4
|
+
// rag-agent.ts still use a canned, turn-counting fake so the whole loop
|
|
5
|
+
// is runnable with no API key — swap that fake for
|
|
6
|
+
// createAnthropicModelCall(...) and nothing in run-agent.ts, the
|
|
7
|
+
// adapters, or any AgentConfig changes. ModelCall is the only seam this
|
|
8
|
+
// needed.
|
|
7
9
|
//
|
|
8
10
|
// loopengine's own Message type mirrors Anthropic's native shape closely
|
|
9
11
|
// on purpose: `content` is either a plain string or a ModelContentBlock[]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import type { Message, ModelCall, ModelResponse } from '
|
|
2
|
+
import type { Message, ModelCall, ModelResponse } from '#run-agent.js';
|
|
3
3
|
type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
|
4
4
|
export interface OpenAIModelCallOptions {
|
|
5
5
|
/** Defaults to the OPENAI_API_KEY env var, same as the SDK itself. */
|
package/dist/run-agent.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { RuleSet } from 'actauth';
|
|
2
|
+
import type { AgentConfig, ToolDefinition, ToolSchema } from '#agent-config.js';
|
|
2
3
|
export interface ModelContentBlock {
|
|
3
4
|
type: string;
|
|
4
5
|
/** text blocks. */
|
|
@@ -38,6 +39,13 @@ export type ModelCall = (messages: Message[], system: string, tools: ToolSchema[
|
|
|
38
39
|
export interface RunAgentOptions {
|
|
39
40
|
/** Emitted at each loop step for callers that want visibility (logging, UI, tests). Default: no-op. */
|
|
40
41
|
onEvent?: (event: string, detail: unknown) => void;
|
|
42
|
+
/** The ActAuth tenant this call runs as — feeds the Gate's scope, so it
|
|
43
|
+
* governs which rules apply. runAgent() never sees a request, so it
|
|
44
|
+
* can't call AgentConfig.tenantFor itself; adapters/http.ts resolves it
|
|
45
|
+
* per request and passes the result here. Default `'default'`, same as
|
|
46
|
+
* omitting it — every standalone/CLI caller (which never has a request
|
|
47
|
+
* to resolve tenantFor from) gets that default automatically. */
|
|
48
|
+
tenant?: string;
|
|
41
49
|
}
|
|
42
50
|
export interface RunAgentResult {
|
|
43
51
|
/** Final assistant text for this turn. */
|
|
@@ -59,4 +67,26 @@ export interface RunAgentResult {
|
|
|
59
67
|
* not something the model said. Absent on a normal finish. */
|
|
60
68
|
stopReason?: 'max_turns';
|
|
61
69
|
}
|
|
70
|
+
/** Resolves AgentConfig.rules the same way skillsDirs resolves its own
|
|
71
|
+
* default: an inline array is used as-is (full 3-segment scopePattern,
|
|
72
|
+
* unchanged — see loadYamlRules for why only the YAML form gets the
|
|
73
|
+
* agent segment auto-appended); omitted entirely defaults to
|
|
74
|
+
* `agents/<name>/actauth.yml`, this repo's own folder-form convention.
|
|
75
|
+
* Unlike skillsDirs, a missing *default* file falls back to an empty
|
|
76
|
+
* ruleset rather than propagating the raw ENOENT — permissions failing
|
|
77
|
+
* shouldn't crash the whole call. That fallback defaults to `'deny'`, not
|
|
78
|
+
* the inline-array form's `'ask'`: no `actauth.yml` at all means this
|
|
79
|
+
* agent's permission story was never actually written, which is a
|
|
80
|
+
* stricter unknown than "written, but this one tool has no rule" — 'ask'
|
|
81
|
+
* still runs an Approver that could auto-approve, while 'deny' refuses
|
|
82
|
+
* outright until real rules exist. `defaultDecision`, if set, still wins
|
|
83
|
+
* over that. An *explicitly* given path that doesn't exist is a real
|
|
84
|
+
* configuration bug, though, and still throws — only the silent, implicit
|
|
85
|
+
* default gets this forgiveness.
|
|
86
|
+
*
|
|
87
|
+
* Exported (alongside loadDefaultTools below) so adapters/http.ts's
|
|
88
|
+
* GET /agents/:name/config route can show the same rules runAgent() would
|
|
89
|
+
* actually enforce, not a re-derived guess at them. */
|
|
90
|
+
export declare function loadRules(config: AgentConfig): RuleSet;
|
|
91
|
+
export declare function loadDefaultTools(config: AgentConfig): Promise<ToolDefinition[]>;
|
|
62
92
|
export declare function runAgent(config: AgentConfig, modelCall: ModelCall, userMessage: string, history?: Message[], options?: RunAgentOptions): Promise<RunAgentResult>;
|
package/dist/run-agent.js
CHANGED
|
@@ -3,11 +3,29 @@
|
|
|
3
3
|
// differ. runAgent itself does no I/O and holds no state between calls:
|
|
4
4
|
// callers own conversation history, which is what lets the same function
|
|
5
5
|
// serve a one-shot CLI invocation and a long-lived chat session.
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { parse as parseYaml } from 'yaml';
|
|
6
10
|
import { Gate, RuleSet, ConsoleApprover } from 'actauth';
|
|
7
11
|
import { SkillGarden } from 'skillgarden';
|
|
8
12
|
import { ContextClipper } from 'contextclip';
|
|
9
13
|
import { ToolLane } from 'toollane';
|
|
10
14
|
import { Reflow } from 'reflowkit';
|
|
15
|
+
import { loadAgentModule } from './discover-agents.js';
|
|
16
|
+
import { agentAsTool } from './agent-as-tool.js';
|
|
17
|
+
// Resolved relative to *this file's own location* (via import.meta.url),
|
|
18
|
+
// not process.cwd() — the same reasoning agent-registry.ts's own
|
|
19
|
+
// agentsDir uses. In dev (tsx), this file and agents/ are both source
|
|
20
|
+
// .ts, side by side. In the built dist/, this file is dist/run-agent.js
|
|
21
|
+
// and its sibling agents/ is dist/agents/**/*.js (tsc-compiled) — cwd is
|
|
22
|
+
// irrelevant to either case, only "next to this module" is reliably
|
|
23
|
+
// right in both. skillsDirs/rules don't need this: actauth.yml/SKILL.md
|
|
24
|
+
// are plain data copied verbatim into the image (see Dockerfile), so
|
|
25
|
+
// they exist at the same cwd-relative path either way; a tools/index.ts
|
|
26
|
+
// is real code that only exists compiled, at a different relative
|
|
27
|
+
// location, once dist/ is what's actually running.
|
|
28
|
+
const agentsRootDir = join(dirname(fileURLToPath(import.meta.url)), 'agents');
|
|
11
29
|
/** ContextClip only ever needs a flat string per message to estimate
|
|
12
30
|
* token usage — it doesn't need to understand tool_use/tool_result
|
|
13
31
|
* structure, so this is a one-way, read-only projection, never something
|
|
@@ -29,20 +47,215 @@ function flattenForContextClip(message) {
|
|
|
29
47
|
.join('\n');
|
|
30
48
|
return { role: message.role, content: text };
|
|
31
49
|
}
|
|
50
|
+
/** Parses a loopengine actauth.yml — same shape as examples/actauth.yml
|
|
51
|
+
* in the actauth package, except each rule's `scope` only needs
|
|
52
|
+
* tenant/environment (a wildcard/wildcard, wildcard/staging, or
|
|
53
|
+
* acme-corp/production pair, say) — the agent segment is appended
|
|
54
|
+
* automatically from AgentConfig.name, then handed to actauth's own
|
|
55
|
+
* RuleSet.fromRaw (not fromYamlFile, which
|
|
56
|
+
* expects the full 3-segment scope already baked into the file). A
|
|
57
|
+
* per-agent actauth.yml (this repo's own convention: one file, one
|
|
58
|
+
* agent, path derived from the agent's own name — see
|
|
59
|
+
* agents/customer-service/actauth.yml) can only ever describe rules for
|
|
60
|
+
* that one agent anyway, so repeating its name in every single rule is
|
|
61
|
+
* exactly the kind of boilerplate skillsDirs/rules/tools' own defaults
|
|
62
|
+
* already avoid elsewhere in this file. */
|
|
63
|
+
function loadYamlRules(path, agentName) {
|
|
64
|
+
const raw = (parseYaml(readFileSync(path, 'utf8')) ?? {});
|
|
65
|
+
// A rule already ending in /<agentName> — the old, full 3-segment habit,
|
|
66
|
+
// or examples/actauth.yml copied verbatim — is left alone rather than
|
|
67
|
+
// getting a second, invalid segment appended; only the new 2-segment
|
|
68
|
+
// (tenant/environment) form actually needs the suffix.
|
|
69
|
+
const rules = (raw.rules ?? []).map((r) => ({
|
|
70
|
+
...r,
|
|
71
|
+
scope: r.scope.endsWith(`/${agentName}`) ? r.scope : `${r.scope}/${agentName}`,
|
|
72
|
+
}));
|
|
73
|
+
return RuleSet.fromRaw({ default_decision: raw.default_decision, rules });
|
|
74
|
+
}
|
|
75
|
+
/** Resolves AgentConfig.rules the same way skillsDirs resolves its own
|
|
76
|
+
* default: an inline array is used as-is (full 3-segment scopePattern,
|
|
77
|
+
* unchanged — see loadYamlRules for why only the YAML form gets the
|
|
78
|
+
* agent segment auto-appended); omitted entirely defaults to
|
|
79
|
+
* `agents/<name>/actauth.yml`, this repo's own folder-form convention.
|
|
80
|
+
* Unlike skillsDirs, a missing *default* file falls back to an empty
|
|
81
|
+
* ruleset rather than propagating the raw ENOENT — permissions failing
|
|
82
|
+
* shouldn't crash the whole call. That fallback defaults to `'deny'`, not
|
|
83
|
+
* the inline-array form's `'ask'`: no `actauth.yml` at all means this
|
|
84
|
+
* agent's permission story was never actually written, which is a
|
|
85
|
+
* stricter unknown than "written, but this one tool has no rule" — 'ask'
|
|
86
|
+
* still runs an Approver that could auto-approve, while 'deny' refuses
|
|
87
|
+
* outright until real rules exist. `defaultDecision`, if set, still wins
|
|
88
|
+
* over that. An *explicitly* given path that doesn't exist is a real
|
|
89
|
+
* configuration bug, though, and still throws — only the silent, implicit
|
|
90
|
+
* default gets this forgiveness.
|
|
91
|
+
*
|
|
92
|
+
* Exported (alongside loadDefaultTools below) so adapters/http.ts's
|
|
93
|
+
* GET /agents/:name/config route can show the same rules runAgent() would
|
|
94
|
+
* actually enforce, not a re-derived guess at them. */
|
|
95
|
+
export function loadRules(config) {
|
|
96
|
+
if (Array.isArray(config.rules))
|
|
97
|
+
return new RuleSet(config.rules, config.defaultDecision ?? 'ask');
|
|
98
|
+
const usingDefaultPath = config.rules === undefined;
|
|
99
|
+
const path = config.rules ?? `agents/${config.name}/actauth.yml`;
|
|
100
|
+
try {
|
|
101
|
+
return loadYamlRules(path, config.name);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
if (usingDefaultPath && err.code === 'ENOENT') {
|
|
105
|
+
return new RuleSet([], config.defaultDecision ?? 'deny');
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Resolves AgentConfig.tools the same way loadRules resolves its own
|
|
111
|
+
* default: an explicit array (including `[]`) is used as-is; omitted
|
|
112
|
+
* entirely defaults to importing `agents/<name>/tools/index.{ts,js}`
|
|
113
|
+
* (this repo's own aggregation-file convention — see
|
|
114
|
+
* `agents/customer-service/tools/index.ts`) and using its exported
|
|
115
|
+
* `tools`. A missing file there is not an error — a flat-file agent with
|
|
116
|
+
* no tools folder at all (or one merging in dynamically-loaded tools, like
|
|
117
|
+
* `agents/file-agent/index.ts`'s Composio ones, so it can't just rely on
|
|
118
|
+
* this default) just gets `[]`, same as if it had explicitly set that. A
|
|
119
|
+
* module that *does* exist but throws while importing, or doesn't export
|
|
120
|
+
* `tools` at all, is a real bug and is not swallowed. */
|
|
121
|
+
async function loadToolsFromDir(toolsDir) {
|
|
122
|
+
for (const indexName of ['index.ts', 'index.js']) {
|
|
123
|
+
const indexPath = join(toolsDir, indexName);
|
|
124
|
+
if (!existsSync(indexPath))
|
|
125
|
+
continue;
|
|
126
|
+
const mod = (await import(pathToFileURL(indexPath).href));
|
|
127
|
+
if (!Array.isArray(mod.tools)) {
|
|
128
|
+
throw new Error(`${indexPath} does not export a 'tools' array — every agents/<name>/tools/index file must.`);
|
|
129
|
+
}
|
|
130
|
+
return mod.tools;
|
|
131
|
+
}
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
export async function loadDefaultTools(config) {
|
|
135
|
+
return loadToolsFromDir(join(agentsRootDir, config.name, 'tools'));
|
|
136
|
+
}
|
|
137
|
+
/** Every other folder-form default in this file (loadDefaultTools,
|
|
138
|
+
* loadRules, the skillsDirs default below) resolves against
|
|
139
|
+
* `agents/<name>/` — correct for a top-level agent, where that's exactly
|
|
140
|
+
* where its module lives, but wrong for a subagent: its `config` only
|
|
141
|
+
* carries its own bare name (e.g. 'billing-agent'), not the nested folder
|
|
142
|
+
* loadSubagentTools actually found it in
|
|
143
|
+
* (`agents/support-orchestrator/subagents/billing-agent/`). Left alone,
|
|
144
|
+
* an omitted `tools`/`rules`/`skillsDirs` on a subagent would silently
|
|
145
|
+
* resolve against `agents/billing-agent/...` instead — empty, if no such
|
|
146
|
+
* top-level folder exists, or worse, some *unrelated* top-level agent's
|
|
147
|
+
* tools/rules/skills, if one happens to share the same name.
|
|
148
|
+
*
|
|
149
|
+
* This patches exactly those three fields — only when the subagent's own
|
|
150
|
+
* config left them unset — to resolve against `dir`, its real folder,
|
|
151
|
+
* before it's ever handed to agentAsTool/runAgent. An explicit value on
|
|
152
|
+
* the subagent's own config (its author opted out on purpose) is always
|
|
153
|
+
* left untouched. `rules` gets the same "missing file is fine, empty
|
|
154
|
+
* deny-everything ruleset" fallback loadRules's own default path gets —
|
|
155
|
+
* computed here instead of left to loadRules, since leaving `rules`
|
|
156
|
+
* unset would have it fall through to loadRules's *own* (wrong,
|
|
157
|
+
* name-based) default path instead of this one. */
|
|
158
|
+
async function resolveSubagentConfig(config, dir) {
|
|
159
|
+
const resolved = { ...config };
|
|
160
|
+
if (resolved.tools === undefined) {
|
|
161
|
+
resolved.tools = await loadToolsFromDir(join(dir, 'tools'));
|
|
162
|
+
}
|
|
163
|
+
if (resolved.rules === undefined) {
|
|
164
|
+
const rulesPath = join(dir, 'actauth.yml');
|
|
165
|
+
if (existsSync(rulesPath)) {
|
|
166
|
+
resolved.rules = rulesPath;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
resolved.rules = [];
|
|
170
|
+
resolved.defaultDecision = resolved.defaultDecision ?? 'deny';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (resolved.skillsDirs === undefined) {
|
|
174
|
+
resolved.skillsDirs = [join(dir, 'skills')];
|
|
175
|
+
}
|
|
176
|
+
return resolved;
|
|
177
|
+
}
|
|
178
|
+
/** Auto-loads agents/<name>/subagents/<child>/index.{ts,js} — each one a
|
|
179
|
+
* full AgentConfig, wrapped with agentAsTool and merged into `config`'s
|
|
180
|
+
* own tools, no import or AgentConfig.tools edit required. Unlike
|
|
181
|
+
* loadDefaultTools, this always runs regardless of whether `config.tools`
|
|
182
|
+
* was left to its own default or set explicitly — see AgentConfig.tools's
|
|
183
|
+
* own doc comment for why subagents are a distinct concern from
|
|
184
|
+
* hand-written tools. A missing subagents/ folder is just `[]`, same
|
|
185
|
+
* missing-is-fine treatment tools/ and skills/ get; a subdirectory with
|
|
186
|
+
* neither index.ts nor index.js is skipped, same as resolveModulePath's
|
|
187
|
+
* own handling in discover-agents.ts.
|
|
188
|
+
*
|
|
189
|
+
* Each subagent is loaded via loadAgentModule — the same per-module
|
|
190
|
+
* resolution discoverAgents itself uses — then patched by
|
|
191
|
+
* resolveSubagentConfig (see its own doc comment for why) before being
|
|
192
|
+
* wrapped. Its own `config` goes through this exact same function again
|
|
193
|
+
* the next time *it* runs (inside agentAsTool's execute, via runAgent).
|
|
194
|
+
* That's what makes nesting (a subagent with its own subagents/ folder)
|
|
195
|
+
* work with no extra recursion code: it's just this function being
|
|
196
|
+
* called again, one level down. A folder can't be its own ancestor, so
|
|
197
|
+
* this can't cycle the way a hand-wired agentAsTool(getEntry(...)) call
|
|
198
|
+
* elsewhere could. */
|
|
199
|
+
async function loadSubagentTools(config) {
|
|
200
|
+
const subagentsDir = join(agentsRootDir, config.name, 'subagents');
|
|
201
|
+
if (!existsSync(subagentsDir))
|
|
202
|
+
return [];
|
|
203
|
+
const tools = [];
|
|
204
|
+
for (const dirent of readdirSync(subagentsDir, { withFileTypes: true })) {
|
|
205
|
+
if (!dirent.isDirectory())
|
|
206
|
+
continue;
|
|
207
|
+
const dir = join(subagentsDir, dirent.name);
|
|
208
|
+
const indexPath = ['index.ts', 'index.js'].map((n) => join(dir, n)).find((p) => existsSync(p));
|
|
209
|
+
if (!indexPath)
|
|
210
|
+
continue;
|
|
211
|
+
const label = `agents/${config.name}/subagents/${dirent.name}/index`;
|
|
212
|
+
const subagent = await loadAgentModule(indexPath, label);
|
|
213
|
+
const subagentConfig = await resolveSubagentConfig(subagent.config, dir);
|
|
214
|
+
tools.push(agentAsTool(subagentConfig, subagent.createModelCall));
|
|
215
|
+
}
|
|
216
|
+
return tools;
|
|
217
|
+
}
|
|
32
218
|
export async function runAgent(config, modelCall, userMessage, history = [], options = {}) {
|
|
33
219
|
const log = options.onEvent ?? (() => { });
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
220
|
+
// tenant: resolved by the caller (adapters/http.ts calls
|
|
221
|
+
// AgentConfig.tenantFor with the request's headers/body; every other
|
|
222
|
+
// caller — adapters/cli.ts, a standalone script — has no request to
|
|
223
|
+
// resolve it from, so it never passes this option and gets 'default').
|
|
224
|
+
// environment: a deployment-wide setting,
|
|
225
|
+
// not a per-agent or per-request one — always LOOPENGINE_ENV, same
|
|
226
|
+
// everywhere this process runs, never something an AgentConfig defines.
|
|
227
|
+
const scope = { tenant: options.tenant ?? 'default', environment: process.env.LOOPENGINE_ENV ?? 'production', agent: config.name };
|
|
228
|
+
// Optional on AgentConfig — an agent can have zero tools. Explicit
|
|
229
|
+
// (including `[]`) is used as-is; omitted entirely defaults to
|
|
230
|
+
// importing agents/<name>/tools/index.{ts,js} — see loadDefaultTools's
|
|
231
|
+
// own doc comment for the full reasoning and the cases that can't use it.
|
|
232
|
+
// agents/<name>/subagents/* is merged in on top either way — see
|
|
233
|
+
// loadSubagentTools's own doc comment for why that one isn't gated by
|
|
234
|
+
// whether `tools` was explicit.
|
|
235
|
+
const tools = [...(config.tools ?? (await loadDefaultTools(config))), ...(await loadSubagentTools(config))];
|
|
236
|
+
// Omitted entirely (undefined): default to this agent's own
|
|
237
|
+
// agents/<name>/skills — the folder-form convention every agent in
|
|
238
|
+
// this repo already follows (see agent-config.ts's skillsDirs doc
|
|
239
|
+
// comment). An explicit `[]` is a real opt-out, not "unset," and skips
|
|
240
|
+
// the default — SkillGarden's own missing-directory handling
|
|
241
|
+
// (discoverSkillFiles walks best-effort, no throw) is what makes
|
|
242
|
+
// pointing this at a folder that doesn't exist (a flat-file agent with
|
|
243
|
+
// no skills at all) harmless: an empty index, not an error.
|
|
244
|
+
const skillsDirs = config.skillsDirs ?? [`agents/${config.name}/skills`];
|
|
245
|
+
const skillGarden = skillsDirs.length ? new SkillGarden({ dirs: skillsDirs, indexBudgetTokens: config.skillIndexBudgetTokens ?? 200 }) : null;
|
|
38
246
|
const skillIndex = skillGarden?.buildIndex().included ?? [];
|
|
39
247
|
// Also the tail-preservation window recover() below relies on — kept as
|
|
40
248
|
// one named constant so the two can never drift out of sync.
|
|
41
249
|
const tailMessages = 4;
|
|
42
250
|
const contextClip = new ContextClipper({ budgetTokens: config.contextBudgetTokens ?? 8000, tailMessages });
|
|
43
|
-
const rules =
|
|
251
|
+
const rules = loadRules(config);
|
|
44
252
|
const gate = new Gate(rules, config.approver ?? new ConsoleApprover());
|
|
45
|
-
|
|
253
|
+
// No explicit isSafeTool: fall back to each called tool's own `safe`
|
|
254
|
+
// flag (looked up by name) rather than defaulting every tool to unsafe
|
|
255
|
+
// outright — see ToolDefinition.safe's own doc comment for why this is
|
|
256
|
+
// the less powerful of the two (name-only, no per-call nuance).
|
|
257
|
+
const isSafeTool = config.isSafeTool ?? ((call) => tools.some((t) => t.name === call.name && t.safe === true));
|
|
258
|
+
const toolLane = new ToolLane({ isSafe: isSafeTool });
|
|
46
259
|
let messages = [...history, { role: 'user', content: userMessage }];
|
|
47
260
|
// Tracked in parallel with `messages`, but never rewritten wholesale by
|
|
48
261
|
// recovery the way `messages` is — this is the actual, authoritative
|
|
@@ -93,8 +306,6 @@ export async function runAgent(config, modelCall, userMessage, history = [], opt
|
|
|
93
306
|
return recovered;
|
|
94
307
|
},
|
|
95
308
|
});
|
|
96
|
-
// Optional on AgentConfig — an agent can have zero tools.
|
|
97
|
-
const tools = config.tools ?? [];
|
|
98
309
|
const toolsByName = new Map(tools.map((t) => [t.name, t]));
|
|
99
310
|
const toolSchemas = tools.map(({ name, description, input_schema }) => ({
|
|
100
311
|
name,
|
package/dist/session-store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Redis } from 'ioredis';
|
|
2
2
|
import { type Storage } from 'sessionknit';
|
|
3
|
-
import type { Message } from '
|
|
3
|
+
import type { Message } from '#run-agent.js';
|
|
4
4
|
export interface SessionResult<T> {
|
|
5
5
|
/** Exactly what this turn added — durably appended as-is, in order.
|
|
6
6
|
* This has to come from the caller explicitly (runAgent's own
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,9 +11,17 @@
|
|
|
11
11
|
},
|
|
12
12
|
"main": "dist/index.js",
|
|
13
13
|
"types": "dist/index.d.ts",
|
|
14
|
+
"bin": {
|
|
15
|
+
"loopengine": "dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"imports": {
|
|
18
|
+
"#*": "./*"
|
|
19
|
+
},
|
|
14
20
|
"files": [
|
|
15
21
|
"dist/index.js",
|
|
16
22
|
"dist/index.d.ts",
|
|
23
|
+
"dist/cli.js",
|
|
24
|
+
"dist/cli.d.ts",
|
|
17
25
|
"dist/run-agent.js",
|
|
18
26
|
"dist/run-agent.d.ts",
|
|
19
27
|
"dist/agent-config.js",
|
|
@@ -22,18 +30,20 @@
|
|
|
22
30
|
"dist/session-store.d.ts",
|
|
23
31
|
"dist/vector-index.js",
|
|
24
32
|
"dist/vector-index.d.ts",
|
|
25
|
-
"dist/anthropic-model-call.js",
|
|
26
|
-
"dist/anthropic-model-call.d.ts",
|
|
27
|
-
"dist/openai-model-call.js",
|
|
28
|
-
"dist/openai-model-call.d.ts",
|
|
29
|
-
"dist/deepseek-model-call.js",
|
|
30
|
-
"dist/deepseek-model-call.d.ts",
|
|
33
|
+
"dist/model-calls/anthropic-model-call.js",
|
|
34
|
+
"dist/model-calls/anthropic-model-call.d.ts",
|
|
35
|
+
"dist/model-calls/openai-model-call.js",
|
|
36
|
+
"dist/model-calls/openai-model-call.d.ts",
|
|
37
|
+
"dist/model-calls/deepseek-model-call.js",
|
|
38
|
+
"dist/model-calls/deepseek-model-call.d.ts",
|
|
31
39
|
"dist/discover-agents.js",
|
|
32
40
|
"dist/discover-agents.d.ts",
|
|
41
|
+
"dist/agent-as-tool.js",
|
|
42
|
+
"dist/agent-as-tool.d.ts",
|
|
33
43
|
"LICENSE"
|
|
34
44
|
],
|
|
35
45
|
"scripts": {
|
|
36
|
-
"build": "tsc",
|
|
46
|
+
"build": "tsc && chmod +x dist/cli.js",
|
|
37
47
|
"prepublishOnly": "npm run build",
|
|
38
48
|
"start": "node dist/adapters/http.js",
|
|
39
49
|
"test": "vitest run"
|
|
@@ -47,8 +57,9 @@
|
|
|
47
57
|
"openai": "^7.4.0",
|
|
48
58
|
"reflowkit": "^0.0.1",
|
|
49
59
|
"sessionknit": "^0.0.2",
|
|
50
|
-
"skillgarden": "^0.0.
|
|
51
|
-
"toollane": "^0.0.1"
|
|
60
|
+
"skillgarden": "^0.0.5",
|
|
61
|
+
"toollane": "^0.0.1",
|
|
62
|
+
"yaml": "^2.6.0"
|
|
52
63
|
},
|
|
53
64
|
"devDependencies": {
|
|
54
65
|
"@types/node": "^26.2.0",
|
|
File without changes
|
|
File without changes
|