@andromarces/agent-loops 0.2.0

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.
@@ -0,0 +1,185 @@
1
+ import { defaultAgents, runAgent } from "./agents/index.mjs";
2
+ import { logError, logInfo, logWarn } from "./lib/log.mjs";
3
+ import { withMutationCheck } from "./lib/snapshot.mjs";
4
+ import { decide } from "./orchestrator.mjs";
5
+ import { initialPrompt, resultPrompt } from "./prompts/orchestrator.mjs";
6
+ import { reviewerPrompt } from "./prompts/reviewer.mjs";
7
+ import { workerPrompt } from "./prompts/worker.mjs";
8
+
9
+ /**
10
+ * Every CLI call goes through here. Emits one `invocation` event per call, carrying the
11
+ * usage the adapter exposed on `state.usage`, and clears that field so it never lingers.
12
+ */
13
+ async function invoke(agents, state, roleName, prompt, opts, onEvent, stepsUsed) {
14
+ delete state.usage;
15
+ const emit = (status) => {
16
+ const event = { type: "invocation", role: roleName, status, stepsUsed };
17
+ if (state.usage) {
18
+ event.usage = state.usage;
19
+ delete state.usage;
20
+ }
21
+ onEvent(event);
22
+ };
23
+ let response;
24
+ try {
25
+ response = await runAgent(state, prompt, { ...opts, role: roleName }, agents);
26
+ } catch (err) {
27
+ emit("error");
28
+ throw err;
29
+ }
30
+ emit("ok");
31
+ return response;
32
+ }
33
+
34
+ /**
35
+ * Run one worker or reviewer turn with the same guards as the headless loop:
36
+ * role prompt wrapping, read-only mutation check for the reviewer, timeout,
37
+ * and cancel propagation. Returns `{ role, status, response }` on success and
38
+ * `{ role, status: "error", error }` on a handled failure. Throws on fatal
39
+ * errors: detected mutation, snapshot failure, or cancel.
40
+ * @param {object} options
41
+ * @returns {Promise<{ role: string, status: "ok", response: string } | { role: string, status: "error", error: string }>}
42
+ */
43
+ export async function runChild(options) {
44
+ const {
45
+ agents = defaultAgents,
46
+ role,
47
+ roleName,
48
+ prompt,
49
+ cwd,
50
+ timeout,
51
+ signal,
52
+ stepsUsed = 0,
53
+ onEvent = () => {},
54
+ } = options;
55
+
56
+ const isWorker = roleName === "worker";
57
+ const readOnly = !isWorker;
58
+ const finalPrompt = isWorker
59
+ ? workerPrompt(prompt, role.sessionId === null)
60
+ : reviewerPrompt(prompt);
61
+
62
+ const runFn = async () => {
63
+ return invoke(
64
+ agents,
65
+ role,
66
+ roleName,
67
+ finalPrompt,
68
+ { cwd, readOnly, timeout, signal },
69
+ onEvent,
70
+ stepsUsed,
71
+ );
72
+ };
73
+
74
+ try {
75
+ const response = readOnly ? await withMutationCheck(cwd, roleName, runFn) : await runFn();
76
+ return { role: roleName, status: "ok", response };
77
+ } catch (err) {
78
+ if (err?.name === "MutationError" || err?.name === "SnapshotError" || err?.isCanceled) {
79
+ if (err?.isCanceled) {
80
+ logError(`${roleName} canceled by signal`);
81
+ }
82
+ throw err;
83
+ }
84
+ let errorMessage = err?.message ?? String(err);
85
+ if (err?.timedOut) {
86
+ errorMessage = `${roleName} timed out after ${timeout} seconds`;
87
+ }
88
+ logWarn(err?.timedOut ? errorMessage : `${roleName}: ${errorMessage.split("\n")[0]}`);
89
+ return { role: roleName, status: "error", error: errorMessage };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Run the orchestrator loop. Returns `{ exitCode: 0, summary }` when the
95
+ * orchestrator returns `finish`, or `{ exitCode: 1 | 2, reason }` on `abort`
96
+ * or a step limit reached with work remaining. Throws on fatal controller
97
+ * errors: orchestrator failure, detected mutation, or cancel.
98
+ * @param {object} options
99
+ * @returns {Promise<{ exitCode: 0, summary: object } | { exitCode: 1 | 2, reason: string }>}
100
+ */
101
+ export async function runLoop(options) {
102
+ const {
103
+ task,
104
+ cwd,
105
+ maxSteps = 20,
106
+ timeout,
107
+ signal,
108
+ roles,
109
+ agents = defaultAgents,
110
+ onEvent = () => {},
111
+ } = options;
112
+
113
+ const { orchestrator, worker, reviewer } = roles;
114
+
115
+ logInfo(`agent loop started (cwd: ${cwd}, maxSteps: ${maxSteps})`);
116
+
117
+ function stopLoop(exitCode, detail) {
118
+ logInfo(`agent loop stopped (exit ${exitCode})`);
119
+ return { exitCode, ...detail };
120
+ }
121
+
122
+ const orchAdapter = {
123
+ async run(state, p, opts) {
124
+ return withMutationCheck(cwd, "orchestrator", () =>
125
+ invoke(agents, state, "orchestrator", p, opts, onEvent, stepsUsed),
126
+ );
127
+ },
128
+ };
129
+
130
+ let stepsUsed = 0;
131
+ let prompt = initialPrompt({ task, maxSteps });
132
+
133
+ while (true) {
134
+ let action;
135
+ try {
136
+ action = await decide({
137
+ agent: orchAdapter,
138
+ state: orchestrator,
139
+ prompt,
140
+ options: { cwd, timeout, signal },
141
+ });
142
+ } catch (err) {
143
+ if (err?.name === "MutationError") {
144
+ // Already logged at the detection site in withMutationCheck.
145
+ throw err;
146
+ }
147
+ logError(`orchestrator turn failed: ${String(err?.message ?? err).split("\n")[0]}`);
148
+ throw err;
149
+ }
150
+
151
+ onEvent({ type: "action", action, stepsUsed });
152
+
153
+ if (action.action === "finish") {
154
+ return stopLoop(0, { summary: action.summary });
155
+ }
156
+
157
+ if (action.action === "abort") {
158
+ return stopLoop(1, { reason: action.reason });
159
+ }
160
+
161
+ if (stepsUsed >= maxSteps) {
162
+ return stopLoop(2, { reason: "Step limit reached with work remaining." });
163
+ }
164
+
165
+ stepsUsed += 1;
166
+ const isWorkerDispatch = action.action === "run_worker";
167
+ const targetRole = isWorkerDispatch ? worker : reviewer;
168
+ const roleName = isWorkerDispatch ? "worker" : "reviewer";
169
+
170
+ const result = await runChild({
171
+ agents,
172
+ role: targetRole,
173
+ roleName,
174
+ prompt: action.prompt,
175
+ cwd,
176
+ timeout,
177
+ signal,
178
+ stepsUsed,
179
+ onEvent,
180
+ });
181
+ onEvent({ type: "result", role: roleName, result, stepsUsed });
182
+
183
+ prompt = resultPrompt({ result, stepsUsed, maxSteps });
184
+ }
185
+ }