pi-harness-runtime 0.2.0 → 0.3.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.
- package/README.md +117 -158
- package/harness/agent-handoff.ts +189 -0
- package/harness/blackboard.ts +291 -0
- package/harness/context-window-manager.ts +210 -0
- package/harness/e2e/playwright-runner.ts +252 -0
- package/harness/e2e/test-engine.ts +402 -0
- package/harness/job-state-machine.ts +363 -0
- package/harness/loop-runtime.ts +337 -0
- package/harness/master-planner.ts +327 -0
- package/harness/project-detector/detector.ts +328 -0
- package/harness/repair-engine.ts +340 -0
- package/harness/task-graph.ts +336 -0
- package/index.ts +294 -2
- package/package.json +5 -2
- package/packages/checkpoint/README.md +3 -0
- package/packages/checkpoint/src/checkpoint-manager.ts +38 -0
- package/packages/provider-router/src/provider-router.ts +42 -0
- package/packages/providers/README.md +3 -0
- package/packages/providers/adapters.ts +261 -0
- package/packages/quota-manager/README.md +3 -0
- package/packages/quota-manager/quota-manager.ts +328 -0
- package/packages/runtime/README.md +3 -0
- package/packages/scheduler/README.md +3 -0
- package/packages/scheduler/src/scheduler.ts +50 -0
- package/packages/shared-context/README.md +3 -0
- package/packages/shared-context/src/shared-context.ts +42 -0
- package/packages/tui/README.md +3 -0
- package/packages/types/src/runtime-types.ts +439 -0
- package/packages/worktree/README.md +3 -0
- package/packages/worktree/worktree.ts +293 -0
- package/skills/harness-runtime/SKILL.md +186 -72
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job State Machine — RFC-0015
|
|
3
|
+
*
|
|
4
|
+
* Manages job lifecycle states with:
|
|
5
|
+
* - Defined state transitions
|
|
6
|
+
* - Event emission on every transition
|
|
7
|
+
* - Automatic checkpointing
|
|
8
|
+
* - Transition guards (validity checks)
|
|
9
|
+
*
|
|
10
|
+
* States:
|
|
11
|
+
* created -> planning -> queued -> running -> testing -> e2e_testing ->
|
|
12
|
+
* reviewing -> repairing -> paused_quota -> waiting_human -> ready_for_client -> archived
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
JobStatus,
|
|
17
|
+
RuntimeEvent,
|
|
18
|
+
RuntimeCheckpoint,
|
|
19
|
+
} from "../packages/types/src/runtime-types.ts";
|
|
20
|
+
|
|
21
|
+
export interface StateTransition {
|
|
22
|
+
from: JobStatus;
|
|
23
|
+
to: JobStatus;
|
|
24
|
+
event: string;
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TransitionResult {
|
|
29
|
+
success: boolean;
|
|
30
|
+
checkpoint?: RuntimeCheckpoint;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Valid transitions map
|
|
35
|
+
const VALID_TRANSITIONS: Record<JobStatus, JobStatus[]> = {
|
|
36
|
+
created: ["planning"],
|
|
37
|
+
planning: ["queued", "cancelled"],
|
|
38
|
+
queued: ["running", "cancelled", "waiting_human"],
|
|
39
|
+
running: [
|
|
40
|
+
"testing",
|
|
41
|
+
"reviewing",
|
|
42
|
+
"repairing",
|
|
43
|
+
"paused_quota",
|
|
44
|
+
"blocked",
|
|
45
|
+
"waiting_human",
|
|
46
|
+
"cancelled",
|
|
47
|
+
],
|
|
48
|
+
testing: [
|
|
49
|
+
"reviewing",
|
|
50
|
+
"running",
|
|
51
|
+
"repairing",
|
|
52
|
+
"paused_quota",
|
|
53
|
+
"waiting_human",
|
|
54
|
+
"cancelled",
|
|
55
|
+
],
|
|
56
|
+
e2e_testing: [
|
|
57
|
+
"reviewing",
|
|
58
|
+
"repairing",
|
|
59
|
+
"paused_quota",
|
|
60
|
+
"waiting_human",
|
|
61
|
+
"cancelled",
|
|
62
|
+
],
|
|
63
|
+
reviewing: [
|
|
64
|
+
"repairing",
|
|
65
|
+
"running",
|
|
66
|
+
"ready_for_client",
|
|
67
|
+
"paused_quota",
|
|
68
|
+
"waiting_human",
|
|
69
|
+
"cancelled",
|
|
70
|
+
],
|
|
71
|
+
repairing: [
|
|
72
|
+
"running",
|
|
73
|
+
"testing",
|
|
74
|
+
"reviewing",
|
|
75
|
+
"paused_quota",
|
|
76
|
+
"waiting_human",
|
|
77
|
+
"cancelled",
|
|
78
|
+
],
|
|
79
|
+
paused_quota: ["running", "waiting_human", "cancelled"],
|
|
80
|
+
waiting_human: ["running", "planning", "cancelled"],
|
|
81
|
+
blocked: ["running", "waiting_human", "cancelled"],
|
|
82
|
+
ready_for_client: ["archived", "repairing"],
|
|
83
|
+
cancelled: [],
|
|
84
|
+
archived: [],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export interface JobStateMachineOptions {
|
|
88
|
+
checkpointManager: CheckpointManager;
|
|
89
|
+
eventEmitter?: (event: RuntimeEvent) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface CheckpointManager {
|
|
93
|
+
save(checkpoint: RuntimeCheckpoint): Promise<void>;
|
|
94
|
+
load(jobId: string): Promise<RuntimeCheckpoint | null>;
|
|
95
|
+
appendEvent(jobId: string, event: RuntimeEvent): Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class JobStateMachine {
|
|
99
|
+
private currentCheckpoint: RuntimeCheckpoint | null = null;
|
|
100
|
+
private readonly eventLog: RuntimeEvent[] = [];
|
|
101
|
+
|
|
102
|
+
constructor(private readonly options: JobStateMachineOptions) {}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Initialize a new job with a requirement
|
|
106
|
+
*/
|
|
107
|
+
async createJob(
|
|
108
|
+
jobId: string,
|
|
109
|
+
requirement: string,
|
|
110
|
+
): Promise<TransitionResult> {
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
const checkpoint: RuntimeCheckpoint = {
|
|
113
|
+
version: 1,
|
|
114
|
+
jobId,
|
|
115
|
+
status: "created",
|
|
116
|
+
requirement,
|
|
117
|
+
createdAt: now,
|
|
118
|
+
updatedAt: now,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const event = this.createEvent(
|
|
122
|
+
jobId,
|
|
123
|
+
"JobCreated",
|
|
124
|
+
`Job ${jobId} created with requirement`,
|
|
125
|
+
{ requirement },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await this.options.checkpointManager.save(checkpoint);
|
|
130
|
+
await this.options.checkpointManager.appendEvent(jobId, event);
|
|
131
|
+
this.currentCheckpoint = checkpoint;
|
|
132
|
+
this.eventLog.push(event);
|
|
133
|
+
return { success: true, checkpoint };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return { success: false, error: String(error) };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Resume a job from checkpoint
|
|
141
|
+
*/
|
|
142
|
+
async resumeJob(jobId: string): Promise<TransitionResult> {
|
|
143
|
+
const checkpoint = await this.options.checkpointManager.load(jobId);
|
|
144
|
+
if (!checkpoint) {
|
|
145
|
+
return { success: false, error: `No checkpoint found for job ${jobId}` };
|
|
146
|
+
}
|
|
147
|
+
this.currentCheckpoint = checkpoint;
|
|
148
|
+
return { success: true, checkpoint };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Transition to a new state
|
|
153
|
+
*/
|
|
154
|
+
async transition(
|
|
155
|
+
to: JobStatus,
|
|
156
|
+
data?: Partial<RuntimeCheckpoint>,
|
|
157
|
+
): Promise<TransitionResult> {
|
|
158
|
+
if (!this.currentCheckpoint) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error: "No active job. Call createJob() or resumeJob() first.",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const from = this.currentCheckpoint.status;
|
|
166
|
+
|
|
167
|
+
// Validate transition
|
|
168
|
+
if (!this.isValidTransition(from, to)) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
error: `Invalid transition: ${from} -> ${to}. Valid transitions: ${VALID_TRANSITIONS[from].join(", ") || "none"}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Create updated checkpoint
|
|
176
|
+
const updated: RuntimeCheckpoint = {
|
|
177
|
+
...this.currentCheckpoint,
|
|
178
|
+
status: to,
|
|
179
|
+
updatedAt: new Date().toISOString(),
|
|
180
|
+
...data,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Create event
|
|
184
|
+
const event = this.createEvent(
|
|
185
|
+
this.currentCheckpoint.jobId,
|
|
186
|
+
`StateTransition:${from}->${to}`,
|
|
187
|
+
`Transitioned from ${from} to ${to}`,
|
|
188
|
+
{ from, to, ...data },
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await this.options.checkpointManager.save(updated);
|
|
193
|
+
await this.options.checkpointManager.appendEvent(
|
|
194
|
+
this.currentCheckpoint.jobId,
|
|
195
|
+
event,
|
|
196
|
+
);
|
|
197
|
+
this.currentCheckpoint = updated;
|
|
198
|
+
this.eventLog.push(event);
|
|
199
|
+
return { success: true, checkpoint: updated };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return { success: false, error: String(error) };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check if a transition is valid
|
|
207
|
+
*/
|
|
208
|
+
isValidTransition(from: JobStatus, to: JobStatus): boolean {
|
|
209
|
+
return VALID_TRANSITIONS[from]?.includes(to) ?? false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get available next states from current state
|
|
214
|
+
*/
|
|
215
|
+
getAvailableTransitions(): JobStatus[] {
|
|
216
|
+
if (!this.currentCheckpoint) return [];
|
|
217
|
+
return VALID_TRANSITIONS[this.currentCheckpoint.status] ?? [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get current checkpoint
|
|
222
|
+
*/
|
|
223
|
+
getCheckpoint(): RuntimeCheckpoint | null {
|
|
224
|
+
return this.currentCheckpoint;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get event log
|
|
229
|
+
*/
|
|
230
|
+
getEventLog(): RuntimeEvent[] {
|
|
231
|
+
return [...this.eventLog];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Set current task
|
|
236
|
+
*/
|
|
237
|
+
async setCurrentTask(taskId: string): Promise<TransitionResult> {
|
|
238
|
+
return this.transition(this.currentCheckpoint!.status, {
|
|
239
|
+
currentTaskId: taskId,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Set provider
|
|
245
|
+
*/
|
|
246
|
+
async setProvider(provider: string): Promise<TransitionResult> {
|
|
247
|
+
return this.transition(this.currentCheckpoint!.status, { provider });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Set resume time (for quota pause)
|
|
252
|
+
*/
|
|
253
|
+
async setResumeTime(resumeAt: string): Promise<TransitionResult> {
|
|
254
|
+
if (!this.currentCheckpoint) {
|
|
255
|
+
return { success: false, error: "No active job" };
|
|
256
|
+
}
|
|
257
|
+
this.currentCheckpoint.resumeAt = resumeAt;
|
|
258
|
+
this.currentCheckpoint.updatedAt = new Date().toISOString();
|
|
259
|
+
try {
|
|
260
|
+
await this.options.checkpointManager.save(this.currentCheckpoint);
|
|
261
|
+
return { success: true, checkpoint: this.currentCheckpoint };
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return { success: false, error: String(error) };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Record an error
|
|
269
|
+
*/
|
|
270
|
+
async recordError(error: string): Promise<TransitionResult> {
|
|
271
|
+
if (!this.currentCheckpoint) {
|
|
272
|
+
return { success: false, error: "No active job" };
|
|
273
|
+
}
|
|
274
|
+
this.currentCheckpoint.lastError = error;
|
|
275
|
+
this.currentCheckpoint.updatedAt = new Date().toISOString();
|
|
276
|
+
try {
|
|
277
|
+
await this.options.checkpointManager.save(this.currentCheckpoint);
|
|
278
|
+
return { success: true, checkpoint: this.currentCheckpoint };
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return { success: false, error: String(error) };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check if job is in a terminal state
|
|
286
|
+
*/
|
|
287
|
+
isTerminal(): boolean {
|
|
288
|
+
if (!this.currentCheckpoint) return false;
|
|
289
|
+
return ["ready_for_client", "cancelled", "archived"].includes(
|
|
290
|
+
this.currentCheckpoint.status,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check if job can be resumed
|
|
296
|
+
*/
|
|
297
|
+
canResume(): boolean {
|
|
298
|
+
if (!this.currentCheckpoint) return false;
|
|
299
|
+
return (
|
|
300
|
+
this.currentCheckpoint.status === "paused_quota" ||
|
|
301
|
+
this.currentCheckpoint.status === "blocked"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get job status summary
|
|
307
|
+
*/
|
|
308
|
+
getStatusSummary(): {
|
|
309
|
+
jobId: string;
|
|
310
|
+
status: JobStatus;
|
|
311
|
+
isTerminal: boolean;
|
|
312
|
+
canResume: boolean;
|
|
313
|
+
} | null {
|
|
314
|
+
if (!this.currentCheckpoint) return null;
|
|
315
|
+
return {
|
|
316
|
+
jobId: this.currentCheckpoint.jobId,
|
|
317
|
+
status: this.currentCheckpoint.status,
|
|
318
|
+
isTerminal: this.isTerminal(),
|
|
319
|
+
canResume: this.canResume(),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private createEvent(
|
|
324
|
+
jobId: string,
|
|
325
|
+
type: string,
|
|
326
|
+
message: string,
|
|
327
|
+
data?: Record<string, unknown>,
|
|
328
|
+
): RuntimeEvent {
|
|
329
|
+
return {
|
|
330
|
+
ts: new Date().toISOString(),
|
|
331
|
+
jobId,
|
|
332
|
+
type,
|
|
333
|
+
message,
|
|
334
|
+
data,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Factory to create a state machine with a JsonCheckpointManager
|
|
341
|
+
*/
|
|
342
|
+
export async function createJobStateMachine(
|
|
343
|
+
rootDir: string,
|
|
344
|
+
jobId?: string,
|
|
345
|
+
): Promise<{ machine: JobStateMachine; checkpoint: RuntimeCheckpoint | null }> {
|
|
346
|
+
const { JsonCheckpointManager } = await import(
|
|
347
|
+
"../packages/checkpoint/src/checkpoint-manager.ts"
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const manager = new JsonCheckpointManager(rootDir);
|
|
351
|
+
|
|
352
|
+
if (jobId) {
|
|
353
|
+
const checkpoint = await manager.load(jobId);
|
|
354
|
+
const machine = new JobStateMachine({ checkpointManager: manager });
|
|
355
|
+
if (checkpoint) {
|
|
356
|
+
machine.resumeJob(jobId);
|
|
357
|
+
}
|
|
358
|
+
return { machine, checkpoint };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const machine = new JobStateMachine({ checkpointManager: manager });
|
|
362
|
+
return { machine, checkpoint: null };
|
|
363
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop Runtime — RFC-0001
|
|
3
|
+
*
|
|
4
|
+
* Core execution loop for the harness runtime.
|
|
5
|
+
* Runs the repeated cycle:
|
|
6
|
+
* pick task -> assign model -> code -> run tests -> review diff
|
|
7
|
+
* -> if failed: repair
|
|
8
|
+
* -> if quota_limit: pause and resume later
|
|
9
|
+
* -> if blocked: escalate to human
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
LoopConfig,
|
|
14
|
+
LoopState,
|
|
15
|
+
JobStatus,
|
|
16
|
+
RuntimeCheckpoint,
|
|
17
|
+
RuntimeTask,
|
|
18
|
+
} from "../packages/types/src/runtime-types.ts";
|
|
19
|
+
import { JobStateMachine } from "./job-state-machine.ts";
|
|
20
|
+
import { TaskGraphManager } from "./task-graph.ts";
|
|
21
|
+
import { RepairEngine } from "./repair-engine.ts";
|
|
22
|
+
import type { CheckpointManager } from "./job-state-machine.ts";
|
|
23
|
+
|
|
24
|
+
export interface LoopResult {
|
|
25
|
+
success: boolean;
|
|
26
|
+
completed: boolean;
|
|
27
|
+
iterations: number;
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface LoopCallbacks {
|
|
32
|
+
onTaskStart?: (taskId: string) => Promise<void>;
|
|
33
|
+
onTaskComplete?: (taskId: string, report: unknown) => Promise<void>;
|
|
34
|
+
onTaskFailure?: (taskId: string, error: string) => Promise<void>;
|
|
35
|
+
onQuotaExceeded?: (provider: string) => Promise<void>;
|
|
36
|
+
onHumanEscalation?: (taskId: string, reason: string) => Promise<void>;
|
|
37
|
+
onIteration?: (iteration: number, status: string) => Promise<void>;
|
|
38
|
+
invokeAgent?: (
|
|
39
|
+
task: RuntimeTask,
|
|
40
|
+
context: unknown,
|
|
41
|
+
) => Promise<{ success: boolean; output?: string; error?: string }>;
|
|
42
|
+
runTests?: (
|
|
43
|
+
taskId: string,
|
|
44
|
+
worktreePath?: string,
|
|
45
|
+
) => Promise<{ passed: boolean; output?: string }>;
|
|
46
|
+
runReview?: (
|
|
47
|
+
taskId: string,
|
|
48
|
+
diffPath: string,
|
|
49
|
+
) => Promise<{ approved: boolean; comments?: string }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class LoopRuntime {
|
|
53
|
+
private state: LoopState;
|
|
54
|
+
private machine: JobStateMachine;
|
|
55
|
+
private graph: TaskGraphManager;
|
|
56
|
+
private repairEngine: RepairEngine;
|
|
57
|
+
private callbacks: LoopCallbacks;
|
|
58
|
+
private running = false;
|
|
59
|
+
private paused = false;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
config: LoopConfig,
|
|
63
|
+
checkpointManager: CheckpointManager,
|
|
64
|
+
callbacks: LoopCallbacks,
|
|
65
|
+
) {
|
|
66
|
+
this.state = {
|
|
67
|
+
jobId: config.jobId,
|
|
68
|
+
iteration: 0,
|
|
69
|
+
status: "running",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.machine = new JobStateMachine({ checkpointManager });
|
|
73
|
+
this.graph = new TaskGraphManager({ jobId: config.jobId });
|
|
74
|
+
this.repairEngine = new RepairEngine(config.jobId);
|
|
75
|
+
this.callbacks = callbacks;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Initialize the loop with a job
|
|
80
|
+
*/
|
|
81
|
+
async init(requirement: string): Promise<void> {
|
|
82
|
+
await this.machine.createJob(this.state.jobId, requirement);
|
|
83
|
+
await this.machine.transition("planning");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resume from checkpoint
|
|
88
|
+
*/
|
|
89
|
+
async resume(checkpoint: RuntimeCheckpoint): Promise<void> {
|
|
90
|
+
this.state.iteration = 0;
|
|
91
|
+
await this.machine.resumeJob(checkpoint.jobId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Run the main loop
|
|
96
|
+
*/
|
|
97
|
+
async run(): Promise<LoopResult> {
|
|
98
|
+
this.running = true;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
while (this.running) {
|
|
102
|
+
// Check for pause
|
|
103
|
+
if (this.paused) {
|
|
104
|
+
await this.saveCheckpoint();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.state.iteration++;
|
|
109
|
+
|
|
110
|
+
// Check iteration limit
|
|
111
|
+
if (this.state.iteration > 1000) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
completed: false,
|
|
115
|
+
iterations: this.state.iteration,
|
|
116
|
+
error: "Max iterations exceeded",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await this.callbacks.onIteration?.(
|
|
121
|
+
this.state.iteration,
|
|
122
|
+
this.state.status,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Check job status
|
|
126
|
+
if (this.machine.isTerminal()) {
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
completed: true,
|
|
130
|
+
iterations: this.state.iteration,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Pick next task
|
|
135
|
+
const nextTask = await this.pickNextTask();
|
|
136
|
+
if (!nextTask) {
|
|
137
|
+
// No more tasks, job is complete
|
|
138
|
+
await this.machine.transition("ready_for_client");
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
completed: true,
|
|
142
|
+
iterations: this.state.iteration,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.state.currentTaskId = nextTask.id;
|
|
147
|
+
this.state.status = "running";
|
|
148
|
+
|
|
149
|
+
// Execute task
|
|
150
|
+
const result = await this.executeTask(nextTask);
|
|
151
|
+
|
|
152
|
+
if (!result.success) {
|
|
153
|
+
// Handle failure
|
|
154
|
+
const { repairTask } = this.repairEngine.analyzeAndRepair(
|
|
155
|
+
nextTask.id,
|
|
156
|
+
result.error ?? "Unknown error",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (this.repairEngine.shouldEscalate(repairTask.id)) {
|
|
160
|
+
await this.machine.transition("waiting_human");
|
|
161
|
+
await this.callbacks.onHumanEscalation?.(
|
|
162
|
+
nextTask.id,
|
|
163
|
+
result.error ?? "Unknown error",
|
|
164
|
+
);
|
|
165
|
+
} else {
|
|
166
|
+
await this.machine.transition("repairing");
|
|
167
|
+
// Continue loop to attempt repair
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
// Task succeeded
|
|
171
|
+
this.graph.updateTaskStatus(nextTask.id, "done");
|
|
172
|
+
await this.machine.transition("testing");
|
|
173
|
+
|
|
174
|
+
// Run tests
|
|
175
|
+
const testResult = await this.callbacks.runTests?.(
|
|
176
|
+
nextTask.id,
|
|
177
|
+
nextTask.worktreePath,
|
|
178
|
+
);
|
|
179
|
+
if (!testResult?.passed) {
|
|
180
|
+
await this.machine.transition("repairing");
|
|
181
|
+
this.graph.updateTaskStatus(nextTask.id, "failed");
|
|
182
|
+
} else {
|
|
183
|
+
await this.machine.transition("reviewing");
|
|
184
|
+
|
|
185
|
+
// Run review
|
|
186
|
+
const reviewResult = await this.callbacks.runReview?.(
|
|
187
|
+
nextTask.id,
|
|
188
|
+
nextTask.worktreePath ?? "",
|
|
189
|
+
);
|
|
190
|
+
if (!reviewResult?.approved) {
|
|
191
|
+
await this.machine.transition("repairing");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await this.saveCheckpoint();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
completed: false,
|
|
202
|
+
iterations: this.state.iteration,
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
completed: false,
|
|
208
|
+
iterations: this.state.iteration,
|
|
209
|
+
error: String(error),
|
|
210
|
+
};
|
|
211
|
+
} finally {
|
|
212
|
+
this.running = false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Pick the next task to execute
|
|
218
|
+
*/
|
|
219
|
+
private async pickNextTask(): Promise<RuntimeTask | null> {
|
|
220
|
+
const readyTasks = this.graph.getReadyTasks();
|
|
221
|
+
|
|
222
|
+
// Pick the first ready task that's not assigned
|
|
223
|
+
for (const task of readyTasks) {
|
|
224
|
+
if (!task.assignedAgent) {
|
|
225
|
+
this.graph.assignAgent(task.id, "loop-runtime");
|
|
226
|
+
return task as unknown as RuntimeTask;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Execute a single task
|
|
235
|
+
*/
|
|
236
|
+
private async executeTask(task: RuntimeTask): Promise<{
|
|
237
|
+
success: boolean;
|
|
238
|
+
output?: string;
|
|
239
|
+
error?: string;
|
|
240
|
+
}> {
|
|
241
|
+
try {
|
|
242
|
+
await this.callbacks.onTaskStart?.(task.id);
|
|
243
|
+
|
|
244
|
+
const result = await this.callbacks.invokeAgent?.(task, {
|
|
245
|
+
jobId: this.state.jobId,
|
|
246
|
+
requirement: this.machine.getCheckpoint()?.requirement,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (result?.success) {
|
|
250
|
+
await this.callbacks.onTaskComplete?.(task.id, {
|
|
251
|
+
output: result.output,
|
|
252
|
+
});
|
|
253
|
+
return { success: true, output: result.output };
|
|
254
|
+
} else {
|
|
255
|
+
const error = result?.error ?? "Agent returned failure";
|
|
256
|
+
await this.callbacks.onTaskFailure?.(task.id, error);
|
|
257
|
+
return { success: false, error };
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return { success: false, error: String(error) };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Pause the loop
|
|
266
|
+
*/
|
|
267
|
+
pause(): void {
|
|
268
|
+
this.paused = true;
|
|
269
|
+
this.state.status = "paused";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Resume the loop
|
|
274
|
+
*/
|
|
275
|
+
async resumeLoop(): Promise<void> {
|
|
276
|
+
this.paused = false;
|
|
277
|
+
this.state.status = "running";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Stop the loop
|
|
282
|
+
*/
|
|
283
|
+
stop(): void {
|
|
284
|
+
this.running = false;
|
|
285
|
+
this.state.status = "failed";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Save checkpoint
|
|
290
|
+
*/
|
|
291
|
+
private async saveCheckpoint(): Promise<void> {
|
|
292
|
+
const checkpoint = this.machine.getCheckpoint();
|
|
293
|
+
if (checkpoint) {
|
|
294
|
+
await this.machine.transition(checkpoint.status, {
|
|
295
|
+
currentTaskId: this.state.currentTaskId,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get current state
|
|
302
|
+
*/
|
|
303
|
+
getState(): LoopState {
|
|
304
|
+
return this.state;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get job status
|
|
309
|
+
*/
|
|
310
|
+
getStatus(): {
|
|
311
|
+
status: JobStatus;
|
|
312
|
+
canResume: boolean;
|
|
313
|
+
isTerminal: boolean;
|
|
314
|
+
} | null {
|
|
315
|
+
const summary = this.machine.getStatusSummary();
|
|
316
|
+
if (!summary) return null;
|
|
317
|
+
return {
|
|
318
|
+
status: summary.status,
|
|
319
|
+
canResume: summary.canResume,
|
|
320
|
+
isTerminal: summary.isTerminal,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Check if loop is running
|
|
326
|
+
*/
|
|
327
|
+
isRunning(): boolean {
|
|
328
|
+
return this.running && !this.paused;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Check if loop is paused
|
|
333
|
+
*/
|
|
334
|
+
isPaused(): boolean {
|
|
335
|
+
return this.paused;
|
|
336
|
+
}
|
|
337
|
+
}
|