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 +10 -1
- package/dist/agent-config.d.ts +8 -0
- package/dist/run-agent.d.ts +5 -0
- package/dist/run-agent.js +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# LoopEngine
|
|
2
2
|
|
|
3
|
+
[](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
|
|
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
|
|
package/dist/agent-config.d.ts
CHANGED
|
@@ -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
|
package/dist/run-agent.d.ts
CHANGED
|
@@ -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)
|