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 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), then
68
- * splices the newly-imported tools into the live agent registry so
69
- * they're callable immediately, no restart — same updateAgent call
70
- * adapters/http.ts's handleHttpToolPost already makes. All-or-nothing:
71
- * every collision check below runs, and the whole install is refused,
72
- * before a single file is written. */
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
- const floor = semver.minVersion(installedRange);
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), then
166
- * splices the newly-imported tools into the live agent registry so
167
- * they're callable immediately, no restart — same updateAgent call
168
- * adapters/http.ts's handleHttpToolPost already makes. All-or-nothing:
169
- * every collision check below runs, and the whole install is refused,
170
- * before a single file is written. */
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, patch tools/index.ts, dynamic-import each fresh
219
- // file. currentTools is captured once, before any write — accumulate
220
- // newly-installed tools locally and splice with one final updateAgent
221
- // call, avoiding the exact double-append hazard handleHttpToolPost's
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
+ }
@@ -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({ budgetTokens: config.contextBudgetTokens ?? 100000, softThreshold: budgetTracker.softThreshold, tailMessages });
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.11",
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",