loopengine 0.0.2 → 0.0.4

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # LoopEngine
2
2
 
3
+ [![CI](https://github.com/evanyan22/loopengine/actions/workflows/ci.yml/badge.svg)](https://github.com/evanyan22/loopengine/actions/workflows/ci.yml)
4
+
3
5
  A minimal framework for defining and running tool-using AI agents. Write an
4
6
  agent as a plain config object — persona, tools, permission rules — and run
5
7
  it through one generic ReAct loop over the CLI, an HTTP API, or both. No
@@ -37,9 +39,16 @@ reference app built on top of it (demo agents, a CLI, an HTTP server). Only
37
39
  the library half ships to npm:
38
40
 
39
41
  ```bash
40
- npm install loopengine actauth contextclip reflowkit toollane skillgarden
42
+ npm install loopengine
41
43
  ```
42
44
 
45
+ `actauth`, `contextclip`, `reflowkit`, `toollane`, `skillgarden`, and the
46
+ rest are regular dependencies of `loopengine` itself — installing it alone
47
+ already pulls them into `node_modules`, fully resolvable for both imports
48
+ and TypeScript types. Install one of them directly only if you want to use
49
+ it standalone, outside `runAgent` (e.g. just `toollane`'s parallel/solo
50
+ scheduling in your own loop).
51
+
43
52
  ```ts
44
53
  import { runAgent, type AgentConfig, type ToolDefinition } from 'loopengine'
45
54
 
@@ -26,6 +26,14 @@ export interface AgentConfig {
26
26
  skillsDirs?: string[];
27
27
  skillIndexBudgetTokens?: number;
28
28
  contextBudgetTokens?: number;
29
+ /** Hard cap on model calls in one runAgent() invocation — the only thing
30
+ * that stops a model stuck re-requesting the same (or ping-ponging)
31
+ * tool calls forever, which nothing else in this loop bounds. Default
32
+ * 25. Hitting it ends the turn the same way running out of tools to
33
+ * call does — a real result, not a thrown error — with
34
+ * RunAgentResult.stopReason set to 'max_turns' so a caller can tell the
35
+ * difference from a normal finish. */
36
+ maxTurns?: number;
29
37
  /** Which tools ToolLane may run in a parallel lane. Default: none (every tool runs solo). */
30
38
  isSafeTool?: SafetyClassifier;
31
39
  /** How adapters/http.ts derives a session key from a request body — this
@@ -53,5 +53,10 @@ export interface RunAgentResult {
53
53
  * only works if `history` only ever grows, which stops being true the
54
54
  * moment recovery can shrink/rewrite it, not just extend it. */
55
55
  newMessages: Message[];
56
+ /** Set to 'max_turns' if the loop stopped because it hit
57
+ * config.maxTurns, not because the model produced a final answer with
58
+ * no more tool_use blocks — `text` in that case is a synthetic notice,
59
+ * not something the model said. Absent on a normal finish. */
60
+ stopReason?: 'max_turns';
56
61
  }
57
62
  export declare function runAgent(config: AgentConfig, modelCall: ModelCall, userMessage: string, history?: Message[], options?: RunAgentOptions): Promise<RunAgentResult>;
package/dist/run-agent.js CHANGED
@@ -128,7 +128,19 @@ export async function runAgent(config, modelCall, userMessage, history = [], opt
128
128
  const systemPrompt = skillIndex.length
129
129
  ? `${config.systemPrompt}\n\nAvailable skills:\n${skillIndex.map((s) => `- ${s.name}: ${s.description}`).join('\n')}`
130
130
  : config.systemPrompt;
131
+ // Nothing else in this loop bounds a model stuck re-requesting the same
132
+ // (or ping-ponging) tool calls forever — checked before spending a model
133
+ // call on the (maxTurns + 1)-th turn, not after.
134
+ const maxTurns = config.maxTurns ?? 25;
135
+ let turn = 0;
131
136
  for (;;) {
137
+ turn++;
138
+ if (turn > maxTurns) {
139
+ const text = `Stopped after ${maxTurns} turns without a final answer — the agent may be stuck in a loop.`;
140
+ pushMessage({ role: 'assistant', content: text });
141
+ log('loop:max_turns', { maxTurns });
142
+ return { text, history: messages, newMessages, stopReason: 'max_turns' };
143
+ }
132
144
  const budget = contextClip.check(messages.map(flattenForContextClip));
133
145
  log('contextclip:check', budget);
134
146
  if (budget.action === 'nudge' && budget.nudge)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "description": "A minimal framework for defining and running tool-using AI agents over CLI, HTTP, or both.",
6
6
  "license": "MIT",