loopengine 0.1.11 → 0.1.13
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/dist/bin/cli.js +3 -0
- package/dist/bin/package-manager.d.ts +24 -6
- package/dist/bin/package-manager.js +42 -25
- package/dist/core/agent-config.d.ts +16 -0
- package/dist/core/llm-summarizer.d.ts +24 -0
- package/dist/core/llm-summarizer.js +42 -0
- package/dist/core/run-agent.js +6 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/package.json +3 -1
package/dist/bin/cli.js
CHANGED
|
@@ -320,6 +320,9 @@ async function main() {
|
|
|
320
320
|
console.log(`Installed into agents/${agent}/:`);
|
|
321
321
|
for (const p of installed)
|
|
322
322
|
console.log(` ${p}`);
|
|
323
|
+
console.log();
|
|
324
|
+
console.log('Already running under `npx loopengine dev`? The new tool becomes active automatically.');
|
|
325
|
+
console.log('Running under `serve` (or nothing yet)? Restart the server to pick it up.');
|
|
323
326
|
}
|
|
324
327
|
catch (err) {
|
|
325
328
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -64,12 +64,30 @@ export interface FetchOptions {
|
|
|
64
64
|
* createHttpTool does), copies each skill directory verbatim (a raw
|
|
65
65
|
* cpSync, not skills-admin.ts's writeSkill — that regenerates
|
|
66
66
|
* frontmatter from scratch and can't carry a package's own
|
|
67
|
-
* frontmatter/assets), appends each actauth rule (addActauthRule)
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* adapters/http.ts's handleHttpToolPost
|
|
71
|
-
*
|
|
72
|
-
*
|
|
67
|
+
* frontmatter/assets), and appends each actauth rule (addActauthRule).
|
|
68
|
+
*
|
|
69
|
+
* Deliberately does *not* attempt a live in-memory registry splice the
|
|
70
|
+
* way adapters/http.ts's own handleHttpToolPost does for an
|
|
71
|
+
* admin-created tool — that works there because the HTTP request
|
|
72
|
+
* creating the tool runs *inside the same process* as the already-running
|
|
73
|
+
* server, so updateAgent's mutation is visible to every later request in
|
|
74
|
+
* that same process immediately. `add-package` is a separate, one-off
|
|
75
|
+
* CLI process with no connection to whatever server might be running
|
|
76
|
+
* elsewhere — even a successful getEntry/updateAgent call here would
|
|
77
|
+
* only mutate *this* CLI invocation's own throwaway registry, then
|
|
78
|
+
* vanish the moment the process exits, having done nothing to the real
|
|
79
|
+
* running server. (Confirmed live: this used to import
|
|
80
|
+
* core/agent-registry.js, whose own discoverAgents does a top-level
|
|
81
|
+
* directory scan relative to *its own compiled file's location* —
|
|
82
|
+
* inside node_modules/loopengine when this runs there, not the
|
|
83
|
+
* consuming project's real agents/ at all, throwing ENOENT immediately.)
|
|
84
|
+
* Same reason add-agent/add-subagent above don't attempt this either —
|
|
85
|
+
* a new tool becomes active the same way a new agent does: the next
|
|
86
|
+
* request under `serve`, or automatically under `npx loopengine dev`'s
|
|
87
|
+
* file watcher once tools/index.ts's own edit is picked up.
|
|
88
|
+
*
|
|
89
|
+
* All-or-nothing: every collision check below runs, and the whole
|
|
90
|
+
* install is refused, before a single file is written. */
|
|
73
91
|
export declare function installPackage(agentName: string, spec: string, options?: FetchOptions): Promise<{
|
|
74
92
|
installed: string[];
|
|
75
93
|
}>;
|
|
@@ -21,12 +21,9 @@ import { execFileSync } from 'node:child_process';
|
|
|
21
21
|
import { basename, join } from 'node:path';
|
|
22
22
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { tmpdir } from 'node:os';
|
|
24
|
-
import { pathToFileURL } from 'node:url';
|
|
25
24
|
import { parse as parseYaml } from 'yaml';
|
|
26
25
|
import semver from 'semver';
|
|
27
26
|
import { agentDir } from '../core/gateway-tools.js';
|
|
28
|
-
import { getEntry, updateAgent } from '../core/agent-registry.js';
|
|
29
|
-
import { loadDefaultTools } from '../core/run-agent.js';
|
|
30
27
|
import { addToolToIndex, toCamelCase } from '../web/http-tool-admin.js';
|
|
31
28
|
import { addActauthRule, updateActauthRule, removeActauthRule, readActauthConfig } from '../web/actauth-admin.js';
|
|
32
29
|
export class PackageManifestError extends Error {
|
|
@@ -137,7 +134,20 @@ function checkLoopengineVersion(manifest, installedRangeOverride) {
|
|
|
137
134
|
throw new PackageVersionError(`This project's package.json has no "loopengine" dependency — can't check compatibility.`);
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
|
-
|
|
137
|
+
// A non-registry dependency specifier (file:, git:, workspace:, ...) —
|
|
138
|
+
// common for local development against an unpublished loopengine
|
|
139
|
+
// build — isn't a semver range at all, so minVersion throws rather
|
|
140
|
+
// than returning null; nothing meaningful to compare against, so skip
|
|
141
|
+
// the check rather than crash the whole command on it (same "nothing
|
|
142
|
+
// to compare, nothing to refuse" reasoning the this-repo-itself skip
|
|
143
|
+
// above already uses).
|
|
144
|
+
let floor;
|
|
145
|
+
try {
|
|
146
|
+
floor = semver.minVersion(installedRange);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
141
151
|
if (!floor || !semver.satisfies(floor, manifest.loopengineVersion)) {
|
|
142
152
|
throw new PackageVersionError(`Package '${manifest.name}' needs loopengine ${manifest.loopengineVersion}, but this project depends on loopengine ${installedRange} — bump the dependency first.`);
|
|
143
153
|
}
|
|
@@ -162,12 +172,30 @@ function parseActauthRules(packageDir, manifest) {
|
|
|
162
172
|
* createHttpTool does), copies each skill directory verbatim (a raw
|
|
163
173
|
* cpSync, not skills-admin.ts's writeSkill — that regenerates
|
|
164
174
|
* frontmatter from scratch and can't carry a package's own
|
|
165
|
-
* frontmatter/assets), appends each actauth rule (addActauthRule)
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* adapters/http.ts's handleHttpToolPost
|
|
169
|
-
*
|
|
170
|
-
*
|
|
175
|
+
* frontmatter/assets), and appends each actauth rule (addActauthRule).
|
|
176
|
+
*
|
|
177
|
+
* Deliberately does *not* attempt a live in-memory registry splice the
|
|
178
|
+
* way adapters/http.ts's own handleHttpToolPost does for an
|
|
179
|
+
* admin-created tool — that works there because the HTTP request
|
|
180
|
+
* creating the tool runs *inside the same process* as the already-running
|
|
181
|
+
* server, so updateAgent's mutation is visible to every later request in
|
|
182
|
+
* that same process immediately. `add-package` is a separate, one-off
|
|
183
|
+
* CLI process with no connection to whatever server might be running
|
|
184
|
+
* elsewhere — even a successful getEntry/updateAgent call here would
|
|
185
|
+
* only mutate *this* CLI invocation's own throwaway registry, then
|
|
186
|
+
* vanish the moment the process exits, having done nothing to the real
|
|
187
|
+
* running server. (Confirmed live: this used to import
|
|
188
|
+
* core/agent-registry.js, whose own discoverAgents does a top-level
|
|
189
|
+
* directory scan relative to *its own compiled file's location* —
|
|
190
|
+
* inside node_modules/loopengine when this runs there, not the
|
|
191
|
+
* consuming project's real agents/ at all, throwing ENOENT immediately.)
|
|
192
|
+
* Same reason add-agent/add-subagent above don't attempt this either —
|
|
193
|
+
* a new tool becomes active the same way a new agent does: the next
|
|
194
|
+
* request under `serve`, or automatically under `npx loopengine dev`'s
|
|
195
|
+
* file watcher once tools/index.ts's own edit is picked up.
|
|
196
|
+
*
|
|
197
|
+
* All-or-nothing: every collision check below runs, and the whole
|
|
198
|
+
* install is refused, before a single file is written. */
|
|
171
199
|
export async function installPackage(agentName, spec, options = {}) {
|
|
172
200
|
const fetch = options.fetchPackageDir ?? fetchPackageDir;
|
|
173
201
|
const packageDir = fetch(spec);
|
|
@@ -215,19 +243,11 @@ export async function installPackage(agentName, spec, options = {}) {
|
|
|
215
243
|
}
|
|
216
244
|
}
|
|
217
245
|
const contentHashes = {};
|
|
218
|
-
// Write tool files
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// own comment already warns about for a folder-form agent with no
|
|
223
|
-
// cached config.tools (re-reading tools/index.ts after it's already
|
|
224
|
-
// been patched would see the just-added entry a second time).
|
|
246
|
+
// Write tool files and patch tools/index.ts — pure filesystem
|
|
247
|
+
// operations, no dynamic import and no registry interaction (see this
|
|
248
|
+
// function's own doc comment for why: a CLI process can't live-splice
|
|
249
|
+
// into a separate, already-running server).
|
|
225
250
|
if (toolNames.length > 0) {
|
|
226
|
-
const entry = getEntry(agentName);
|
|
227
|
-
if (!entry)
|
|
228
|
-
throw new Error(`Unknown agent '${agentName}'.`);
|
|
229
|
-
const currentTools = entry.config.tools ?? (await loadDefaultTools(entry.config));
|
|
230
|
-
const newTools = [];
|
|
231
251
|
mkdirSync(toolsDir, { recursive: true });
|
|
232
252
|
const indexPath = join(toolsDir, 'index.ts');
|
|
233
253
|
for (const toolFile of toolFiles) {
|
|
@@ -243,10 +263,7 @@ export async function installPackage(agentName, spec, options = {}) {
|
|
|
243
263
|
else {
|
|
244
264
|
writeFileSync(indexPath, `import type { ToolDefinition } from 'loopengine'\nimport { ${exportName} } from './${toolName}.js'\n\nexport const tools: ToolDefinition[] = [${exportName}]\n`);
|
|
245
265
|
}
|
|
246
|
-
const mod = (await import(pathToFileURL(destPath).href));
|
|
247
|
-
newTools.push(mod[exportName]);
|
|
248
266
|
}
|
|
249
|
-
updateAgent(agentName, { config: { tools: [...currentTools, ...newTools] } });
|
|
250
267
|
}
|
|
251
268
|
// Copy skill directories verbatim.
|
|
252
269
|
for (const skillDir of skillDirs) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Rule, Decision, Scope } from 'actauth';
|
|
2
2
|
import type { Redis } from 'ioredis';
|
|
3
3
|
import type { SafetyClassifier } from 'toollane';
|
|
4
|
+
import type { Summarizer } from './compaction.js';
|
|
4
5
|
/** Which channel a runAgent() call is on — the value RunAgentOptions.channel
|
|
5
6
|
* carries, checked against directly by AgentConfig.httpNotifier's own
|
|
6
7
|
* resolution (see its own doc comment for why it only ever matches
|
|
@@ -443,6 +444,21 @@ export interface AgentConfig {
|
|
|
443
444
|
skillsDirs?: string[];
|
|
444
445
|
skillIndexBudgetTokens?: number;
|
|
445
446
|
contextBudgetTokens?: number;
|
|
447
|
+
/** How compaction.ts's Compactor compresses old history once a turn
|
|
448
|
+
* goes over contextBudgetTokens. Omitted entirely defaults to
|
|
449
|
+
* TruncatingSummarizer — a real, dependency-free fallback that just
|
|
450
|
+
* concatenates and cuts text, with no model call and no added latency
|
|
451
|
+
* or cost. Pass `new LLMSummarizer({ modelCall })` (llm-summarizer.js)
|
|
452
|
+
* to compact with a real model call instead — genuinely worth it for
|
|
453
|
+
* an agent whose sessions run long enough to hit compaction at all,
|
|
454
|
+
* since a fact stated once early on (an order number, an id) tends to
|
|
455
|
+
* survive a structured LLM summary but not a hard character cutoff.
|
|
456
|
+
* Not defaulted to LLMSummarizer automatically: that would add a
|
|
457
|
+
* hidden extra model call — cost, latency, and a new failure mode —
|
|
458
|
+
* to every agent, including ones that never expected compaction to do
|
|
459
|
+
* anything but this repo's own tests, which assert exact modelCall
|
|
460
|
+
* call counts. */
|
|
461
|
+
summarizer?: Summarizer;
|
|
446
462
|
/** Hard cap on model calls in one runAgent() invocation — the only thing
|
|
447
463
|
* that stops a model stuck re-requesting the same (or ping-ponging)
|
|
448
464
|
* tool calls forever, which nothing else in this loop bounds. Default
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Message } from './budget.js';
|
|
2
|
+
import type { ModelCall } from './run-agent.js';
|
|
3
|
+
import { type Summarizer } from './compaction.js';
|
|
4
|
+
export interface LLMSummarizerOptions {
|
|
5
|
+
modelCall: ModelCall;
|
|
6
|
+
/** Replaces the default instructions entirely — not appended to them. */
|
|
7
|
+
systemPrompt?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Calls modelCall with no tools and asks it to compact `messages` per
|
|
10
|
+
* DEFAULT_SYSTEM_PROMPT's own structure (see that constant's own doc
|
|
11
|
+
* comment for why "facts established" is its own named section rather
|
|
12
|
+
* than left to a generic "summarize this" prompt — a fact mentioned once
|
|
13
|
+
* early in a long conversation is exactly what a free-form summary tends
|
|
14
|
+
* to paraphrase away). Falls back to TruncatingSummarizer — rather than
|
|
15
|
+
* throwing, which would surface as a whole turn failing — if the call
|
|
16
|
+
* itself throws (rate limit, network) or comes back with no usable text
|
|
17
|
+
* (a model that only ever returns tool_use has nothing this can use). */
|
|
18
|
+
export declare class LLMSummarizer implements Summarizer {
|
|
19
|
+
private readonly modelCall;
|
|
20
|
+
private readonly systemPrompt;
|
|
21
|
+
private readonly fallback;
|
|
22
|
+
constructor(options: LLMSummarizerOptions);
|
|
23
|
+
summarize(messages: Message[]): Promise<Message>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TruncatingSummarizer } from './compaction.js';
|
|
2
|
+
const DEFAULT_SYSTEM_PROMPT = `You are compacting an in-progress agent conversation to free up context window. You will be given a segment of older messages (the most recent messages are kept verbatim elsewhere and are not shown to you). Write a dense summary that lets the agent continue this task without having seen the original messages.
|
|
3
|
+
|
|
4
|
+
Structure your summary with these sections, using only the ones that have content:
|
|
5
|
+
- Facts established: concrete values stated even once — ids, numbers, names, dates, amounts, decisions — verbatim, not paraphrased. A fact that only appears here and gets paraphrased or dropped is lost for good.
|
|
6
|
+
- Actions taken: tool calls made and their outcomes, briefly.
|
|
7
|
+
- Open question / next step: what's still unresolved, if anything.
|
|
8
|
+
|
|
9
|
+
Output only the summary itself — no preamble, no "here is a summary".`;
|
|
10
|
+
/** Calls modelCall with no tools and asks it to compact `messages` per
|
|
11
|
+
* DEFAULT_SYSTEM_PROMPT's own structure (see that constant's own doc
|
|
12
|
+
* comment for why "facts established" is its own named section rather
|
|
13
|
+
* than left to a generic "summarize this" prompt — a fact mentioned once
|
|
14
|
+
* early in a long conversation is exactly what a free-form summary tends
|
|
15
|
+
* to paraphrase away). Falls back to TruncatingSummarizer — rather than
|
|
16
|
+
* throwing, which would surface as a whole turn failing — if the call
|
|
17
|
+
* itself throws (rate limit, network) or comes back with no usable text
|
|
18
|
+
* (a model that only ever returns tool_use has nothing this can use). */
|
|
19
|
+
export class LLMSummarizer {
|
|
20
|
+
modelCall;
|
|
21
|
+
systemPrompt;
|
|
22
|
+
fallback = new TruncatingSummarizer();
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.modelCall = options.modelCall;
|
|
25
|
+
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
26
|
+
}
|
|
27
|
+
async summarize(messages) {
|
|
28
|
+
const transcript = messages.map((m) => `[${m.role}] ${m.content}`).join('\n');
|
|
29
|
+
let text;
|
|
30
|
+
try {
|
|
31
|
+
const response = await this.modelCall([{ role: 'user', content: transcript }], this.systemPrompt, []);
|
|
32
|
+
text = response.content.find((block) => block.type === 'text')?.text?.trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// modelCall threw — fall through to the truncating fallback below,
|
|
36
|
+
// same as text staying undefined.
|
|
37
|
+
}
|
|
38
|
+
if (!text)
|
|
39
|
+
return this.fallback.summarize(messages);
|
|
40
|
+
return { role: 'system', content: `[compacted ${messages.length} earlier message(s)]\n${text}` };
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/core/run-agent.js
CHANGED
|
@@ -336,7 +336,12 @@ async function buildTurnContext(config, modelCall, options) {
|
|
|
336
336
|
// 70000-token nudge point) and hardThreshold's 0.92 still leave real
|
|
337
337
|
// margin before hitting it.
|
|
338
338
|
const budgetTracker = new BudgetTracker({ budgetTokens: config.contextBudgetTokens ?? 100000 });
|
|
339
|
-
const compactor = new Compactor({
|
|
339
|
+
const compactor = new Compactor({
|
|
340
|
+
budgetTokens: config.contextBudgetTokens ?? 100000,
|
|
341
|
+
softThreshold: budgetTracker.softThreshold,
|
|
342
|
+
tailMessages,
|
|
343
|
+
summarizer: config.summarizer,
|
|
344
|
+
});
|
|
340
345
|
const rules = loadRules(config);
|
|
341
346
|
// AgentConfig.httpNotifier only ever stands in for the http channel —
|
|
342
347
|
// see its own doc comment for why cli/http_stream never consult it at
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export { discoverAgents, loadAgentModule, synthesizeCreateModelCall } from './co
|
|
|
24
24
|
export type { AgentModule } from './core/discover-agents.js';
|
|
25
25
|
export { agentAsTool } from './core/agent-as-tool.js';
|
|
26
26
|
export { resumeAgent, loadRules, loadDefaultTools, loadSubagentAsTools, systemTools, systemSkillsDir } from './core/run-agent.js';
|
|
27
|
+
export { Compactor, TruncatingSummarizer } from './core/compaction.js';
|
|
28
|
+
export type { Summarizer, RecoverResult, RecoverAction } from './core/compaction.js';
|
|
29
|
+
export { LLMSummarizer } from './core/llm-summarizer.js';
|
|
30
|
+
export type { LLMSummarizerOptions } from './core/llm-summarizer.js';
|
|
27
31
|
export { createCheckpointStore } from './core/durable-approvals.js';
|
|
28
32
|
export type { TurnCheckpoint, CheckpointStore, OutstandingItem } from './core/durable-approvals.js';
|
|
29
33
|
export { addGatewayTool, agentDir, disconnectComposioAccount, describeGatewayTools, listComposioConnections, listComposioTools, loadGatewayToolsFromDir, removeGatewayTool, removeGatewayToolSlug, GatewayToolExistsError, GatewayToolNotFoundError, } from './core/gateway-tools.js';
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,8 @@ export { agentAsTool } from './core/agent-as-tool.js';
|
|
|
29
29
|
// copy of adapters/http.ts (which, like agent-registry.ts, is meant to be
|
|
30
30
|
// owned/edited, not imported — see this file's own header comment).
|
|
31
31
|
export { resumeAgent, loadRules, loadDefaultTools, loadSubagentAsTools, systemTools, systemSkillsDir } from './core/run-agent.js';
|
|
32
|
+
export { Compactor, TruncatingSummarizer } from './core/compaction.js';
|
|
33
|
+
export { LLMSummarizer } from './core/llm-summarizer.js';
|
|
32
34
|
export { createCheckpointStore } from './core/durable-approvals.js';
|
|
33
35
|
export { addGatewayTool, agentDir, disconnectComposioAccount, describeGatewayTools, listComposioConnections, listComposioTools, loadGatewayToolsFromDir, removeGatewayTool, removeGatewayToolSlug, GatewayToolExistsError, GatewayToolNotFoundError, } from './core/gateway-tools.js';
|
|
34
36
|
export { WebhookNotifier } from './core/http-notify-triggers/webhook.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
"dist/core/budget.d.ts",
|
|
60
60
|
"dist/core/compaction.js",
|
|
61
61
|
"dist/core/compaction.d.ts",
|
|
62
|
+
"dist/core/llm-summarizer.js",
|
|
63
|
+
"dist/core/llm-summarizer.d.ts",
|
|
62
64
|
"dist/core/recovery.js",
|
|
63
65
|
"dist/core/recovery.d.ts",
|
|
64
66
|
"dist/core/http-notifier.js",
|