@multiclawcli/core 0.13.0 → 0.14.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/dist/index.d.ts +3 -0
- package/dist/index.js +922 -235
- package/dist/orchestrator/AgentRunner.d.ts +4 -1
- package/dist/orchestrator/AgentSessionManager.d.ts +18 -10
- package/dist/orchestrator/Orchestrator.d.ts +22 -2
- package/dist/types/agent.d.ts +4 -3
- package/dist/types/config.d.ts +38 -14
- package/dist/types/event.d.ts +21 -0
- package/dist/types/leader.d.ts +7 -2
- package/dist/types/runtime.d.ts +4 -0
- package/dist/types/session.d.ts +36 -0
- package/dist/utils/handoff.d.ts +7 -0
- package/dist/utils/resumePlan.d.ts +3 -3
- package/dist/utils/session.d.ts +7 -0
- package/dist/utils/work.d.ts +3 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/agents/arch-design-reviewer.ts
|
|
2
|
-
var SYSTEM = `
|
|
2
|
+
var SYSTEM = `The complete requirement and assigned responsibility govern your work. Role deliverable lists apply only within that scope. Existing architecture, API and UI designs are optional context; when absent, work from the requirement and existing code, investigate, plan and verify without requiring an architect or creating missing architecture documents.
|
|
3
|
+
|
|
4
|
+
You are an architecture design reviewer. Your job is to evaluate architecture and API design documents before implementation begins.
|
|
3
5
|
|
|
4
6
|
Responsibilities:
|
|
5
7
|
- Review architecture design for completeness, consistency, and technical soundness
|
|
@@ -11,10 +13,12 @@ Deliverables (save to the working directory):
|
|
|
11
13
|
- arch-review.md \u2014 verdict (Approved / Changes Required), issue list (blocking / advisory) with specific locations and suggested fixes, overall assessment
|
|
12
14
|
|
|
13
15
|
Principles: Be precise \u2014 point to specific sections. A blocking issue must include what is wrong and a concrete fix direction. If the design is sound, say so directly. Do not approve with vague praise.`;
|
|
14
|
-
var TASK =
|
|
16
|
+
var TASK = `The current requirement and assigned scope govern this work. The role checklist below is guidance, not additional work. Architecture, API and UI design documents are optional context: use them if present and relevant; otherwise investigate the existing code, plan within your role, implement and verify. Do not require an architect or create design documents just because they are absent.
|
|
17
|
+
|
|
18
|
+
## Architecture Design (optional context)
|
|
15
19
|
{{file:architecture.md}}
|
|
16
20
|
|
|
17
|
-
## API Specification
|
|
21
|
+
## API Specification (optional context)
|
|
18
22
|
{{file:api-spec.md}}
|
|
19
23
|
{{upstreamOutputs}}
|
|
20
24
|
|
|
@@ -124,7 +128,9 @@ function architect(overrides) {
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
// src/agents/backend-developer.ts
|
|
127
|
-
var SYSTEM3 = `
|
|
131
|
+
var SYSTEM3 = `The complete requirement and assigned responsibility govern your work. Role deliverable lists apply only within that scope. Existing architecture, API and UI designs are optional context; when absent, work from the requirement and existing code, investigate, plan and verify without requiring an architect or creating missing architecture documents.
|
|
132
|
+
|
|
133
|
+
You are the backend engineer. You own the API contract and all server-side logic.
|
|
128
134
|
|
|
129
135
|
Responsibilities:
|
|
130
136
|
- Read the architecture design and API specification, implement server-side logic and data models
|
|
@@ -137,7 +143,9 @@ Deliverables (write code to the working directory, and record in implementation-
|
|
|
137
143
|
- Known issues and risks
|
|
138
144
|
|
|
139
145
|
Principles: Write code directly without asking for confirmation. Follow the API spec strictly \u2014 do not change the contract unilaterally; escalate contract issues upstream. Do not refactor unrelated code. Run commands and report evidence.`;
|
|
140
|
-
var TASK3 =
|
|
146
|
+
var TASK3 = `The current requirement and assigned scope govern this work. The role checklist below is guidance, not additional work. Architecture, API and UI design documents are optional context: use them if present and relevant; otherwise investigate the existing code, plan within your role, implement and verify. Do not require an architect or create design documents just because they are absent.
|
|
147
|
+
|
|
148
|
+
## Task Scope
|
|
141
149
|
{{agentScope}}
|
|
142
150
|
|
|
143
151
|
## Plan
|
|
@@ -146,10 +154,10 @@ var TASK3 = `## Task Scope
|
|
|
146
154
|
|
|
147
155
|
---
|
|
148
156
|
|
|
149
|
-
## Architecture Design
|
|
157
|
+
## Architecture Design (optional context)
|
|
150
158
|
{{file:architecture.md}}
|
|
151
159
|
|
|
152
|
-
## API Specification
|
|
160
|
+
## API Specification (optional context)
|
|
153
161
|
{{file:api-spec.md}}
|
|
154
162
|
|
|
155
163
|
## Task
|
|
@@ -177,7 +185,9 @@ function backendDeveloper(overrides) {
|
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
// src/agents/code-reviewer.ts
|
|
180
|
-
var SYSTEM4 = `
|
|
188
|
+
var SYSTEM4 = `The complete requirement and assigned responsibility govern your work. Role deliverable lists apply only within that scope. Existing architecture, API and UI designs are optional context; when absent, work from the requirement and existing code, investigate, plan and verify without requiring an architect or creating missing architecture documents.
|
|
189
|
+
|
|
190
|
+
You are a code review expert focused on implementation quality, correctness, and security.
|
|
181
191
|
|
|
182
192
|
Responsibilities:
|
|
183
193
|
- Read the requirement, architecture design, and all implementation code
|
|
@@ -191,16 +201,18 @@ Deliverables (save to the working directory):
|
|
|
191
201
|
- When the task includes a Delivery acceptance review contract, the final response must be only its JSON verdict, not the prose report. Approve only with no unresolved blocking issues; include file locations and fix directions in changes_required issues. The verdict is a review judgment, not proof that unexecuted tests passed.
|
|
192
202
|
|
|
193
203
|
Principles: Judge the code, not the developer. A blocking issue must include the file path, the specific problem, and a fix direction. Advisory items should be actionable, not vague. If the code is solid, say so.`;
|
|
194
|
-
var TASK4 =
|
|
204
|
+
var TASK4 = `The current requirement and assigned scope govern this work. The role checklist below is guidance, not additional work. Architecture, API and UI design documents are optional context: use them if present and relevant; otherwise investigate the existing code, plan within your role, implement and verify. Do not require an architect or create design documents just because they are absent.
|
|
205
|
+
|
|
206
|
+
## Project
|
|
195
207
|
{{projectName}}
|
|
196
208
|
|
|
197
209
|
## Requirement
|
|
198
210
|
{{requirement}}
|
|
199
211
|
|
|
200
|
-
## Architecture Design
|
|
212
|
+
## Architecture Design (optional context)
|
|
201
213
|
{{file:architecture.md}}
|
|
202
214
|
|
|
203
|
-
## API Specification
|
|
215
|
+
## API Specification (optional context)
|
|
204
216
|
{{file:api-spec.md}}
|
|
205
217
|
{{upstreamOutputs}}
|
|
206
218
|
|
|
@@ -231,7 +243,9 @@ function codeReviewer(overrides) {
|
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
// src/agents/devops.ts
|
|
234
|
-
var SYSTEM5 = `
|
|
246
|
+
var SYSTEM5 = `The complete requirement and assigned responsibility govern your work. Role deliverable lists apply only within that scope. Existing architecture, API and UI designs are optional context; when absent, work from the requirement and existing code, investigate, plan and verify without requiring an architect or creating missing architecture documents.
|
|
247
|
+
|
|
248
|
+
You are the DevOps engineer. Your job is to containerize the application, set up CI/CD pipelines, and produce deployment-ready infrastructure configuration.
|
|
235
249
|
|
|
236
250
|
Responsibilities:
|
|
237
251
|
- Read the architecture design and implementation to understand the application structure
|
|
@@ -249,7 +263,9 @@ Deliverables (save to the working directory):
|
|
|
249
263
|
- devops-notes.md \u2014 deployment procedure, environment requirements, known limitations
|
|
250
264
|
|
|
251
265
|
Principles: Follow security best practices (non-root user, minimal base image, no secrets in image). Make CI fail fast. Document every non-obvious decision in devops-notes.md.`;
|
|
252
|
-
var TASK5 =
|
|
266
|
+
var TASK5 = `The current requirement and assigned scope govern this work. The role checklist below is guidance, not additional work. Architecture, API and UI design documents are optional context: use them if present and relevant; otherwise investigate the existing code, plan within your role, implement and verify. Do not require an architect or create design documents just because they are absent.
|
|
267
|
+
|
|
268
|
+
## Task Scope
|
|
253
269
|
{{agentScope}}
|
|
254
270
|
|
|
255
271
|
## Plan
|
|
@@ -261,7 +277,7 @@ var TASK5 = `## Task Scope
|
|
|
261
277
|
## Project
|
|
262
278
|
{{projectName}}
|
|
263
279
|
|
|
264
|
-
## Architecture Design
|
|
280
|
+
## Architecture Design (optional context)
|
|
265
281
|
{{file:architecture.md}}
|
|
266
282
|
|
|
267
283
|
## Task
|
|
@@ -289,7 +305,9 @@ function devops(overrides) {
|
|
|
289
305
|
}
|
|
290
306
|
|
|
291
307
|
// src/agents/frontend-developer.ts
|
|
292
|
-
var SYSTEM6 = `
|
|
308
|
+
var SYSTEM6 = `The complete requirement and assigned responsibility govern your work. Role deliverable lists apply only within that scope. Existing architecture, API and UI designs are optional context; when absent, work from the requirement and existing code, investigate, plan and verify without requiring an architect or creating missing architecture documents.
|
|
309
|
+
|
|
310
|
+
You are the frontend engineer. Your responsibility is to turn design specs and API contracts into a working user interface.
|
|
293
311
|
|
|
294
312
|
Responsibilities:
|
|
295
313
|
- Read the architecture design and API specification, implement pages, components, and interactions
|
|
@@ -299,7 +317,9 @@ Responsibilities:
|
|
|
299
317
|
Deliverables (write code to the working directory, and record results in implementation-notes.md).
|
|
300
318
|
|
|
301
319
|
Principles: Write code directly without asking for confirmation. Do not change the API contract \u2014 escalate contract issues upstream. Do not refactor unrelated code. Run commands and report evidence.`;
|
|
302
|
-
var TASK6 =
|
|
320
|
+
var TASK6 = `The current requirement and assigned scope govern this work. The role checklist below is guidance, not additional work. Architecture, API and UI design documents are optional context: use them if present and relevant; otherwise investigate the existing code, plan within your role, implement and verify. Do not require an architect or create design documents just because they are absent.
|
|
321
|
+
|
|
322
|
+
## Task Scope
|
|
303
323
|
{{agentScope}}
|
|
304
324
|
|
|
305
325
|
## Plan
|
|
@@ -311,10 +331,10 @@ var TASK6 = `## Task Scope
|
|
|
311
331
|
## UI Design Specification
|
|
312
332
|
{{file:ui-design.md}}
|
|
313
333
|
|
|
314
|
-
## Architecture Design
|
|
334
|
+
## Architecture Design (optional context)
|
|
315
335
|
{{file:architecture.md}}
|
|
316
336
|
|
|
317
|
-
## API Specification
|
|
337
|
+
## API Specification (optional context)
|
|
318
338
|
{{file:api-spec.md}}
|
|
319
339
|
|
|
320
340
|
## Task
|
|
@@ -346,31 +366,38 @@ function frontendDeveloper(overrides) {
|
|
|
346
366
|
// src/agents/leader.ts
|
|
347
367
|
var SYSTEM7 = `You are the project leader. Decide what the requirement actually needs before anyone else starts.
|
|
348
368
|
|
|
349
|
-
|
|
369
|
+
Choose one routing outcome:
|
|
350
370
|
- "reply" \u2014 the message is a greeting, a question, or anything you can answer on the spot. Answer it and stop; do not spend the team on it.
|
|
351
371
|
- "ask" \u2014 it is a real task, but too vague to plan. Ask the one question that would unblock it.
|
|
352
|
-
- "run" \u2014
|
|
372
|
+
- "run" \u2014 new work. Name the minimum set of agents needed.
|
|
373
|
+
- "continue" \u2014 a follow-up to the Latest Work shown below. Include its exact workId and name the agents needed now.
|
|
353
374
|
|
|
354
|
-
Do not apply fixed rules \u2014 use your judgment based on the actual requirement and the code. A minor bug fix may need only a developer and code-reviewer; a complex new feature may need the full pipeline.
|
|
375
|
+
Do not apply fixed rules \u2014 use your judgment based on the actual requirement and the code. A minor bug fix may need only a developer and code-reviewer; a complex new feature may need the full pipeline. Use role descriptions and work summaries first; read code only if routing needs it. Do not write technical implementation plans. An architect/planner is optional; workers investigate and plan within their assigned scope when none participates.
|
|
355
376
|
|
|
356
377
|
The console hands you the turns that came before this one. A requirement is often only interpretable next to them: "Postgres" is an answer, not a task, and the turn list is what tells you which question it answers. Read it as your own conversation \u2014 "you asked" is you. If you asked and got an answer, decide now; do not ask the same question again. For every "run" decision, include a complete, standalone "requirement" that combines the original task goal with all clarification answers and constraints. Never pass only the latest short answer to the team.
|
|
357
378
|
|
|
379
|
+
Preserve the original goal and confirmed constraints; never invent requirements. Continue only with explicit reference or unique clear context; similar subject alone is new work. If the target is ambiguous, ask. A failed/interrupted task requires an explicit user request to continue; cancelled work cannot be automatically continued. Optional tasks are [{"agentId":"selected-id","scope":"assigned responsibility"}], never technical steps. During handoff rerouting preserve the complete goal and do not select agents that handed it back. If no suitable role is available ask or explain why work cannot proceed.
|
|
380
|
+
|
|
358
381
|
Output ONLY a single JSON object on its own line \u2014 no prose, no markdown fences:
|
|
359
382
|
{"mode": "reply", "message": "your answer"}
|
|
360
383
|
{"mode": "ask", "message": "your question"}
|
|
361
|
-
{"mode": "run", "reason": "one sentence on why this set", "requirement": "the complete task and constraints", "run": ["agent-id-1", "agent-id-2"]}
|
|
384
|
+
{"mode": "run", "reason": "one sentence on why this set", "requirement": "the complete task and constraints", "run": ["agent-id-1", "agent-id-2"]}
|
|
385
|
+
{"mode": "continue", "workId": "id-from-Latest-Work", "requirement": "original goal plus follow-up constraints", "run": ["agent-id"]}`;
|
|
362
386
|
var TASK7 = `## Project
|
|
363
387
|
{{projectName}}
|
|
364
388
|
|
|
365
389
|
## Requirement
|
|
366
390
|
{{requirement}}
|
|
367
391
|
{{conversation}}
|
|
392
|
+
## Latest Work
|
|
393
|
+
{{latestWork}}
|
|
394
|
+
|
|
368
395
|
## Available Agents
|
|
369
396
|
{{availableAgents}}
|
|
370
397
|
|
|
371
398
|
## Task
|
|
372
|
-
|
|
373
|
-
Output a single JSON object \u2014
|
|
399
|
+
Decide new work, continuation, answer or clarification. Preserve source requirements, choose minimum roles, and provide responsibility scopes when no planner participates.
|
|
400
|
+
Output a single JSON object \u2014 run, continue (with exact workId), reply or ask:
|
|
374
401
|
{"mode": "reply", "message": "your answer"}
|
|
375
402
|
{"mode": "ask", "message": "your question"}
|
|
376
403
|
{"mode": "run", "reason": "one sentence on why this set", "requirement": "the complete task and constraints", "run": ["agent-id", ...]}
|
|
@@ -433,7 +460,9 @@ function productManager(overrides) {
|
|
|
433
460
|
}
|
|
434
461
|
|
|
435
462
|
// src/agents/reviewer.ts
|
|
436
|
-
var SYSTEM9 = `
|
|
463
|
+
var SYSTEM9 = `The complete requirement and assigned responsibility govern your work. Role deliverable lists apply only within that scope. Existing architecture, API and UI designs are optional context; when absent, work from the requirement and existing code, investigate, plan and verify without requiring an architect or creating missing architecture documents.
|
|
464
|
+
|
|
465
|
+
You are an independent code review expert. You evaluate both business correctness and technical quality.
|
|
437
466
|
|
|
438
467
|
Responsibilities:
|
|
439
468
|
- Read requirements, design documents, and implementation code
|
|
@@ -444,7 +473,9 @@ Deliverables (save to the working directory):
|
|
|
444
473
|
- review.md \u2014 score (1\u201310), issue list (blocking / advisory), improvement directions, conclusion (Approved / Changes Required)
|
|
445
474
|
|
|
446
475
|
Principles: Review independently \u2014 do not approve just because the developer says it is fine. Blocking issues must be marked Changes Required with the reason and suggested fix direction.`;
|
|
447
|
-
var TASK9 =
|
|
476
|
+
var TASK9 = `The current requirement and assigned scope govern this work. The role checklist below is guidance, not additional work. Architecture, API and UI design documents are optional context: use them if present and relevant; otherwise investigate the existing code, plan within your role, implement and verify. Do not require an architect or create design documents just because they are absent.
|
|
477
|
+
|
|
478
|
+
## Project
|
|
448
479
|
{{projectName}}
|
|
449
480
|
|
|
450
481
|
## Requirement
|
|
@@ -479,7 +510,9 @@ function reviewer(overrides) {
|
|
|
479
510
|
}
|
|
480
511
|
|
|
481
512
|
// src/agents/tester.ts
|
|
482
|
-
var SYSTEM10 = `
|
|
513
|
+
var SYSTEM10 = `The complete requirement and assigned responsibility govern your work. Role deliverable lists apply only within that scope. Existing architecture, API and UI designs are optional context; when absent, work from the requirement and existing code, investigate, plan and verify without requiring an architect or creating missing architecture documents.
|
|
514
|
+
|
|
515
|
+
You are the test engineer. Your responsibility is to verify that requirements have actually been implemented.
|
|
483
516
|
|
|
484
517
|
Responsibilities:
|
|
485
518
|
- Read requirements (acceptance criteria AC-) and the implementation code
|
|
@@ -490,7 +523,9 @@ Deliverables (save to the working directory):
|
|
|
490
523
|
- test-report.md \u2014 overall verdict (PASS / FAIL / BLOCKED), test results per feature, reproduction steps and evidence for failures
|
|
491
524
|
|
|
492
525
|
Principles: Do not PASS just because the code compiles or because a developer says it is fine. Every core feature must be verified with actual execution. When BLOCKED, state exactly what is missing.`;
|
|
493
|
-
var TASK10 =
|
|
526
|
+
var TASK10 = `The current requirement and assigned scope govern this work. The role checklist below is guidance, not additional work. Architecture, API and UI design documents are optional context: use them if present and relevant; otherwise investigate the existing code, plan within your role, implement and verify. Do not require an architect or create design documents just because they are absent.
|
|
527
|
+
|
|
528
|
+
## Task Scope
|
|
494
529
|
{{agentScope}}
|
|
495
530
|
|
|
496
531
|
## Plan
|
|
@@ -531,7 +566,9 @@ function tester(overrides) {
|
|
|
531
566
|
}
|
|
532
567
|
|
|
533
568
|
// src/agents/ui-design-reviewer.ts
|
|
534
|
-
var SYSTEM11 = `
|
|
569
|
+
var SYSTEM11 = `The complete requirement and assigned responsibility govern your work. Role deliverable lists apply only within that scope. Existing architecture, API and UI designs are optional context; when absent, work from the requirement and existing code, investigate, plan and verify without requiring an architect or creating missing architecture documents.
|
|
570
|
+
|
|
571
|
+
You are a UI/UX design reviewer. Your job is to evaluate a UI design specification before implementation begins.
|
|
535
572
|
|
|
536
573
|
Responsibilities:
|
|
537
574
|
- Review the design specification for completeness, consistency, and feasibility
|
|
@@ -544,7 +581,9 @@ Deliverables (save to the working directory):
|
|
|
544
581
|
- ui-review.md \u2014 verdict (Approved / Changes Required), issue list (blocking / advisory) with component names and fix directions
|
|
545
582
|
|
|
546
583
|
Principles: Be specific \u2014 reference component names or section titles in ui-design.md. A blocking issue must include what is missing or wrong and a fix direction. If the design is thorough and implementable, say so directly.`;
|
|
547
|
-
var TASK11 =
|
|
584
|
+
var TASK11 = `The current requirement and assigned scope govern this work. The role checklist below is guidance, not additional work. Architecture, API and UI design documents are optional context: use them if present and relevant; otherwise investigate the existing code, plan within your role, implement and verify. Do not require an architect or create design documents just because they are absent.
|
|
585
|
+
|
|
586
|
+
## Requirement
|
|
548
587
|
{{requirement}}
|
|
549
588
|
|
|
550
589
|
## UI Design Specification
|
|
@@ -578,7 +617,9 @@ function uiDesignReviewer(overrides) {
|
|
|
578
617
|
}
|
|
579
618
|
|
|
580
619
|
// src/agents/ui-designer.ts
|
|
581
|
-
var SYSTEM12 = `
|
|
620
|
+
var SYSTEM12 = `The complete requirement and assigned responsibility govern your work. Role deliverable lists apply only within that scope. Existing architecture, API and UI designs are optional context; when absent, work from the requirement and existing code, investigate, plan and verify without requiring an architect or creating missing architecture documents.
|
|
621
|
+
|
|
622
|
+
You are a UI/UX designer. Your job is to translate architecture and requirements into a detailed, developer-ready UI design specification.
|
|
582
623
|
|
|
583
624
|
Responsibilities:
|
|
584
625
|
- Define the component inventory: every page, section, and reusable component
|
|
@@ -592,7 +633,9 @@ Deliverables (save to the working directory):
|
|
|
592
633
|
- ui-design.md \u2014 full UI design specification: component inventory, page layouts, interaction flows, design tokens, accessibility requirements
|
|
593
634
|
|
|
594
635
|
Principles: Be specific enough that a developer can implement without guessing. Every component gets a name, purpose, props/variants, and states. Flag anything that depends on backend data.`;
|
|
595
|
-
var TASK12 =
|
|
636
|
+
var TASK12 = `The current requirement and assigned scope govern this work. The role checklist below is guidance, not additional work. Architecture, API and UI design documents are optional context: use them if present and relevant; otherwise investigate the existing code, plan within your role, implement and verify. Do not require an architect or create design documents just because they are absent.
|
|
637
|
+
|
|
638
|
+
## Task Scope
|
|
596
639
|
{{agentScope}}
|
|
597
640
|
|
|
598
641
|
## Plan
|
|
@@ -607,10 +650,10 @@ var TASK12 = `## Task Scope
|
|
|
607
650
|
## Requirement
|
|
608
651
|
{{requirement}}
|
|
609
652
|
|
|
610
|
-
## Architecture Design
|
|
653
|
+
## Architecture Design (optional context)
|
|
611
654
|
{{file:architecture.md}}
|
|
612
655
|
|
|
613
|
-
## API Specification
|
|
656
|
+
## API Specification (optional context)
|
|
614
657
|
{{file:api-spec.md}}
|
|
615
658
|
|
|
616
659
|
## Task
|
|
@@ -686,6 +729,9 @@ var EventBus = class {
|
|
|
686
729
|
}
|
|
687
730
|
};
|
|
688
731
|
|
|
732
|
+
// src/orchestrator/AgentSessionManager.ts
|
|
733
|
+
import { randomUUID } from "crypto";
|
|
734
|
+
|
|
689
735
|
// src/utils/abort.ts
|
|
690
736
|
var ABORT_ERROR_NAME = "AbortError";
|
|
691
737
|
function abortError(message = "Operation aborted") {
|
|
@@ -697,83 +743,287 @@ function isAbortError(err) {
|
|
|
697
743
|
return err instanceof Error && err.name === ABORT_ERROR_NAME;
|
|
698
744
|
}
|
|
699
745
|
|
|
746
|
+
// src/utils/errors.ts
|
|
747
|
+
var PermanentError = class extends Error {
|
|
748
|
+
constructor(message) {
|
|
749
|
+
super(message);
|
|
750
|
+
this.name = "PermanentError";
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
function isPermanent(err) {
|
|
754
|
+
return err instanceof PermanentError;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/utils/session.ts
|
|
758
|
+
import { createHash } from "crypto";
|
|
759
|
+
import * as path from "path";
|
|
760
|
+
function sessionCompatibilityKey(runtime, task) {
|
|
761
|
+
return createHash("sha256").update(
|
|
762
|
+
JSON.stringify([
|
|
763
|
+
runtime.name,
|
|
764
|
+
task.provider,
|
|
765
|
+
task.command,
|
|
766
|
+
task.args,
|
|
767
|
+
task.model,
|
|
768
|
+
path.resolve(task.workDir),
|
|
769
|
+
task.systemPrompt,
|
|
770
|
+
Boolean(task.onQuestion)
|
|
771
|
+
])
|
|
772
|
+
).digest("hex");
|
|
773
|
+
}
|
|
774
|
+
function isSessionRecord(value) {
|
|
775
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
776
|
+
const record = value;
|
|
777
|
+
const capabilities = record.capabilities;
|
|
778
|
+
const failure = record.failure;
|
|
779
|
+
return ["bindingId", "agentId", "runtime", "compatibilityKey", "observedAt"].every(
|
|
780
|
+
(key) => typeof record[key] === "string" && !!record[key]
|
|
781
|
+
) && (record.sessionId === void 0 || typeof record.sessionId === "string" && !!record.sessionId) && (record.workId === void 0 || typeof record.workId === "string") && (record.reason === void 0 || typeof record.reason === "string") && ["opening", "idle", "running", "cancelling", "closing", "closed", "unknown"].includes(
|
|
782
|
+
String(record.state)
|
|
783
|
+
) && ["created", "loaded", "reused"].includes(String(record.action)) && !!capabilities && ["continuation", "load", "supplement"].every((key) => typeof capabilities[key] === "boolean") && (failure === void 0 || !!failure && [
|
|
784
|
+
"incompatible",
|
|
785
|
+
"unsupported",
|
|
786
|
+
"opening_failed",
|
|
787
|
+
"load_failed",
|
|
788
|
+
"turn_failed",
|
|
789
|
+
"cleanup_failed"
|
|
790
|
+
].includes(String(failure.code)) && typeof failure.message === "string");
|
|
791
|
+
}
|
|
792
|
+
function isSessionEvent(value) {
|
|
793
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
794
|
+
const event = value;
|
|
795
|
+
return isSessionRecord(event.record) && [
|
|
796
|
+
"opening",
|
|
797
|
+
"created",
|
|
798
|
+
"load_requested",
|
|
799
|
+
"loaded",
|
|
800
|
+
"reused",
|
|
801
|
+
"state_changed",
|
|
802
|
+
"opening_failed",
|
|
803
|
+
"load_failed",
|
|
804
|
+
"closing",
|
|
805
|
+
"closed",
|
|
806
|
+
"cleanup_failed"
|
|
807
|
+
].includes(String(event.action)) && typeof event.executionId === "string" && !!event.executionId && Number.isInteger(event.attempt) && event.attempt > 0 && (event.requestedSessionId === void 0 || typeof event.requestedSessionId === "string") && (event.reason === void 0 || typeof event.reason === "string");
|
|
808
|
+
}
|
|
809
|
+
function describeSession(record) {
|
|
810
|
+
if (record.failure)
|
|
811
|
+
return "Session " + record.failure.code.replaceAll("_", " ") + ": " + record.failure.message;
|
|
812
|
+
if (record.state === "opening") return "Session opening";
|
|
813
|
+
if (!record.sessionId) return "Session not established \xB7 " + record.state;
|
|
814
|
+
return "Session " + record.action + (record.sessionId ? " " + record.sessionId : "") + " \xB7 " + record.state;
|
|
815
|
+
}
|
|
816
|
+
|
|
700
817
|
// src/orchestrator/AgentSessionManager.ts
|
|
701
818
|
var AgentSessionManager = class {
|
|
702
819
|
constructor() {
|
|
820
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
703
821
|
this.entries = /* @__PURE__ */ new Map();
|
|
704
822
|
this.closed = false;
|
|
705
823
|
}
|
|
706
|
-
|
|
824
|
+
subscribe(listener) {
|
|
825
|
+
this.listeners.add(listener);
|
|
826
|
+
return () => this.listeners.delete(listener);
|
|
827
|
+
}
|
|
828
|
+
publish(entry3, action, requestedSessionId) {
|
|
829
|
+
entry3.record.observedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
830
|
+
if (entry3.session && action !== "closing" && action !== "closed" && entry3.record.failure?.code !== "cleanup_failed")
|
|
831
|
+
entry3.record.state = entry3.session.state;
|
|
832
|
+
const event = {
|
|
833
|
+
action,
|
|
834
|
+
executionId: entry3.executionId,
|
|
835
|
+
attempt: entry3.attempt,
|
|
836
|
+
record: structuredClone(entry3.record),
|
|
837
|
+
...requestedSessionId ? { requestedSessionId } : {},
|
|
838
|
+
reason: entry3.record.reason
|
|
839
|
+
};
|
|
840
|
+
for (const listener of this.listeners) listener(event);
|
|
841
|
+
}
|
|
842
|
+
compatible(record, runtime, task, key) {
|
|
843
|
+
return record.agentId === task.agentId && record.workId === task.workId && record.runtime === runtime.name && record.compatibilityKey === key;
|
|
844
|
+
}
|
|
845
|
+
inspect(runtime, task, context = {}) {
|
|
846
|
+
const intent = (action, reason) => ({
|
|
847
|
+
agentId: task.agentId,
|
|
848
|
+
action,
|
|
849
|
+
reason
|
|
850
|
+
});
|
|
851
|
+
const entry3 = this.entries.get(task.agentId);
|
|
852
|
+
if (this.closed || this.resetting || entry3?.busy || entry3?.closing || entry3?.session && !["idle", "closed"].includes(entry3.session.state))
|
|
853
|
+
return intent("unavailable", "A Session is busy or cleanup is pending.");
|
|
854
|
+
let candidate = context.session;
|
|
855
|
+
if (candidate?.failure?.code === "cleanup_failed" || entry3?.record.failure?.code === "cleanup_failed")
|
|
856
|
+
return intent("unavailable", "Previous Session cleanup was not acknowledged.");
|
|
857
|
+
if (!candidate?.sessionId) candidate = void 0;
|
|
858
|
+
const key = sessionCompatibilityKey(runtime, task);
|
|
859
|
+
const compatible = (record) => this.compatible(record, runtime, task, key);
|
|
860
|
+
if (candidate && !compatible(candidate))
|
|
861
|
+
return intent(
|
|
862
|
+
context.strict ? "unavailable" : "create",
|
|
863
|
+
"Recorded work identity or immutable Session settings differ."
|
|
864
|
+
);
|
|
865
|
+
if (entry3 && compatible(entry3.record)) {
|
|
866
|
+
if (runtime.openSession && entry3.runtime === runtime && entry3.session?.state === "idle" && entry3.session.capabilities.continuation && (!candidate?.sessionId || candidate.sessionId === entry3.session.sessionId))
|
|
867
|
+
return intent("reuse", "Will reuse the matching live Session if still idle.");
|
|
868
|
+
candidate ??= entry3.record;
|
|
869
|
+
}
|
|
870
|
+
if (candidate) {
|
|
871
|
+
if (!runtime.openSession || !candidate.capabilities.load)
|
|
872
|
+
return intent(
|
|
873
|
+
context.strict ? "unavailable" : "create",
|
|
874
|
+
"Recorded Session loading is unsupported."
|
|
875
|
+
);
|
|
876
|
+
return intent(
|
|
877
|
+
"load",
|
|
878
|
+
"Will attempt to load the recorded conversation; restoration is not yet confirmed."
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return intent(
|
|
882
|
+
"create",
|
|
883
|
+
"Will start a new executor conversation; any previous worker must first acknowledge cleanup."
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
async execute(runtime, task, context = {}) {
|
|
707
887
|
if (this.closed) throw new Error("AgentSessionManager is closed");
|
|
708
888
|
if (this.resetting) throw new Error("AgentSessionManager is resetting");
|
|
709
889
|
if (task.signal?.aborted) throw abortError();
|
|
890
|
+
const compatibilityKey = sessionCompatibilityKey(runtime, task);
|
|
891
|
+
const executionId = context.executionId ?? randomUUID();
|
|
892
|
+
const attempt = context.attempt ?? 1;
|
|
710
893
|
let entry3 = this.entries.get(task.agentId);
|
|
711
|
-
if (entry3?.busy || entry3?.session &&
|
|
712
|
-
throw new Error(
|
|
713
|
-
}
|
|
894
|
+
if (entry3?.busy || entry3?.session && !["idle", "closed"].includes(entry3.session.state))
|
|
895
|
+
throw new Error("Agent session is busy: " + task.agentId);
|
|
714
896
|
if (entry3?.closing) await entry3.closing;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
897
|
+
let candidate = context.session;
|
|
898
|
+
let reason;
|
|
899
|
+
const compatible = (record) => this.compatible(record, runtime, task, compatibilityKey);
|
|
900
|
+
const observation = () => ({
|
|
901
|
+
record: {
|
|
902
|
+
bindingId: randomUUID(),
|
|
903
|
+
agentId: task.agentId,
|
|
904
|
+
workId: task.workId,
|
|
905
|
+
runtime: runtime.name,
|
|
906
|
+
compatibilityKey,
|
|
907
|
+
state: "opening",
|
|
908
|
+
capabilities: { continuation: false, load: false, supplement: false },
|
|
909
|
+
action: "created",
|
|
910
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
911
|
+
reason
|
|
912
|
+
},
|
|
913
|
+
executionId,
|
|
914
|
+
attempt
|
|
915
|
+
});
|
|
916
|
+
const rejectTarget = (code, message) => {
|
|
917
|
+
const failure = observation();
|
|
918
|
+
failure.record.state = "unknown";
|
|
919
|
+
failure.record.failure = { code, message };
|
|
920
|
+
this.publish(failure, "load_failed", candidate?.sessionId);
|
|
921
|
+
throw new PermanentError(message);
|
|
922
|
+
};
|
|
923
|
+
if (candidate?.failure?.code === "cleanup_failed")
|
|
924
|
+
rejectTarget(
|
|
925
|
+
"unsupported",
|
|
926
|
+
"Previous Session cleanup was not acknowledged; refusing to start a replacement executor."
|
|
927
|
+
);
|
|
928
|
+
if (!candidate?.sessionId) candidate = void 0;
|
|
929
|
+
if (candidate && !compatible(candidate)) {
|
|
930
|
+
reason = "The recorded Session belongs to different work or has incompatible creation settings.";
|
|
931
|
+
if (context.strict) rejectTarget("incompatible", reason);
|
|
932
|
+
candidate = void 0;
|
|
933
|
+
}
|
|
934
|
+
const requested = candidate?.sessionId;
|
|
935
|
+
if (entry3 && (entry3.runtime !== runtime || !compatible(entry3.record) || entry3.session?.state === "closed" || !entry3.session?.capabilities.continuation || requested && requested !== entry3.session.sessionId || task.resume === "fork" || !runtime.openSession)) {
|
|
936
|
+
reason ??= !compatible(entry3.record) ? "Work identity or immutable Session settings changed." : "The previous live Session cannot continue.";
|
|
937
|
+
if (!candidate && compatible(entry3.record) && entry3.record.failure?.code !== "cleanup_failed")
|
|
938
|
+
candidate = structuredClone(entry3.record);
|
|
939
|
+
await this.closeEntry(entry3);
|
|
940
|
+
this.entries.delete(task.agentId);
|
|
733
941
|
entry3 = void 0;
|
|
734
|
-
if (this.closed || this.resetting) throw abortError();
|
|
942
|
+
if (this.closed || this.resetting || task.signal?.aborted) throw abortError();
|
|
943
|
+
}
|
|
944
|
+
if (!entry3 && candidate && !candidate.capabilities.load) {
|
|
945
|
+
reason = "The recorded provider Session does not support loading.";
|
|
946
|
+
if (context.strict) rejectTarget("unsupported", reason);
|
|
947
|
+
candidate = void 0;
|
|
948
|
+
}
|
|
949
|
+
if (!runtime.openSession) {
|
|
950
|
+
if (candidate) rejectTarget("unsupported", "This runtime cannot load a live Session.");
|
|
951
|
+
const output = await runtime.execute(task);
|
|
952
|
+
if (output.sessionId) {
|
|
953
|
+
const observed = observation();
|
|
954
|
+
observed.record.sessionId = output.sessionId;
|
|
955
|
+
observed.record.state = "unknown";
|
|
956
|
+
observed.record.reason = "The execute-only runtime reported an ID without a managed Session.";
|
|
957
|
+
this.publish(observed, "state_changed");
|
|
958
|
+
}
|
|
959
|
+
return output;
|
|
735
960
|
}
|
|
736
|
-
if (task.signal?.aborted) throw abortError();
|
|
737
|
-
const openSession = runtime.openSession;
|
|
738
|
-
if (!openSession) return runtime.execute(task);
|
|
739
961
|
let detach;
|
|
740
962
|
if (!entry3) {
|
|
741
|
-
const
|
|
742
|
-
const forwardAbort = () => startup.abort(task.signal?.reason);
|
|
743
|
-
task.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
744
|
-
detach = () => task.signal?.removeEventListener("abort", forwardAbort);
|
|
963
|
+
const sessionId = candidate?.sessionId;
|
|
745
964
|
const created = {
|
|
965
|
+
...observation(),
|
|
746
966
|
runtime,
|
|
747
|
-
|
|
748
|
-
startup,
|
|
967
|
+
startup: new AbortController(),
|
|
749
968
|
busy: true,
|
|
750
969
|
onWarning: task.onWarning,
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
970
|
+
opening: Promise.resolve().then(() => open())
|
|
971
|
+
};
|
|
972
|
+
const open = async () => {
|
|
973
|
+
try {
|
|
974
|
+
const session = await runtime.openSession({
|
|
754
975
|
agentId: task.agentId,
|
|
976
|
+
provider: task.provider,
|
|
755
977
|
systemPrompt: task.systemPrompt,
|
|
756
978
|
workDir: task.workDir,
|
|
757
979
|
command: task.command,
|
|
758
980
|
args: task.args,
|
|
759
981
|
model: task.model,
|
|
760
|
-
sessionId
|
|
982
|
+
sessionId,
|
|
761
983
|
resume: task.resume,
|
|
762
984
|
timeout: task.timeout,
|
|
763
985
|
questionForms: Boolean(task.onQuestion),
|
|
764
|
-
signal: startup.signal,
|
|
986
|
+
signal: created.startup.signal,
|
|
765
987
|
onWarning: (message) => created.onWarning?.(message)
|
|
766
|
-
})
|
|
767
|
-
).then((session) => {
|
|
988
|
+
});
|
|
768
989
|
created.session = session;
|
|
990
|
+
created.record.capabilities = { ...session.capabilities };
|
|
991
|
+
if (sessionId && session.sessionId !== sessionId) {
|
|
992
|
+
created.record.failure = {
|
|
993
|
+
code: "load_failed",
|
|
994
|
+
message: "Provider returned a different Session ID; the requested conversation was not restored."
|
|
995
|
+
};
|
|
996
|
+
await this.closeSession(created, session);
|
|
997
|
+
throw new PermanentError(created.record.failure.message);
|
|
998
|
+
}
|
|
999
|
+
created.record.sessionId = session.sessionId;
|
|
1000
|
+
this.publish(created, sessionId ? "loaded" : "created", sessionId);
|
|
769
1001
|
return session;
|
|
770
|
-
})
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
created.record.failure ??= {
|
|
1004
|
+
code: sessionId ? "load_failed" : "opening_failed",
|
|
1005
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1006
|
+
};
|
|
1007
|
+
if (!created.session) created.record.state = "unknown";
|
|
1008
|
+
this.publish(created, sessionId ? "load_failed" : "opening_failed", sessionId);
|
|
1009
|
+
throw error;
|
|
1010
|
+
}
|
|
771
1011
|
};
|
|
1012
|
+
created.record.action = sessionId ? "loaded" : "created";
|
|
1013
|
+
const forwardAbort = () => created.startup.abort(task.signal?.reason);
|
|
1014
|
+
task.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
1015
|
+
detach = () => task.signal?.removeEventListener("abort", forwardAbort);
|
|
1016
|
+
this.entries.set(task.agentId, created);
|
|
1017
|
+
this.publish(created, sessionId ? "load_requested" : "opening", sessionId);
|
|
772
1018
|
entry3 = created;
|
|
773
|
-
this.entries.set(task.agentId, entry3);
|
|
774
1019
|
} else {
|
|
775
1020
|
entry3.busy = true;
|
|
776
1021
|
entry3.onWarning = task.onWarning;
|
|
1022
|
+
entry3.executionId = executionId;
|
|
1023
|
+
entry3.attempt = attempt;
|
|
1024
|
+
entry3.record.action = "reused";
|
|
1025
|
+
entry3.record.failure = void 0;
|
|
1026
|
+
this.publish(entry3, "reused");
|
|
777
1027
|
}
|
|
778
1028
|
try {
|
|
779
1029
|
const session = await entry3.opening;
|
|
@@ -783,8 +1033,9 @@ var AgentSessionManager = class {
|
|
|
783
1033
|
await this.closeEntry(entry3);
|
|
784
1034
|
throw abortError();
|
|
785
1035
|
}
|
|
786
|
-
if (session.state !== "idle") throw new Error(
|
|
787
|
-
|
|
1036
|
+
if (session.state !== "idle") throw new Error("Agent session is busy: " + task.agentId);
|
|
1037
|
+
const prompt = task.workId ? task.prompt + "\n\n## Session continuity\n" + (entry3.record.action === "created" ? "A new executor Session was created; no old conversation was restored. Inspect the workspace before repeating operations." : entry3.record.action === "loaded" ? "The recorded provider conversation was loaded successfully. Inspect current workspace state before continuing." : "The matching live executor conversation is being reused.") + (entry3.record.reason ? "\n" + entry3.record.reason : "") : task.prompt;
|
|
1038
|
+
const turn = session.prompt(prompt, {
|
|
788
1039
|
signal: task.signal,
|
|
789
1040
|
timeout: task.timeout,
|
|
790
1041
|
onEvent: task.onEvent,
|
|
@@ -792,6 +1043,17 @@ var AgentSessionManager = class {
|
|
|
792
1043
|
onPermission: task.onPermission,
|
|
793
1044
|
onQuestion: task.onQuestion
|
|
794
1045
|
});
|
|
1046
|
+
this.publish(entry3, "state_changed");
|
|
1047
|
+
return await turn;
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
if (entry3.session) {
|
|
1050
|
+
entry3.record.failure ??= {
|
|
1051
|
+
code: "turn_failed",
|
|
1052
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1053
|
+
};
|
|
1054
|
+
this.publish(entry3, "state_changed");
|
|
1055
|
+
}
|
|
1056
|
+
throw error;
|
|
795
1057
|
} finally {
|
|
796
1058
|
detach?.();
|
|
797
1059
|
entry3.onWarning = void 0;
|
|
@@ -801,25 +1063,12 @@ var AgentSessionManager = class {
|
|
|
801
1063
|
if (this.entries.get(task.agentId) === entry3) this.entries.delete(task.agentId);
|
|
802
1064
|
} else if (!entry3.session) {
|
|
803
1065
|
if (this.entries.get(task.agentId) === entry3) this.entries.delete(task.agentId);
|
|
804
|
-
}
|
|
1066
|
+
} else this.publish(entry3, "state_changed");
|
|
805
1067
|
} finally {
|
|
806
1068
|
entry3.busy = false;
|
|
807
1069
|
}
|
|
808
1070
|
}
|
|
809
1071
|
}
|
|
810
|
-
/**
|
|
811
|
-
* Whether this manager still holds a session for `agentId`.
|
|
812
|
-
*
|
|
813
|
-
* A caller about to continue a past conversation uses this to avoid naming a
|
|
814
|
-
* session it already has open: passing the id would change the entry's
|
|
815
|
-
* fingerprint, and the manager would close a perfectly good session and
|
|
816
|
-
* `session/load` it straight back.
|
|
817
|
-
*/
|
|
818
|
-
has(agentId) {
|
|
819
|
-
if (this.closed) return false;
|
|
820
|
-
const entry3 = this.entries.get(agentId);
|
|
821
|
-
return entry3 !== void 0 && !entry3.closing && entry3.session?.state !== "closed";
|
|
822
|
-
}
|
|
823
1072
|
/**
|
|
824
1073
|
* Deliver a message into every session that is running a turn right now.
|
|
825
1074
|
*
|
|
@@ -858,6 +1107,16 @@ var AgentSessionManager = class {
|
|
|
858
1107
|
})
|
|
859
1108
|
);
|
|
860
1109
|
}
|
|
1110
|
+
/** Close selected idle conversations without resetting the leader. */
|
|
1111
|
+
async resetAgents(agentIds) {
|
|
1112
|
+
for (const id of agentIds) {
|
|
1113
|
+
const entry3 = this.entries.get(id);
|
|
1114
|
+
if (!entry3) continue;
|
|
1115
|
+
if (entry3.busy) throw new Error("Cannot reset a busy agent: " + id);
|
|
1116
|
+
await this.closeEntry(entry3);
|
|
1117
|
+
if (this.entries.get(id) === entry3) this.entries.delete(id);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
861
1120
|
/** Closes all sessions and permits a fresh conversation after cleanup completes. */
|
|
862
1121
|
reset() {
|
|
863
1122
|
if (this.closing) return this.closing;
|
|
@@ -894,26 +1153,49 @@ var AgentSessionManager = class {
|
|
|
894
1153
|
entry3.closing = (async () => {
|
|
895
1154
|
let session;
|
|
896
1155
|
try {
|
|
897
|
-
session = await entry3.opening;
|
|
1156
|
+
session = entry3.session ?? await entry3.opening;
|
|
898
1157
|
} catch (error) {
|
|
899
|
-
if (
|
|
900
|
-
|
|
1158
|
+
if (entry3.session) session = entry3.session;
|
|
1159
|
+
else if (isAbortError(error)) return;
|
|
1160
|
+
else throw error;
|
|
901
1161
|
}
|
|
902
|
-
await
|
|
1162
|
+
await this.closeSession(entry3, session);
|
|
903
1163
|
})();
|
|
904
1164
|
}
|
|
905
1165
|
return entry3.closing;
|
|
906
1166
|
}
|
|
1167
|
+
async closeSession(entry3, session) {
|
|
1168
|
+
if (entry3.cleanupComplete) return;
|
|
1169
|
+
if (entry3.record.failure?.code === "cleanup_failed")
|
|
1170
|
+
throw new Error(entry3.record.failure.message);
|
|
1171
|
+
entry3.record.state = "closing";
|
|
1172
|
+
this.publish(entry3, "closing");
|
|
1173
|
+
try {
|
|
1174
|
+
await session.close();
|
|
1175
|
+
entry3.cleanupComplete = true;
|
|
1176
|
+
entry3.record.state = "closed";
|
|
1177
|
+
this.publish(entry3, "closed");
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
entry3.record.state = "unknown";
|
|
1180
|
+
entry3.record.failure = {
|
|
1181
|
+
code: "cleanup_failed",
|
|
1182
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1183
|
+
};
|
|
1184
|
+
this.publish(entry3, "cleanup_failed");
|
|
1185
|
+
throw error;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
907
1188
|
};
|
|
908
1189
|
|
|
909
1190
|
// src/orchestrator/Orchestrator.ts
|
|
1191
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
910
1192
|
import * as fs4 from "fs";
|
|
911
|
-
import * as
|
|
1193
|
+
import * as path6 from "path";
|
|
912
1194
|
|
|
913
1195
|
// src/providers/catalog.ts
|
|
914
|
-
import * as
|
|
1196
|
+
import * as path2 from "path";
|
|
915
1197
|
function normalizeCommandKey(command) {
|
|
916
|
-
return
|
|
1198
|
+
return path2.basename(command).toLowerCase().replace(/\.(cmd|exe|bat)$/i, "");
|
|
917
1199
|
}
|
|
918
1200
|
var PROVIDERS = [
|
|
919
1201
|
{
|
|
@@ -1199,14 +1481,25 @@ function asDecision(value) {
|
|
|
1199
1481
|
const message = typeof record.message === "string" ? record.message.trim() : "";
|
|
1200
1482
|
return message ? { mode, message } : void 0;
|
|
1201
1483
|
}
|
|
1202
|
-
if (mode === "run" || mode === void 0) {
|
|
1484
|
+
if (mode === "run" || mode === "continue" || mode === void 0) {
|
|
1203
1485
|
const run = record.run;
|
|
1204
1486
|
if (!Array.isArray(run) || !run.every((id) => typeof id === "string")) return void 0;
|
|
1205
1487
|
const reason = typeof record.reason === "string" ? record.reason.trim() : "";
|
|
1206
1488
|
const requirement = typeof record.requirement === "string" ? record.requirement.trim() : "";
|
|
1207
1489
|
if ("requirement" in record && !requirement) return void 0;
|
|
1490
|
+
const workId = typeof record.workId === "string" ? record.workId.trim() : "";
|
|
1491
|
+
if (mode === "continue" && !workId) return void 0;
|
|
1492
|
+
const tasks = record.tasks;
|
|
1493
|
+
if (tasks !== void 0 && (!Array.isArray(tasks) || !tasks.every(
|
|
1494
|
+
(task) => typeof task === "object" && task !== null && typeof task.agentId === "string" && task.agentId.trim() && typeof task.scope === "string" && task.scope.trim()
|
|
1495
|
+
)))
|
|
1496
|
+
return void 0;
|
|
1497
|
+
if (Array.isArray(tasks) && (new Set(tasks.map((task) => task.agentId)).size !== tasks.length || tasks.some((task) => !run.includes(task.agentId))))
|
|
1498
|
+
return void 0;
|
|
1208
1499
|
return {
|
|
1209
|
-
mode: "run",
|
|
1500
|
+
mode: mode === "continue" ? "continue" : "run",
|
|
1501
|
+
...workId ? { workId } : {},
|
|
1502
|
+
...Array.isArray(tasks) ? { tasks: tasks.map((task) => ({ agentId: task.agentId, scope: task.scope.trim() })) } : {},
|
|
1210
1503
|
run,
|
|
1211
1504
|
...reason ? { reason } : {},
|
|
1212
1505
|
...requirement ? { requirement } : {}
|
|
@@ -1255,32 +1548,61 @@ function renderUpstreamOutputs(outputs) {
|
|
|
1255
1548
|
return block(entries);
|
|
1256
1549
|
}
|
|
1257
1550
|
|
|
1551
|
+
// src/utils/work.ts
|
|
1552
|
+
function isWorkContext(value) {
|
|
1553
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1554
|
+
const work = value;
|
|
1555
|
+
if (typeof work.id !== "string" || !work.id.trim() || typeof work.originalRequirement !== "string" || typeof work.requirement !== "string" || !Array.isArray(work.agents))
|
|
1556
|
+
return false;
|
|
1557
|
+
if (!work.agents.every((item) => {
|
|
1558
|
+
if (typeof item !== "object" || item === null) return false;
|
|
1559
|
+
const agent = item;
|
|
1560
|
+
return typeof agent.agentId === "string" && !!agent.agentId.trim() && ["scope", "plan", "taskTitle"].every(
|
|
1561
|
+
(key) => agent[key] === void 0 || typeof agent[key] === "string"
|
|
1562
|
+
) && (agent.session === void 0 || isSessionRecord(agent.session) && agent.session.agentId === agent.agentId && agent.session.workId === work.id) && (agent.dependsOn === void 0 || Array.isArray(agent.dependsOn) && agent.dependsOn.every((id) => typeof id === "string"));
|
|
1563
|
+
}))
|
|
1564
|
+
return false;
|
|
1565
|
+
if (new Set(work.agents.map((item) => item.agentId)).size !== work.agents.length) return false;
|
|
1566
|
+
if (work.outcome !== void 0) {
|
|
1567
|
+
if (typeof work.outcome !== "object" || work.outcome === null) return false;
|
|
1568
|
+
const outcome = work.outcome;
|
|
1569
|
+
if (!["success", "failed", "cancelled", "interrupted", "unknown"].includes(
|
|
1570
|
+
String(outcome.execution)
|
|
1571
|
+
) || !Array.isArray(outcome.completedAgents) || !outcome.completedAgents.every((id) => typeof id === "string") || !Array.isArray(outcome.failedAgents) || !outcome.failedAgents.every(
|
|
1572
|
+
(item) => item && typeof item.agentId === "string" && (item.reason === void 0 || typeof item.reason === "string")
|
|
1573
|
+
))
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
return true;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1258
1579
|
// src/orchestrator/AgentRunner.ts
|
|
1580
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1259
1581
|
import * as fs3 from "fs";
|
|
1260
|
-
import * as
|
|
1582
|
+
import * as path5 from "path";
|
|
1261
1583
|
|
|
1262
1584
|
// src/runtime/AcpRuntime.ts
|
|
1263
1585
|
import { spawn } from "child_process";
|
|
1264
|
-
import { randomUUID } from "crypto";
|
|
1586
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1265
1587
|
import { Readable, Writable } from "stream";
|
|
1266
1588
|
import { StringDecoder } from "string_decoder";
|
|
1267
1589
|
|
|
1268
1590
|
// src/providers/bridge.ts
|
|
1269
1591
|
import * as fs from "fs";
|
|
1270
1592
|
import { createRequire } from "module";
|
|
1271
|
-
import * as
|
|
1593
|
+
import * as path3 from "path";
|
|
1272
1594
|
var BRIDGE_PACKAGES = {
|
|
1273
1595
|
"claude-agent-acp": "@agentclientprotocol/claude-agent-acp",
|
|
1274
1596
|
"codex-acp": "@agentclientprotocol/codex-acp"
|
|
1275
1597
|
};
|
|
1276
1598
|
function resolvesOnPath(command) {
|
|
1277
|
-
const dirs = (process.env.PATH ?? "").split(
|
|
1599
|
+
const dirs = (process.env.PATH ?? "").split(path3.delimiter).filter(Boolean);
|
|
1278
1600
|
const exts = (process.env.PATHEXT ?? "").split(";").filter(Boolean);
|
|
1279
1601
|
const suffixes = exts.length > 0 ? exts : [""];
|
|
1280
1602
|
for (const dir of dirs) {
|
|
1281
1603
|
for (const ext of suffixes) {
|
|
1282
1604
|
try {
|
|
1283
|
-
fs.accessSync(
|
|
1605
|
+
fs.accessSync(path3.join(dir, command + ext), fs.constants.X_OK);
|
|
1284
1606
|
return true;
|
|
1285
1607
|
} catch {
|
|
1286
1608
|
}
|
|
@@ -1297,7 +1619,7 @@ function resolveBridgeBinary(command) {
|
|
|
1297
1619
|
const bin = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).bin;
|
|
1298
1620
|
const binRel = typeof bin === "string" ? bin : bin?.[command] ?? Object.values(bin)[0];
|
|
1299
1621
|
if (!binRel) return null;
|
|
1300
|
-
return
|
|
1622
|
+
return path3.join(path3.dirname(pkgJsonPath), binRel);
|
|
1301
1623
|
} catch {
|
|
1302
1624
|
return null;
|
|
1303
1625
|
}
|
|
@@ -1307,17 +1629,6 @@ function resolveExecutable(command) {
|
|
|
1307
1629
|
return resolveBridgeBinary(command);
|
|
1308
1630
|
}
|
|
1309
1631
|
|
|
1310
|
-
// src/utils/errors.ts
|
|
1311
|
-
var PermanentError = class extends Error {
|
|
1312
|
-
constructor(message) {
|
|
1313
|
-
super(message);
|
|
1314
|
-
this.name = "PermanentError";
|
|
1315
|
-
}
|
|
1316
|
-
};
|
|
1317
|
-
function isPermanent(err) {
|
|
1318
|
-
return err instanceof PermanentError;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
1632
|
// src/utils/toolSummary.ts
|
|
1322
1633
|
var MAX_SUMMARY_COLUMNS = 120;
|
|
1323
1634
|
function stringifySafe(value) {
|
|
@@ -1582,6 +1893,7 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
1582
1893
|
let state = "idle";
|
|
1583
1894
|
let sessionId = "";
|
|
1584
1895
|
let steeringSupported = false;
|
|
1896
|
+
let loadSupported = false;
|
|
1585
1897
|
let active;
|
|
1586
1898
|
let childClosed = false;
|
|
1587
1899
|
let terminalError;
|
|
@@ -1641,8 +1953,8 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
1641
1953
|
};
|
|
1642
1954
|
const close = () => {
|
|
1643
1955
|
if (closePromise) return closePromise;
|
|
1644
|
-
closePromise = new Promise((
|
|
1645
|
-
closeResolve =
|
|
1956
|
+
closePromise = new Promise((resolve5, reject) => {
|
|
1957
|
+
closeResolve = resolve5;
|
|
1646
1958
|
closeReject = reject;
|
|
1647
1959
|
});
|
|
1648
1960
|
options.signal?.removeEventListener("abort", onSessionAbort);
|
|
@@ -1806,7 +2118,7 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
1806
2118
|
const signal = AbortSignal.any([turn.decisions.signal, context.signal]);
|
|
1807
2119
|
const offered = new Set(params.options.map((option) => option.optionId));
|
|
1808
2120
|
const request = {
|
|
1809
|
-
requestId:
|
|
2121
|
+
requestId: randomUUID2(),
|
|
1810
2122
|
runId: turn.runId,
|
|
1811
2123
|
agentId: options.agentId,
|
|
1812
2124
|
seq: turn.seq++,
|
|
@@ -1819,8 +2131,8 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
1819
2131
|
options: params.options.map(({ optionId, name, kind }) => ({ optionId, name, kind }))
|
|
1820
2132
|
};
|
|
1821
2133
|
let onAbort;
|
|
1822
|
-
const aborted = new Promise((
|
|
1823
|
-
onAbort = () =>
|
|
2134
|
+
const aborted = new Promise((resolve5) => {
|
|
2135
|
+
onAbort = () => resolve5(void 0);
|
|
1824
2136
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1825
2137
|
if (signal.aborted) onAbort();
|
|
1826
2138
|
});
|
|
@@ -1865,7 +2177,7 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
1865
2177
|
const schema = structuredClone(params.requestedSchema);
|
|
1866
2178
|
const signal = AbortSignal.any([turn.decisions.signal, context.signal]);
|
|
1867
2179
|
const request = {
|
|
1868
|
-
requestId:
|
|
2180
|
+
requestId: randomUUID2(),
|
|
1869
2181
|
runId: turn.runId,
|
|
1870
2182
|
agentId: options.agentId,
|
|
1871
2183
|
seq: turn.seq++,
|
|
@@ -1875,8 +2187,8 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
1875
2187
|
requestedSchema: structuredClone(schema)
|
|
1876
2188
|
};
|
|
1877
2189
|
let onAbort;
|
|
1878
|
-
const aborted = new Promise((
|
|
1879
|
-
onAbort = () =>
|
|
2190
|
+
const aborted = new Promise((resolve5) => {
|
|
2191
|
+
onAbort = () => resolve5(void 0);
|
|
1880
2192
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1881
2193
|
if (signal.aborted) onAbort();
|
|
1882
2194
|
});
|
|
@@ -1986,20 +2298,20 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
1986
2298
|
} catch (error) {
|
|
1987
2299
|
return Promise.reject(error);
|
|
1988
2300
|
}
|
|
1989
|
-
let
|
|
2301
|
+
let resolve5;
|
|
1990
2302
|
let reject;
|
|
1991
2303
|
const promise = new Promise((res, rej) => {
|
|
1992
|
-
|
|
2304
|
+
resolve5 = res;
|
|
1993
2305
|
reject = rej;
|
|
1994
2306
|
});
|
|
1995
2307
|
const turn = {
|
|
1996
|
-
runId:
|
|
2308
|
+
runId: randomUUID2(),
|
|
1997
2309
|
seq: 0,
|
|
1998
2310
|
options: promptOptions,
|
|
1999
2311
|
output: "",
|
|
2000
2312
|
decisions: new AbortController(),
|
|
2001
2313
|
promise,
|
|
2002
|
-
resolve:
|
|
2314
|
+
resolve: resolve5,
|
|
2003
2315
|
reject
|
|
2004
2316
|
};
|
|
2005
2317
|
active = turn;
|
|
@@ -2059,7 +2371,7 @@ ${text}` : text
|
|
|
2059
2371
|
return state;
|
|
2060
2372
|
},
|
|
2061
2373
|
get capabilities() {
|
|
2062
|
-
return { continuation: true, supplement: steeringSupported };
|
|
2374
|
+
return { continuation: true, load: loadSupported, supplement: steeringSupported };
|
|
2063
2375
|
},
|
|
2064
2376
|
prompt,
|
|
2065
2377
|
supplement,
|
|
@@ -2078,6 +2390,7 @@ ${text}` : text
|
|
|
2078
2390
|
});
|
|
2079
2391
|
if (terminalError) throw terminalError;
|
|
2080
2392
|
steeringSupported = advertisesSteering(init);
|
|
2393
|
+
loadSupported = init.agentCapabilities?.loadSession === true;
|
|
2081
2394
|
if (options.sessionId) {
|
|
2082
2395
|
if (init.agentCapabilities?.loadSession !== true) {
|
|
2083
2396
|
throw new PermanentError(
|
|
@@ -2178,13 +2491,44 @@ var RuntimeRegistry = class {
|
|
|
2178
2491
|
};
|
|
2179
2492
|
var runtimeRegistry = new RuntimeRegistry();
|
|
2180
2493
|
|
|
2494
|
+
// src/utils/handoff.ts
|
|
2495
|
+
var HANDOFF_INSTRUCTION = [
|
|
2496
|
+
"## Routing handoff",
|
|
2497
|
+
"If this assignment belongs to another role or needs rerouting beyond your scope, explain what remains and end with exactly:",
|
|
2498
|
+
'<multiclaw-handoff>{"reason":"specific reason and remaining work"}</multiclaw-handoff>',
|
|
2499
|
+
"Do not use this for routine implementation choices, progress, or provider errors. Do not rewrite the original requirement. Otherwise finish normally."
|
|
2500
|
+
].join("\n");
|
|
2501
|
+
function parseHandoff(output) {
|
|
2502
|
+
const text = output.trim();
|
|
2503
|
+
const unfenced = text.replace(/```[\s\S]*?```/g, "[quoted example]");
|
|
2504
|
+
const lastLine = unfenced.trim().split("\n").slice(-1)[0] ?? "";
|
|
2505
|
+
const terminal = unfenced.match(/(?:^|\n)<multiclaw-handoff>([\s\S]*?)<\/multiclaw-handoff>$/);
|
|
2506
|
+
const unclosed = /(?:^|\n)<multiclaw-handoff>(?:(?!<\/multiclaw-handoff>)[\s\S])*$/.test(unfenced);
|
|
2507
|
+
if (!terminal && !unclosed && !/^<\/?multiclaw-handoff(?:>|\s)/.test(lastLine.trim()))
|
|
2508
|
+
return void 0;
|
|
2509
|
+
const lines = unfenced.split("\n").filter((line) => /^<multiclaw-handoff(?:>|\s)/.test(line.trim()));
|
|
2510
|
+
if (text.split(String.fromCharCode(96).repeat(3)).length % 2 === 0) return void 0;
|
|
2511
|
+
const match = text.match(/(?:^|\n)<multiclaw-handoff>([\s\S]*?)<\/multiclaw-handoff>$/);
|
|
2512
|
+
if (lines.length !== 1 || !match) return { error: "Invalid terminal handoff marker." };
|
|
2513
|
+
try {
|
|
2514
|
+
const item = JSON.parse(match[1]);
|
|
2515
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Error();
|
|
2516
|
+
const fields = item;
|
|
2517
|
+
if (Object.keys(fields).length !== 1 || typeof fields.reason !== "string" || !fields.reason.trim())
|
|
2518
|
+
throw new Error();
|
|
2519
|
+
return { reason: fields.reason.trim() };
|
|
2520
|
+
} catch {
|
|
2521
|
+
return { error: "Handoff requires a JSON object with a non-empty reason." };
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2181
2525
|
// src/utils/prompt.ts
|
|
2182
2526
|
import * as fs2 from "fs/promises";
|
|
2183
|
-
import * as
|
|
2527
|
+
import * as path4 from "path";
|
|
2184
2528
|
function resolveInWorkspace(workDir, reference) {
|
|
2185
|
-
const root =
|
|
2186
|
-
const resolved =
|
|
2187
|
-
if (resolved !== root && !resolved.startsWith(root +
|
|
2529
|
+
const root = path4.resolve(workDir);
|
|
2530
|
+
const resolved = path4.resolve(root, reference);
|
|
2531
|
+
if (resolved !== root && !resolved.startsWith(root + path4.sep)) return void 0;
|
|
2188
2532
|
return resolved;
|
|
2189
2533
|
}
|
|
2190
2534
|
async function renderPrompt(template, context, workDir) {
|
|
@@ -2213,14 +2557,14 @@ async function renderPrompt(template, context, workDir) {
|
|
|
2213
2557
|
var DEFAULT_RETRY_DELAY_MS = 2e3;
|
|
2214
2558
|
function sleep(ms, signal) {
|
|
2215
2559
|
if (signal?.aborted) return Promise.reject(abortError());
|
|
2216
|
-
return new Promise((
|
|
2560
|
+
return new Promise((resolve5, reject) => {
|
|
2217
2561
|
const onAbort = () => {
|
|
2218
2562
|
clearTimeout(timer);
|
|
2219
2563
|
reject(abortError());
|
|
2220
2564
|
};
|
|
2221
2565
|
const timer = setTimeout(() => {
|
|
2222
2566
|
signal?.removeEventListener("abort", onAbort);
|
|
2223
|
-
|
|
2567
|
+
resolve5();
|
|
2224
2568
|
}, ms);
|
|
2225
2569
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2226
2570
|
});
|
|
@@ -2295,8 +2639,45 @@ var AgentRunner = class {
|
|
|
2295
2639
|
this.config = config;
|
|
2296
2640
|
this.sessionManager = sessionManager;
|
|
2297
2641
|
}
|
|
2642
|
+
sessionTask(agent) {
|
|
2643
|
+
return {
|
|
2644
|
+
agentId: agent.id,
|
|
2645
|
+
workId: agent.id === this.config.leader?.id ? void 0 : this.contextManager.get("workId"),
|
|
2646
|
+
systemPrompt: agent.systemPrompt,
|
|
2647
|
+
prompt: "",
|
|
2648
|
+
workDir: agent.workDir ? path5.resolve(this.globalWorkDir, agent.workDir) : this.globalWorkDir,
|
|
2649
|
+
timeout: agent.timeout ?? 6e5,
|
|
2650
|
+
model: resolveModel(agent, this.config),
|
|
2651
|
+
provider: resolveProvider(agent, this.config),
|
|
2652
|
+
command: resolveCommand(agent, this.config),
|
|
2653
|
+
args: resolveArgs(agent, this.config),
|
|
2654
|
+
onQuestion: this.config.questions?.onRequest
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
previewSession(agent) {
|
|
2658
|
+
const task = this.sessionTask(agent);
|
|
2659
|
+
return (this.sessionManager ?? new AgentSessionManager()).inspect(
|
|
2660
|
+
runtimeRegistry.get(agent.runtime ?? "acp"),
|
|
2661
|
+
task,
|
|
2662
|
+
{ session: this.config.resume?.sessions?.[agent.id], strict: !!this.config.resume?.agents }
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2298
2665
|
async run(agent, signal, instruction) {
|
|
2299
2666
|
const startTime = /* @__PURE__ */ new Date();
|
|
2667
|
+
const executionId = randomUUID3();
|
|
2668
|
+
const manager = this.sessionManager ?? new AgentSessionManager();
|
|
2669
|
+
let session;
|
|
2670
|
+
let finalResult;
|
|
2671
|
+
const offSession = manager.subscribe((event) => {
|
|
2672
|
+
if (event.executionId !== executionId) return;
|
|
2673
|
+
session = event.record;
|
|
2674
|
+
if (!this.sessionManager)
|
|
2675
|
+
this.eventBus.emit({
|
|
2676
|
+
type: "agent:session",
|
|
2677
|
+
timestamp: event.record.observedAt,
|
|
2678
|
+
payload: event
|
|
2679
|
+
});
|
|
2680
|
+
});
|
|
2300
2681
|
const model = resolveModel(agent, this.config);
|
|
2301
2682
|
this.eventBus.emit({
|
|
2302
2683
|
type: "agent:start",
|
|
@@ -2314,7 +2695,7 @@ var AgentRunner = class {
|
|
|
2314
2695
|
let usage;
|
|
2315
2696
|
let costUsd;
|
|
2316
2697
|
try {
|
|
2317
|
-
const workDir = agent.workDir ?
|
|
2698
|
+
const workDir = agent.workDir ? path5.resolve(this.globalWorkDir, agent.workDir) : this.globalWorkDir;
|
|
2318
2699
|
fs3.mkdirSync(workDir, { recursive: true });
|
|
2319
2700
|
const runtime = runtimeRegistry.get(agent.runtime ?? "acp");
|
|
2320
2701
|
const retries = agent.retries ?? 0;
|
|
@@ -2332,8 +2713,8 @@ var AgentRunner = class {
|
|
|
2332
2713
|
// agent, and the no-leader path has no plan at all — so without a
|
|
2333
2714
|
// default the model is handed the literal text `{{agentScope}}`
|
|
2334
2715
|
// and has to guess what it was supposed to say.
|
|
2335
|
-
agentScope: agent.agentScope
|
|
2336
|
-
agentPlan: agent.agentPlan
|
|
2716
|
+
agentScope: agent.agentScope?.trim() || "Fulfil the current requirement within your role: " + (agent.description ?? agent.name),
|
|
2717
|
+
agentPlan: agent.agentPlan?.trim() || "No step-by-step plan was provided \u2014 use your judgement."
|
|
2337
2718
|
};
|
|
2338
2719
|
let prompt = await renderPrompt(agent.taskPrompt, agentContext, this.globalWorkDir);
|
|
2339
2720
|
const requirement = this.contextManager.get("requirement") ?? this.config.context?.requirement;
|
|
@@ -2343,6 +2724,10 @@ ${requirement}
|
|
|
2343
2724
|
|
|
2344
2725
|
${prompt}`;
|
|
2345
2726
|
}
|
|
2727
|
+
if (agent.id !== this.config.leader?.id && agent.id !== this.config.review?.reviewer)
|
|
2728
|
+
prompt = "## Assigned responsibility\n" + agentContext.agentScope + "\n\n## Planning\n" + agentContext.agentPlan + "\n\n" + prompt;
|
|
2729
|
+
if (agent.id !== this.config.leader?.id && agent.id !== this.config.review?.reviewer)
|
|
2730
|
+
prompt += "\n\n" + HANDOFF_INSTRUCTION;
|
|
2346
2731
|
if (instruction) prompt += `
|
|
2347
2732
|
|
|
2348
2733
|
${instruction}`;
|
|
@@ -2357,28 +2742,14 @@ ${instruction}`;
|
|
|
2357
2742
|
`, "tool");
|
|
2358
2743
|
};
|
|
2359
2744
|
const task = {
|
|
2360
|
-
|
|
2361
|
-
systemPrompt: agent.systemPrompt,
|
|
2745
|
+
...this.sessionTask(agent),
|
|
2362
2746
|
prompt,
|
|
2363
2747
|
permissionMode,
|
|
2364
2748
|
onPermission: this.config.permissions?.onRequest,
|
|
2365
2749
|
onQuestion: this.config.questions?.onRequest,
|
|
2366
|
-
workDir,
|
|
2367
|
-
timeout,
|
|
2368
2750
|
signal: runtime.openSession && signal ? AbortSignal.any([innerSignal, signal]) : innerSignal,
|
|
2369
2751
|
model,
|
|
2370
|
-
|
|
2371
|
-
// fresh one. It must stay `undefined` rather than `""` — the ACP
|
|
2372
|
-
// runtime tests `if (task.sessionId)`, so an empty string would
|
|
2373
|
-
// read as "load this session" for a session that does not exist.
|
|
2374
|
-
sessionId: this.config.resume?.sessions?.[agent.id],
|
|
2375
|
-
// Resolve through provider/defaults exactly as the CLI's
|
|
2376
|
-
// availability gate does — an agent that pins `provider` (or
|
|
2377
|
-
// leans on `defaults`) must launch that provider's bridge, not
|
|
2378
|
-
// the runtime's built-in default. Passing the raw `agent.command`
|
|
2379
|
-
// here sent a `provider: "claude"` config straight to `opencode`.
|
|
2380
|
-
command: resolveCommand(agent, this.config),
|
|
2381
|
-
args: resolveArgs(agent, this.config),
|
|
2752
|
+
provider: resolveProvider(agent, this.config),
|
|
2382
2753
|
// A runtime's diagnostics travel on the event bus rather than
|
|
2383
2754
|
// stderr: under the TUI's alternate screen a stderr write is
|
|
2384
2755
|
// never seen. The agent name prefixes the line so it is
|
|
@@ -2413,7 +2784,12 @@ ${instruction}`;
|
|
|
2413
2784
|
}
|
|
2414
2785
|
}
|
|
2415
2786
|
};
|
|
2416
|
-
return await
|
|
2787
|
+
return await manager.execute(runtime, task, {
|
|
2788
|
+
session: this.config.resume?.sessions?.[agent.id],
|
|
2789
|
+
strict: !!this.config.resume?.agents,
|
|
2790
|
+
executionId,
|
|
2791
|
+
attempt
|
|
2792
|
+
});
|
|
2417
2793
|
},
|
|
2418
2794
|
retries,
|
|
2419
2795
|
(attempt, err) => {
|
|
@@ -2446,16 +2822,17 @@ ${instruction}`;
|
|
|
2446
2822
|
output = completed.output;
|
|
2447
2823
|
usage = completed.usage;
|
|
2448
2824
|
costUsd = completed.costUsd;
|
|
2449
|
-
const sessionId = completed.sessionId;
|
|
2450
2825
|
const incomplete = completed.stopReason !== void 0 && completed.stopReason !== "success";
|
|
2451
2826
|
const cancelled = completed.stopReason === "cancelled";
|
|
2452
|
-
const
|
|
2453
|
-
const
|
|
2827
|
+
const handoff = completed.exitCode === 0 && !incomplete && agent.id !== this.config.leader?.id && agent.id !== this.config.review?.reviewer ? parseHandoff(output) : void 0;
|
|
2828
|
+
const failed = completed.exitCode !== 0 || incomplete || !!(handoff && "error" in handoff);
|
|
2829
|
+
const error = handoff && "error" in handoff ? handoff.error : failed ? `Runtime turn did not complete (${completed.stopReason ?? `exit code ${completed.exitCode}`})` : void 0;
|
|
2454
2830
|
const endTime = /* @__PURE__ */ new Date();
|
|
2455
2831
|
const result = {
|
|
2456
2832
|
agentId: agent.id,
|
|
2457
2833
|
agentName: agent.name,
|
|
2458
|
-
status: cancelled ? "cancelled" : failed ? "failed" : "success",
|
|
2834
|
+
status: cancelled ? "cancelled" : failed ? "failed" : handoff ? "handed_back" : "success",
|
|
2835
|
+
...handoff && "reason" in handoff ? { handoffReason: handoff.reason } : {},
|
|
2459
2836
|
...error ? { error } : {},
|
|
2460
2837
|
output,
|
|
2461
2838
|
duration: endTime.getTime() - startTime.getTime(),
|
|
@@ -2465,13 +2842,9 @@ ${instruction}`;
|
|
|
2465
2842
|
model,
|
|
2466
2843
|
...usage ? { usage } : {},
|
|
2467
2844
|
...costUsd !== void 0 ? { costUsd } : {},
|
|
2468
|
-
...
|
|
2845
|
+
...session ? { session } : {}
|
|
2469
2846
|
};
|
|
2470
|
-
|
|
2471
|
-
type: failed ? "agent:failed" : "agent:complete",
|
|
2472
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2473
|
-
payload: result
|
|
2474
|
-
});
|
|
2847
|
+
finalResult = result;
|
|
2475
2848
|
return result;
|
|
2476
2849
|
} catch (err) {
|
|
2477
2850
|
const endTime = /* @__PURE__ */ new Date();
|
|
@@ -2487,14 +2860,29 @@ ${instruction}`;
|
|
|
2487
2860
|
error: err.message,
|
|
2488
2861
|
startTime: startTime.toISOString(),
|
|
2489
2862
|
endTime: endTime.toISOString(),
|
|
2490
|
-
model
|
|
2863
|
+
model,
|
|
2864
|
+
...session ? { session } : {}
|
|
2491
2865
|
};
|
|
2492
|
-
|
|
2493
|
-
type: "agent:failed",
|
|
2494
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2495
|
-
payload: result
|
|
2496
|
-
});
|
|
2866
|
+
finalResult = result;
|
|
2497
2867
|
return result;
|
|
2868
|
+
} finally {
|
|
2869
|
+
try {
|
|
2870
|
+
if (!this.sessionManager) await manager.close();
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
if (finalResult) {
|
|
2873
|
+
finalResult.status = "failed";
|
|
2874
|
+
finalResult.error = (finalResult.error ? finalResult.error + "; " : "") + "Session cleanup failed: " + String(error);
|
|
2875
|
+
}
|
|
2876
|
+
} finally {
|
|
2877
|
+
if (finalResult && session) finalResult.session = session;
|
|
2878
|
+
offSession();
|
|
2879
|
+
if (finalResult)
|
|
2880
|
+
this.eventBus.emit({
|
|
2881
|
+
type: finalResult.status === "success" ? "agent:complete" : finalResult.status === "handed_back" ? "agent:handed-back" : "agent:failed",
|
|
2882
|
+
timestamp: finalResult.endTime,
|
|
2883
|
+
payload: finalResult
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2498
2886
|
}
|
|
2499
2887
|
}
|
|
2500
2888
|
/**
|
|
@@ -2559,10 +2947,51 @@ function buildStages(agents2) {
|
|
|
2559
2947
|
var Orchestrator = class {
|
|
2560
2948
|
constructor(config, sessions) {
|
|
2561
2949
|
this.config = config;
|
|
2950
|
+
this.sessions = sessions;
|
|
2562
2951
|
this.eventBus = new EventBus();
|
|
2563
2952
|
/** Which configured checkpoints have already been triggered this run. */
|
|
2564
2953
|
this.checkpointTriggered = /* @__PURE__ */ new Set();
|
|
2565
|
-
this.
|
|
2954
|
+
this.assignments = /* @__PURE__ */ new Map();
|
|
2955
|
+
this.excluded = /* @__PURE__ */ new Set();
|
|
2956
|
+
this.rerouting = false;
|
|
2957
|
+
this.repairSpent = 0;
|
|
2958
|
+
this.leaderCosts = [];
|
|
2959
|
+
this.cleanupAttempted = false;
|
|
2960
|
+
this.sessionRecords = /* @__PURE__ */ new Map();
|
|
2961
|
+
this.recovery = "";
|
|
2962
|
+
config = {
|
|
2963
|
+
...config,
|
|
2964
|
+
latestWork: isWorkContext(config.latestWork) ? config.latestWork : void 0,
|
|
2965
|
+
resume: config.resume ? { ...config.resume } : void 0
|
|
2966
|
+
};
|
|
2967
|
+
this.config = config;
|
|
2968
|
+
this.ownsSessions = !sessions;
|
|
2969
|
+
this.sessions = sessions ?? new AgentSessionManager();
|
|
2970
|
+
this.offSessions = this.sessions.subscribe((event) => {
|
|
2971
|
+
this.sessionRecords.set(event.record.bindingId, event.record);
|
|
2972
|
+
if (event.record.sessionId && this.work && this.work.id === event.record.workId) {
|
|
2973
|
+
let entry3 = this.work.agents.find((item) => item.agentId === event.record.agentId);
|
|
2974
|
+
if (!entry3) {
|
|
2975
|
+
entry3 = { agentId: event.record.agentId };
|
|
2976
|
+
this.work.agents.push(entry3);
|
|
2977
|
+
}
|
|
2978
|
+
if (!entry3.session || entry3.session.bindingId === event.record.bindingId || event.action === "created" || event.action === "loaded")
|
|
2979
|
+
entry3.session = event.record;
|
|
2980
|
+
}
|
|
2981
|
+
this.eventBus.emit({
|
|
2982
|
+
type: "agent:session",
|
|
2983
|
+
timestamp: event.record.observedAt,
|
|
2984
|
+
payload: event
|
|
2985
|
+
});
|
|
2986
|
+
if (event.record.sessionId && this.work && this.work.id === event.record.workId && this.routing)
|
|
2987
|
+
this.eventBus.emit({
|
|
2988
|
+
type: "orchestration:route",
|
|
2989
|
+
timestamp: event.record.observedAt,
|
|
2990
|
+
payload: { routing: { ...this.routing }, work: structuredClone(this.work) }
|
|
2991
|
+
});
|
|
2992
|
+
});
|
|
2993
|
+
this.originalRequirement = config.context?.requirement ?? "";
|
|
2994
|
+
this.workDir = path6.resolve(config.workDir);
|
|
2566
2995
|
fs4.mkdirSync(this.workDir, { recursive: true });
|
|
2567
2996
|
this.contextManager = new ContextManager(config.context);
|
|
2568
2997
|
this.contextManager.set("conversation", renderConversation(config.history));
|
|
@@ -2572,21 +3001,32 @@ var Orchestrator = class {
|
|
|
2572
3001
|
this.contextManager,
|
|
2573
3002
|
this.workDir,
|
|
2574
3003
|
config,
|
|
2575
|
-
sessions
|
|
3004
|
+
this.sessions
|
|
2576
3005
|
);
|
|
2577
3006
|
}
|
|
2578
3007
|
async preview(options = {}) {
|
|
3008
|
+
try {
|
|
3009
|
+
return await this.previewManaged(options);
|
|
3010
|
+
} finally {
|
|
3011
|
+
try {
|
|
3012
|
+
if (this.ownsSessions) await this.sessions?.close();
|
|
3013
|
+
} finally {
|
|
3014
|
+
this.offSessions();
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
async previewManaged(options = {}) {
|
|
2579
3019
|
if (options.signal?.aborted)
|
|
2580
3020
|
return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
|
|
2581
3021
|
const policyError = this.reviewPolicyError();
|
|
2582
3022
|
if (policyError) throw new Error(policyError);
|
|
2583
3023
|
if (options.signal?.aborted)
|
|
2584
3024
|
return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
|
|
2585
|
-
this.prepareTaskPlan();
|
|
2586
3025
|
const resolution = await this.resolveTurn(options.signal);
|
|
2587
3026
|
if (resolution.kind === "cancelled" || options.signal?.aborted) {
|
|
2588
3027
|
return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
|
|
2589
3028
|
}
|
|
3029
|
+
if (resolution.kind === "error") throw new Error(resolution.message);
|
|
2590
3030
|
if (resolution.kind === "message") {
|
|
2591
3031
|
return {
|
|
2592
3032
|
name: this.config.name,
|
|
@@ -2606,11 +3046,135 @@ var Orchestrator = class {
|
|
|
2606
3046
|
name: this.config.name,
|
|
2607
3047
|
totalAgents: agents2.length + (reviewer2 && agents2.length ? 1 : 0),
|
|
2608
3048
|
stages: stageInfos,
|
|
3049
|
+
sessions: [...agents2, ...reviewer2 && agents2.length ? [reviewer2] : []].map(
|
|
3050
|
+
(agent) => this.runner.previewSession(agent)
|
|
3051
|
+
),
|
|
2609
3052
|
...this.reasonField(resolution.reason),
|
|
2610
|
-
...this.requirementField()
|
|
3053
|
+
...this.requirementField(),
|
|
3054
|
+
originalRequirement: this.originalRequirement,
|
|
3055
|
+
routing: this.routing
|
|
2611
3056
|
};
|
|
2612
3057
|
}
|
|
2613
3058
|
async run(options = {}) {
|
|
3059
|
+
try {
|
|
3060
|
+
return await this.runManaged(options);
|
|
3061
|
+
} finally {
|
|
3062
|
+
try {
|
|
3063
|
+
if (this.ownsSessions && !this.cleanupAttempted) await this.sessions?.close();
|
|
3064
|
+
} finally {
|
|
3065
|
+
this.offSessions();
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
async runManaged(options = {}) {
|
|
3070
|
+
const started = /* @__PURE__ */ new Date();
|
|
3071
|
+
const audit = [];
|
|
3072
|
+
let result;
|
|
3073
|
+
for (; ; ) {
|
|
3074
|
+
this.checkpointTriggered.clear();
|
|
3075
|
+
result = await this.runPass(options);
|
|
3076
|
+
const effective2 = [
|
|
3077
|
+
...new Map(result.agentResults.map((item) => [item.agentId, item])).values()
|
|
3078
|
+
];
|
|
3079
|
+
audit.push(...result.agentResults);
|
|
3080
|
+
const handbacks = effective2.filter((item) => item.status === "handed_back");
|
|
3081
|
+
const failure = effective2.some(
|
|
3082
|
+
(item) => item.status === "failed" || item.status === "cancelled"
|
|
3083
|
+
);
|
|
3084
|
+
if (!handbacks.length || failure || result.cancelled || options.signal?.aborted) break;
|
|
3085
|
+
if (this.rerouting || !this.config.leader || this.config.useLeader === false || this.config.resume?.agents) {
|
|
3086
|
+
this.routeError = this.rerouting ? "Work handed back again; rerouting budget exhausted." : "Work handed back; automatic rerouting is unavailable for this fixed team.";
|
|
3087
|
+
result.success = false;
|
|
3088
|
+
break;
|
|
3089
|
+
}
|
|
3090
|
+
this.rerouting = true;
|
|
3091
|
+
for (const item of handbacks) this.excluded.add(item.agentId);
|
|
3092
|
+
if (this.routing) this.routing.reroutes = 1;
|
|
3093
|
+
this.recovery = "## Rerouting after worker handoff\nPreserve the complete requirement. Check the workspace before repeating completed operations.\n" + JSON.stringify({
|
|
3094
|
+
originalRequirement: this.work?.originalRequirement,
|
|
3095
|
+
requirement: this.contextManager.get("requirement"),
|
|
3096
|
+
handbacks: handbacks.map((item) => ({
|
|
3097
|
+
agentId: item.agentId,
|
|
3098
|
+
handoffReason: item.handoffReason,
|
|
3099
|
+
session: item.session
|
|
3100
|
+
})),
|
|
3101
|
+
completed: effective2.filter((item) => item.status === "success").map((item) => ({ agentId: item.agentId, output: item.output.slice(-4e3) })),
|
|
3102
|
+
assignments: this.work?.agents
|
|
3103
|
+
});
|
|
3104
|
+
this.contextManager.set("rerouting", this.recovery);
|
|
3105
|
+
this.contextManager.set("upstreamOutputs", "");
|
|
3106
|
+
}
|
|
3107
|
+
const effective = [...new Map(result.agentResults.map((item) => [item.agentId, item])).values()];
|
|
3108
|
+
result.effectiveAgentResults = effective;
|
|
3109
|
+
result.agentResults = audit;
|
|
3110
|
+
if (this.rerouting && result.reply)
|
|
3111
|
+
this.routeError = "Work remains incomplete: " + result.reply.message;
|
|
3112
|
+
result.originalRequirement = this.originalRequirement;
|
|
3113
|
+
result.routing = this.routing;
|
|
3114
|
+
if (this.routeError) {
|
|
3115
|
+
result.success = false;
|
|
3116
|
+
result.routing = {
|
|
3117
|
+
mode: this.routing?.mode ?? "run",
|
|
3118
|
+
workId: this.work?.id,
|
|
3119
|
+
reroutes: this.rerouting ? 1 : 0,
|
|
3120
|
+
error: this.routeError
|
|
3121
|
+
};
|
|
3122
|
+
this.eventBus.emit({
|
|
3123
|
+
type: "orchestration:routing-error",
|
|
3124
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3125
|
+
payload: { message: this.routeError }
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
if (this.work) {
|
|
3129
|
+
this.work.requirement = this.contextManager.get("requirement") ?? this.work.requirement;
|
|
3130
|
+
const workers = effective.filter((item) => item.agentId !== this.config.review?.reviewer);
|
|
3131
|
+
this.work.outcome = {
|
|
3132
|
+
execution: result.cancelled ? "cancelled" : !this.routeError && workers.length > 0 && workers.every((item) => item.status === "success") ? "success" : "failed",
|
|
3133
|
+
completedAgents: effective.filter(
|
|
3134
|
+
(item) => item.status === "success" && item.agentId !== this.config.review?.reviewer
|
|
3135
|
+
).map((item) => item.agentId),
|
|
3136
|
+
failedAgents: workers.filter((item) => item.status !== "success").map((item) => ({ agentId: item.agentId, reason: item.error ?? item.handoffReason })),
|
|
3137
|
+
...result.acceptance ? { acceptance: { status: result.acceptance.status, reason: result.acceptance.reason } } : {}
|
|
3138
|
+
};
|
|
3139
|
+
result.work = this.work;
|
|
3140
|
+
}
|
|
3141
|
+
if (this.ownsSessions) {
|
|
3142
|
+
this.cleanupAttempted = true;
|
|
3143
|
+
try {
|
|
3144
|
+
await this.sessions?.close();
|
|
3145
|
+
} catch (error) {
|
|
3146
|
+
result.success = false;
|
|
3147
|
+
result.routing = {
|
|
3148
|
+
mode: this.routing?.mode ?? "run",
|
|
3149
|
+
workId: this.work?.id,
|
|
3150
|
+
reroutes: this.rerouting ? 1 : 0,
|
|
3151
|
+
error: "Session cleanup failed: " + errorMessage2(error)
|
|
3152
|
+
};
|
|
3153
|
+
if (this.work?.outcome) this.work.outcome.execution = "failed";
|
|
3154
|
+
this.eventBus.emit({
|
|
3155
|
+
type: "orchestration:routing-error",
|
|
3156
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3157
|
+
payload: { message: result.routing.error }
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
for (const item of [...result.agentResults, ...result.effectiveAgentResults ?? []]) {
|
|
3161
|
+
const latest = item.session && this.sessionRecords.get(item.session.bindingId);
|
|
3162
|
+
if (latest) item.session = { ...latest, action: item.session.action };
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
const cost = sumCosts([...audit, ...this.leaderCosts]);
|
|
3166
|
+
if (cost !== void 0) result.totalCostUsd = cost;
|
|
3167
|
+
result.startTime = started.toISOString();
|
|
3168
|
+
result.endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
3169
|
+
result.totalDuration = Date.now() - started.getTime();
|
|
3170
|
+
this.eventBus.emit({
|
|
3171
|
+
type: "orchestration:complete",
|
|
3172
|
+
timestamp: result.endTime,
|
|
3173
|
+
payload: result
|
|
3174
|
+
});
|
|
3175
|
+
return result;
|
|
3176
|
+
}
|
|
3177
|
+
async runPass(options) {
|
|
2614
3178
|
const startTime = /* @__PURE__ */ new Date();
|
|
2615
3179
|
if (options.signal?.aborted) return this.finishCancelled(startTime);
|
|
2616
3180
|
const policyError = this.reviewPolicyError();
|
|
@@ -2626,16 +3190,38 @@ var Orchestrator = class {
|
|
|
2626
3190
|
return result2;
|
|
2627
3191
|
}
|
|
2628
3192
|
if (options.signal?.aborted) return this.finishCancelled(startTime);
|
|
2629
|
-
this.prepareTaskPlan();
|
|
2630
3193
|
const resolution = await this.resolveTurn(options.signal);
|
|
2631
3194
|
if (resolution.kind === "cancelled" || options.signal?.aborted) {
|
|
2632
3195
|
return this.finishCancelled(startTime);
|
|
2633
3196
|
}
|
|
3197
|
+
if (resolution.kind === "error") {
|
|
3198
|
+
this.routeError = resolution.message;
|
|
3199
|
+
return this.finishEmpty(startTime, void 0, false);
|
|
3200
|
+
}
|
|
2634
3201
|
if (resolution.kind === "message") {
|
|
2635
3202
|
return this.finishWithMessage(resolution.mode, resolution.message, startTime);
|
|
2636
3203
|
}
|
|
2637
3204
|
const agents2 = resolution.agents;
|
|
2638
|
-
if (this.config.resume)
|
|
3205
|
+
if (this.routing?.mode === "run" && !this.rerouting && !this.config.resume?.agents) {
|
|
3206
|
+
try {
|
|
3207
|
+
await this.sessions?.resetAgents(
|
|
3208
|
+
this.config.agents.filter((agent) => agent.id !== this.config.leader?.id).map((agent) => agent.id)
|
|
3209
|
+
);
|
|
3210
|
+
this.prepareTaskPlan();
|
|
3211
|
+
} catch (error) {
|
|
3212
|
+
this.routeError = "Routing incomplete: could not prepare new work: " + errorMessage2(error);
|
|
3213
|
+
return this.finishEmpty(startTime, void 0, false);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
if (!this.config.resume?.agents && (this.rerouting || this.routing?.mode === "continue") && agents2.some((agent) => this.isPlanner(agent))) {
|
|
3217
|
+
try {
|
|
3218
|
+
fs4.rmSync(path6.join(this.workDir, "task-plan.json"), { force: true });
|
|
3219
|
+
} catch (error) {
|
|
3220
|
+
this.routeError = "Routing incomplete: could not clear the previous plan: " + errorMessage2(error);
|
|
3221
|
+
return this.finishEmpty(startTime, void 0, false);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
if (this.config.resume?.agents && !this.config.latestWork) this.applyResumeTaskPlan(agents2);
|
|
2639
3225
|
if (agents2.length === 0)
|
|
2640
3226
|
return this.finishEmpty(
|
|
2641
3227
|
startTime,
|
|
@@ -2666,6 +3252,7 @@ var Orchestrator = class {
|
|
|
2666
3252
|
let cancelled = false;
|
|
2667
3253
|
let planApplied = false;
|
|
2668
3254
|
const executedIds = /* @__PURE__ */ new Set();
|
|
3255
|
+
let handedBack = false;
|
|
2669
3256
|
for (let i = 0; i < stages.length; i++) {
|
|
2670
3257
|
if (options.signal?.aborted) {
|
|
2671
3258
|
cancelled = true;
|
|
@@ -2692,11 +3279,29 @@ var Orchestrator = class {
|
|
|
2692
3279
|
allSuccess = false;
|
|
2693
3280
|
break;
|
|
2694
3281
|
}
|
|
3282
|
+
this.captureWork(agents2, results);
|
|
3283
|
+
handedBack = stageResults.some((item) => item.status === "handed_back");
|
|
3284
|
+
if (handedBack) {
|
|
3285
|
+
allSuccess = false;
|
|
3286
|
+
this.eventBus.emit({
|
|
3287
|
+
type: "stage:complete",
|
|
3288
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3289
|
+
payload: { stageIndex: i }
|
|
3290
|
+
});
|
|
3291
|
+
for (const pending of stages.slice(i + 1).flat())
|
|
3292
|
+
this.eventBus.emit({
|
|
3293
|
+
type: "agent:skipped",
|
|
3294
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3295
|
+
payload: { agentId: pending.id, agentName: pending.name, icon: pending.icon }
|
|
3296
|
+
});
|
|
3297
|
+
break;
|
|
3298
|
+
}
|
|
2695
3299
|
if (!planApplied && stage.some((a) => this.isPlanner(a)) && stageResults.some(
|
|
2696
3300
|
(r) => r.status === "success" && stage.some((a) => a.id === r.agentId && this.isPlanner(a))
|
|
2697
3301
|
)) {
|
|
2698
3302
|
planApplied = true;
|
|
2699
3303
|
await this.applyTaskPlan(agents2, executedIds);
|
|
3304
|
+
this.captureWork(agents2, results);
|
|
2700
3305
|
const remaining = agents2.filter((a) => !executedIds.has(a.id)).map((a) => ({
|
|
2701
3306
|
...a,
|
|
2702
3307
|
dependsOn: a.dependsOn?.filter((dep) => !executedIds.has(dep))
|
|
@@ -2761,7 +3366,7 @@ var Orchestrator = class {
|
|
|
2761
3366
|
}
|
|
2762
3367
|
}
|
|
2763
3368
|
let acceptance;
|
|
2764
|
-
if (this.config.review) {
|
|
3369
|
+
if (this.config.review && !handedBack) {
|
|
2765
3370
|
acceptance = await this.reviewDelivery(
|
|
2766
3371
|
agents2,
|
|
2767
3372
|
results,
|
|
@@ -2773,6 +3378,7 @@ var Orchestrator = class {
|
|
|
2773
3378
|
cancelled ||= acceptance.status === "cancelled";
|
|
2774
3379
|
allSuccess = allSuccess && acceptance.status === "approved";
|
|
2775
3380
|
}
|
|
3381
|
+
this.captureWork(agents2, results);
|
|
2776
3382
|
const endTime = /* @__PURE__ */ new Date();
|
|
2777
3383
|
const totalCostUsd = sumCosts(results);
|
|
2778
3384
|
const result = {
|
|
@@ -2788,11 +3394,6 @@ var Orchestrator = class {
|
|
|
2788
3394
|
...this.reasonField(resolution.reason),
|
|
2789
3395
|
...this.requirementField()
|
|
2790
3396
|
};
|
|
2791
|
-
this.eventBus.emit({
|
|
2792
|
-
type: "orchestration:complete",
|
|
2793
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2794
|
-
payload: result
|
|
2795
|
-
});
|
|
2796
3397
|
return result;
|
|
2797
3398
|
}
|
|
2798
3399
|
reviewPolicyError() {
|
|
@@ -2820,7 +3421,7 @@ var Orchestrator = class {
|
|
|
2820
3421
|
const acceptance = {
|
|
2821
3422
|
status: "failed",
|
|
2822
3423
|
reviewer: policy.reviewer,
|
|
2823
|
-
repairRounds:
|
|
3424
|
+
repairRounds: this.repairSpent,
|
|
2824
3425
|
reviews: []
|
|
2825
3426
|
};
|
|
2826
3427
|
if (cancelled || signal?.aborted) return { ...acceptance, status: "cancelled" };
|
|
@@ -2866,7 +3467,8 @@ var Orchestrator = class {
|
|
|
2866
3467
|
const result = await this.runner.run(
|
|
2867
3468
|
agent,
|
|
2868
3469
|
signal,
|
|
2869
|
-
`${
|
|
3470
|
+
`${this.recovery}
|
|
3471
|
+
${instruction}
|
|
2870
3472
|
|
|
2871
3473
|
## Latest worker outputs
|
|
2872
3474
|
${this.contextManager.get("upstreamOutputs") ?? ""}`
|
|
@@ -2916,6 +3518,7 @@ Approval requires an empty issues array; changes_required requires at least one
|
|
|
2916
3518
|
return acceptance;
|
|
2917
3519
|
}
|
|
2918
3520
|
acceptance.repairRounds++;
|
|
3521
|
+
this.repairSpent++;
|
|
2919
3522
|
this.warn(
|
|
2920
3523
|
`Delivery requires changes \u2014 repair pass ${acceptance.repairRounds}/${policy.maxRepairRounds ?? 1}.`
|
|
2921
3524
|
);
|
|
@@ -2986,8 +3589,8 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
2986
3589
|
* generic "no plan was provided" fallback.
|
|
2987
3590
|
*/
|
|
2988
3591
|
prepareTaskPlan() {
|
|
2989
|
-
if (this.config.resume) return;
|
|
2990
|
-
fs4.rmSync(
|
|
3592
|
+
if (this.config.resume?.agents) return;
|
|
3593
|
+
fs4.rmSync(path6.join(this.workDir, "task-plan.json"), { force: true });
|
|
2991
3594
|
}
|
|
2992
3595
|
/**
|
|
2993
3596
|
* Decide what this turn is: a pipeline, an answer, or a question.
|
|
@@ -3000,38 +3603,132 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3000
3603
|
async resolveTurn(signal) {
|
|
3001
3604
|
if (signal?.aborted) return { kind: "cancelled" };
|
|
3002
3605
|
if (this.config.resume?.agents) {
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
};
|
|
3606
|
+
const agents2 = this.selectAgents(this.config.resume.agents, "The resume list");
|
|
3607
|
+
this.beginWork(agents2, "continue", this.config.latestWork);
|
|
3608
|
+
return { kind: "run", agents: agents2 };
|
|
3007
3609
|
}
|
|
3008
3610
|
if (!this.config.leader || this.config.useLeader === false) {
|
|
3009
|
-
|
|
3611
|
+
const agents2 = this.configuredAgents();
|
|
3612
|
+
this.beginWork(agents2, "run");
|
|
3613
|
+
return { kind: "run", agents: agents2 };
|
|
3010
3614
|
}
|
|
3615
|
+
const available = this.configuredAgents().filter((agent) => !this.excluded.has(agent.id));
|
|
3011
3616
|
this.contextManager.set(
|
|
3012
3617
|
"availableAgents",
|
|
3013
|
-
|
|
3618
|
+
available.map((agent) => agent.id + ": " + agent.name + " \u2014 " + (agent.description ?? "")).join("\n")
|
|
3619
|
+
);
|
|
3620
|
+
this.contextManager.set(
|
|
3621
|
+
"latestWork",
|
|
3622
|
+
JSON.stringify(this.rerouting ? this.work : this.config.latestWork) ?? "No previous work."
|
|
3014
3623
|
);
|
|
3015
|
-
const
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
if (
|
|
3624
|
+
const leader2 = this.config.leader;
|
|
3625
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3626
|
+
const result = await this.runner.run(
|
|
3627
|
+
leader2,
|
|
3628
|
+
signal,
|
|
3629
|
+
(this.recovery || "") + (attempt ? "\nYour previous routing decision was invalid or unavailable. Return exactly one valid run, continue, reply or ask decision. Do not select unavailable agents." : "")
|
|
3630
|
+
);
|
|
3631
|
+
this.leaderCosts.push(result);
|
|
3632
|
+
if (signal?.aborted || result.status === "cancelled") return { kind: "cancelled" };
|
|
3633
|
+
const decision = result.status === "success" ? parseLeaderDecision(result.output) : void 0;
|
|
3634
|
+
if (decision && (decision.mode === "run" || decision.mode === "continue")) {
|
|
3635
|
+
const known = new Set(available.map((agent) => agent.id));
|
|
3636
|
+
if (!decision.run.length || decision.run.some((id) => !known.has(id)) || new Set(decision.run).size !== decision.run.length) {
|
|
3637
|
+
this.warn("Invalid routing agents: " + decision.run.join(", "));
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
const previous = this.rerouting ? this.work : this.config.latestWork;
|
|
3641
|
+
if (decision.mode === "continue" && (!previous || previous.outcome?.execution === "cancelled" || decision.workId !== previous.id))
|
|
3642
|
+
continue;
|
|
3643
|
+
if (decision.requirement) this.contextManager.set("requirement", decision.requirement);
|
|
3644
|
+
else if (decision.mode === "continue" && previous)
|
|
3645
|
+
this.contextManager.set(
|
|
3646
|
+
"requirement",
|
|
3647
|
+
previous.requirement + "\n\n[Follow-up] " + this.originalRequirement
|
|
3648
|
+
);
|
|
3649
|
+
const agents2 = this.selectAgents(decision.run);
|
|
3650
|
+
this.beginWork(agents2, decision.mode, previous);
|
|
3651
|
+
this.assignments = new Map((decision.tasks ?? []).map((task) => [task.agentId, task.scope]));
|
|
3652
|
+
for (const task of decision.tasks ?? []) {
|
|
3653
|
+
const agent = agents2.find((item) => item.id === task.agentId);
|
|
3654
|
+
if (agent)
|
|
3655
|
+
agent.agentScope = this.assignments.get(agent.id) ?? task.scope ?? agent.agentScope;
|
|
3656
|
+
}
|
|
3657
|
+
this.captureWork(agents2, []);
|
|
3658
|
+
return { kind: "run", agents: agents2, reason: decision.reason };
|
|
3659
|
+
}
|
|
3660
|
+
if (decision && (decision.mode === "reply" || decision.mode === "ask"))
|
|
3024
3661
|
return { kind: "message", mode: decision.mode, message: decision.message };
|
|
3662
|
+
const message = result.output.trim();
|
|
3663
|
+
if (result.status === "success" && message && !/^[{[]|"(?:mode|run|tasks|workId)"\s*:|multiclaw-handoff/.test(message))
|
|
3664
|
+
return { kind: "message", mode: "reply", message };
|
|
3665
|
+
}
|
|
3666
|
+
return {
|
|
3667
|
+
kind: "error",
|
|
3668
|
+
message: "Routing incomplete: the leader did not return a valid decision after one retry."
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
beginWork(agents2, mode, previous) {
|
|
3672
|
+
const continuing = mode === "continue" || this.rerouting || !!this.config.resume?.agents;
|
|
3673
|
+
this.work = continuing && previous ? structuredClone(previous) : {
|
|
3674
|
+
id: randomUUID4(),
|
|
3675
|
+
originalRequirement: this.originalRequirement,
|
|
3676
|
+
requirement: this.contextManager.get("requirement") ?? this.originalRequirement,
|
|
3677
|
+
agents: []
|
|
3678
|
+
};
|
|
3679
|
+
this.routing = {
|
|
3680
|
+
mode: continuing ? "continue" : "run",
|
|
3681
|
+
workId: this.work.id,
|
|
3682
|
+
reroutes: this.rerouting ? 1 : 0
|
|
3683
|
+
};
|
|
3684
|
+
this.contextManager.set("workId", this.work.id);
|
|
3685
|
+
if (continuing) {
|
|
3686
|
+
const sessions = {
|
|
3687
|
+
...this.config.resume?.agents ? this.config.resume.sessions : {}
|
|
3688
|
+
};
|
|
3689
|
+
for (const agent of agents2) {
|
|
3690
|
+
const prior = this.work.agents.find((item) => item.agentId === agent.id);
|
|
3691
|
+
if (prior) {
|
|
3692
|
+
agent.agentScope = prior.scope ?? agent.agentScope;
|
|
3693
|
+
agent.agentPlan = prior.plan ?? agent.agentPlan;
|
|
3694
|
+
agent.taskTitle = prior.taskTitle ?? agent.taskTitle;
|
|
3695
|
+
if (prior.dependsOn)
|
|
3696
|
+
agent.dependsOn = prior.dependsOn.filter((id) => agents2.some((item) => item.id === id));
|
|
3697
|
+
if (prior.session) sessions[agent.id] = prior.session;
|
|
3698
|
+
}
|
|
3025
3699
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3700
|
+
this.config.resume = {
|
|
3701
|
+
...this.config.resume?.agents ? { agents: this.config.resume.agents } : {},
|
|
3702
|
+
sessions
|
|
3703
|
+
};
|
|
3704
|
+
this.recovery = this.recovery || "## Continued work\nInspect the workspace and git state before repeating operations. Previous execution and acceptance facts (partial output is not verified):\n" + JSON.stringify(this.work);
|
|
3705
|
+
} else {
|
|
3706
|
+
this.config.resume = void 0;
|
|
3707
|
+
this.recovery = "";
|
|
3028
3708
|
}
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3709
|
+
}
|
|
3710
|
+
captureWork(agents2, results) {
|
|
3711
|
+
if (!this.work || !this.routing) return;
|
|
3712
|
+
this.work.requirement = this.contextManager.get("requirement") ?? this.work.requirement;
|
|
3713
|
+
for (const agent of agents2) {
|
|
3714
|
+
const result = [...results].reverse().find((item) => item.agentId === agent.id);
|
|
3715
|
+
const old = this.work.agents.find((item) => item.agentId === agent.id);
|
|
3716
|
+
const entry3 = {
|
|
3717
|
+
agentId: agent.id,
|
|
3718
|
+
scope: agent.agentScope,
|
|
3719
|
+
plan: agent.agentPlan,
|
|
3720
|
+
taskTitle: agent.taskTitle,
|
|
3721
|
+
dependsOn: agent.dependsOn ? [...agent.dependsOn] : void 0,
|
|
3722
|
+
session: result?.session?.sessionId || result?.session?.failure?.code === "cleanup_failed" ? result.session : old?.session ?? result?.session
|
|
3723
|
+
};
|
|
3724
|
+
this.work.agents = this.work.agents.filter((item) => item.agentId !== agent.id);
|
|
3725
|
+
this.work.agents.push(entry3);
|
|
3033
3726
|
}
|
|
3034
|
-
|
|
3727
|
+
this.eventBus.emit({
|
|
3728
|
+
type: "orchestration:route",
|
|
3729
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3730
|
+
payload: { routing: { ...this.routing }, work: structuredClone(this.work) }
|
|
3731
|
+
});
|
|
3035
3732
|
}
|
|
3036
3733
|
/**
|
|
3037
3734
|
* The configured team, as copies.
|
|
@@ -3122,7 +3819,7 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3122
3819
|
while (next < agents2.length && !signal?.aborted && !cancelled) {
|
|
3123
3820
|
const index = next;
|
|
3124
3821
|
next += 1;
|
|
3125
|
-
const result = await this.runner.run(agents2[index], signal);
|
|
3822
|
+
const result = await this.runner.run(agents2[index], signal, this.recovery || void 0);
|
|
3126
3823
|
results.set(index, result);
|
|
3127
3824
|
if (result.status === "cancelled") cancelled = true;
|
|
3128
3825
|
}
|
|
@@ -3154,7 +3851,7 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3154
3851
|
* it deliberately left out.
|
|
3155
3852
|
*/
|
|
3156
3853
|
readTaskPlan() {
|
|
3157
|
-
const taskPlanPath =
|
|
3854
|
+
const taskPlanPath = path6.join(this.workDir, "task-plan.json");
|
|
3158
3855
|
if (!fs4.existsSync(taskPlanPath)) return void 0;
|
|
3159
3856
|
let taskPlan;
|
|
3160
3857
|
try {
|
|
@@ -3210,8 +3907,8 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3210
3907
|
}
|
|
3211
3908
|
if (executedIds.has(agent.id)) continue;
|
|
3212
3909
|
agent.taskTitle = task.title;
|
|
3213
|
-
agent.dependsOn = dependsOn;
|
|
3214
|
-
agent.agentScope = task.scope;
|
|
3910
|
+
agent.dependsOn = dependsOn ?? agent.dependsOn;
|
|
3911
|
+
agent.agentScope = this.assignments.get(agent.id) ?? task.scope ?? agent.agentScope;
|
|
3215
3912
|
if (task.plan?.length) {
|
|
3216
3913
|
agent.agentPlan = task.plan.map((s, i) => `${i + 1}. ${s}`).join("\n");
|
|
3217
3914
|
}
|
|
@@ -3233,7 +3930,7 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3233
3930
|
const task = taskMap.get(agent.id);
|
|
3234
3931
|
if (!task) continue;
|
|
3235
3932
|
agent.taskTitle = task.title;
|
|
3236
|
-
agent.agentScope = task.scope;
|
|
3933
|
+
agent.agentScope = this.assignments.get(agent.id) ?? task.scope ?? agent.agentScope;
|
|
3237
3934
|
if (task.plan?.length) {
|
|
3238
3935
|
agent.agentPlan = task.plan.map((s, i) => `${i + 1}. ${s}`).join("\n");
|
|
3239
3936
|
}
|
|
@@ -3278,11 +3975,6 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3278
3975
|
reply: { mode, message },
|
|
3279
3976
|
...this.requirementField()
|
|
3280
3977
|
};
|
|
3281
|
-
this.eventBus.emit({
|
|
3282
|
-
type: "orchestration:complete",
|
|
3283
|
-
timestamp: endTime.toISOString(),
|
|
3284
|
-
payload: result
|
|
3285
|
-
});
|
|
3286
3978
|
return result;
|
|
3287
3979
|
}
|
|
3288
3980
|
warn(message) {
|
|
@@ -3304,11 +3996,6 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3304
3996
|
endTime: endTime.toISOString(),
|
|
3305
3997
|
...this.requirementField()
|
|
3306
3998
|
};
|
|
3307
|
-
this.eventBus.emit({
|
|
3308
|
-
type: "orchestration:complete",
|
|
3309
|
-
timestamp: endTime.toISOString(),
|
|
3310
|
-
payload: result
|
|
3311
|
-
});
|
|
3312
3999
|
return result;
|
|
3313
4000
|
}
|
|
3314
4001
|
/**
|
|
@@ -3335,11 +4022,6 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3335
4022
|
endTime: endTime.toISOString(),
|
|
3336
4023
|
...this.requirementField()
|
|
3337
4024
|
};
|
|
3338
|
-
this.eventBus.emit({
|
|
3339
|
-
type: "orchestration:complete",
|
|
3340
|
-
timestamp: endTime.toISOString(),
|
|
3341
|
-
payload: result
|
|
3342
|
-
});
|
|
3343
4025
|
return result;
|
|
3344
4026
|
}
|
|
3345
4027
|
};
|
|
@@ -3394,24 +4076,24 @@ function parseVersion(raw) {
|
|
|
3394
4076
|
}
|
|
3395
4077
|
var SGR_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
3396
4078
|
async function versionFor(command) {
|
|
3397
|
-
return new Promise((
|
|
4079
|
+
return new Promise((resolve5) => {
|
|
3398
4080
|
let out = "";
|
|
3399
4081
|
const child = spawn2(command, ["--version"], { env: process.env });
|
|
3400
4082
|
child.stdout.on("data", (d) => {
|
|
3401
4083
|
out += d.toString();
|
|
3402
4084
|
});
|
|
3403
4085
|
child.on("close", () => {
|
|
3404
|
-
|
|
4086
|
+
resolve5(out.trim() || void 0);
|
|
3405
4087
|
});
|
|
3406
4088
|
child.on("error", () => {
|
|
3407
|
-
|
|
4089
|
+
resolve5(void 0);
|
|
3408
4090
|
});
|
|
3409
4091
|
setTimeout(() => {
|
|
3410
4092
|
try {
|
|
3411
4093
|
child.kill("SIGKILL");
|
|
3412
4094
|
} catch {
|
|
3413
4095
|
}
|
|
3414
|
-
|
|
4096
|
+
resolve5(void 0);
|
|
3415
4097
|
}, 3e3);
|
|
3416
4098
|
});
|
|
3417
4099
|
}
|
|
@@ -3480,7 +4162,7 @@ function describeResumePlan({ agents: agents2, sessions = {}, team }) {
|
|
|
3480
4162
|
const teamById = new Map(team.map((member) => [member.id, member]));
|
|
3481
4163
|
const running = new Set(agents2);
|
|
3482
4164
|
const clauses = agents2.map(
|
|
3483
|
-
(id) => sessions[id] ? `${id}
|
|
4165
|
+
(id) => sessions[id]?.sessionId ? `${id} will attempt to resume its session` : `${id} starts fresh`
|
|
3484
4166
|
);
|
|
3485
4167
|
for (const id of agents2) {
|
|
3486
4168
|
const dropped = (teamById.get(id)?.dependsOn ?? []).filter(
|
|
@@ -3504,8 +4186,12 @@ export {
|
|
|
3504
4186
|
buildStages,
|
|
3505
4187
|
defineConfig,
|
|
3506
4188
|
describeResumePlan,
|
|
4189
|
+
describeSession,
|
|
3507
4190
|
detectProviders,
|
|
3508
4191
|
isAbortError,
|
|
4192
|
+
isSessionEvent,
|
|
4193
|
+
isSessionRecord,
|
|
4194
|
+
isWorkContext,
|
|
3509
4195
|
presentProviders,
|
|
3510
4196
|
providerCommands,
|
|
3511
4197
|
providerIds,
|
|
@@ -3519,6 +4205,7 @@ export {
|
|
|
3519
4205
|
resolveProvider,
|
|
3520
4206
|
resolvesOnPath,
|
|
3521
4207
|
runtimeRegistry,
|
|
4208
|
+
sessionCompatibilityKey,
|
|
3522
4209
|
specForCommand,
|
|
3523
4210
|
specForProvider,
|
|
3524
4211
|
tierModel,
|