@multiclawcli/core 0.7.0 → 0.8.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.js
CHANGED
|
@@ -2054,6 +2054,19 @@ var AgentSessionManager = class {
|
|
|
2054
2054
|
}
|
|
2055
2055
|
}
|
|
2056
2056
|
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Whether this manager still holds a session for `agentId`.
|
|
2059
|
+
*
|
|
2060
|
+
* A caller about to continue a past conversation uses this to avoid naming a
|
|
2061
|
+
* session it already has open: passing the id would change the entry's
|
|
2062
|
+
* fingerprint, and the manager would close a perfectly good session and
|
|
2063
|
+
* `session/load` it straight back.
|
|
2064
|
+
*/
|
|
2065
|
+
has(agentId) {
|
|
2066
|
+
if (this.closed) return false;
|
|
2067
|
+
const entry3 = this.entries.get(agentId);
|
|
2068
|
+
return entry3 !== void 0 && !entry3.closing && entry3.session?.state !== "closed";
|
|
2069
|
+
}
|
|
2057
2070
|
/**
|
|
2058
2071
|
* Deliver a message into every session that is running a turn right now.
|
|
2059
2072
|
*
|
|
@@ -2735,7 +2748,7 @@ var Orchestrator = class {
|
|
|
2735
2748
|
}
|
|
2736
2749
|
if (options.signal?.aborted)
|
|
2737
2750
|
return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
|
|
2738
|
-
this.
|
|
2751
|
+
this.prepareTaskPlan();
|
|
2739
2752
|
const resolution = await this.resolveTurn(options.signal);
|
|
2740
2753
|
if (resolution.kind === "cancelled" || options.signal?.aborted) {
|
|
2741
2754
|
return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
|
|
@@ -2786,7 +2799,7 @@ var Orchestrator = class {
|
|
|
2786
2799
|
return this.finishEmpty(startTime, void 0, false);
|
|
2787
2800
|
}
|
|
2788
2801
|
if (options.signal?.aborted) return this.finishCancelled(startTime);
|
|
2789
|
-
this.
|
|
2802
|
+
this.prepareTaskPlan();
|
|
2790
2803
|
const resolution = await this.resolveTurn(options.signal);
|
|
2791
2804
|
if (resolution.kind === "cancelled" || options.signal?.aborted) {
|
|
2792
2805
|
return this.finishCancelled(startTime);
|
|
@@ -2795,6 +2808,7 @@ var Orchestrator = class {
|
|
|
2795
2808
|
return this.finishWithMessage(resolution.mode, resolution.message, startTime);
|
|
2796
2809
|
}
|
|
2797
2810
|
const agents2 = resolution.agents;
|
|
2811
|
+
if (this.config.resume) this.applyResumeTaskPlan(agents2);
|
|
2798
2812
|
if (agents2.length === 0)
|
|
2799
2813
|
return this.finishEmpty(
|
|
2800
2814
|
startTime,
|
|
@@ -3093,14 +3107,20 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3093
3107
|
}
|
|
3094
3108
|
}
|
|
3095
3109
|
/**
|
|
3096
|
-
*
|
|
3110
|
+
* Decide what to do with last run's task plan before this turn starts.
|
|
3097
3111
|
*
|
|
3098
3112
|
* The console runs every turn against the same workspace, so the plan file
|
|
3099
3113
|
* survives between turns. If a turn's architect fails to write a new one,
|
|
3100
3114
|
* `applyTaskPlan` would silently re-apply the previous turn's titles, scopes
|
|
3101
3115
|
* and dependencies to a completely different requirement.
|
|
3116
|
+
*
|
|
3117
|
+
* A resumed turn is the exception: the plan in the workspace describes the
|
|
3118
|
+
* task being continued, and it is the only record of what each resumed agent
|
|
3119
|
+
* was asked to do. Deleting it would leave every retried agent with the
|
|
3120
|
+
* generic "no plan was provided" fallback.
|
|
3102
3121
|
*/
|
|
3103
|
-
|
|
3122
|
+
prepareTaskPlan() {
|
|
3123
|
+
if (this.config.resume) return;
|
|
3104
3124
|
fs4.rmSync(path5.join(this.workDir, "task-plan.json"), { force: true });
|
|
3105
3125
|
}
|
|
3106
3126
|
/**
|
|
@@ -3113,7 +3133,7 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3113
3133
|
*/
|
|
3114
3134
|
async resolveTurn(signal) {
|
|
3115
3135
|
if (signal?.aborted) return { kind: "cancelled" };
|
|
3116
|
-
if (this.config.resume) {
|
|
3136
|
+
if (this.config.resume?.agents) {
|
|
3117
3137
|
return {
|
|
3118
3138
|
kind: "run",
|
|
3119
3139
|
agents: this.selectAgents(this.config.resume.agents, "The resume list")
|
|
@@ -3153,7 +3173,7 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3153
3173
|
* `applyTaskPlan` writes `taskTitle` / `dependsOn` / `agentScope` onto whatever
|
|
3154
3174
|
* objects it is handed. The console reuses a single config across turns, so
|
|
3155
3175
|
* returning the config's own objects would leak turn 1's dependency graph into
|
|
3156
|
-
* turn 2 — the exact cross-turn contamination `
|
|
3176
|
+
* turn 2 — the exact cross-turn contamination `prepareTaskPlan` exists to
|
|
3157
3177
|
* prevent, but for the in-memory fields rather than the file.
|
|
3158
3178
|
*/
|
|
3159
3179
|
configuredAgents() {
|
|
@@ -3230,9 +3250,21 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3230
3250
|
isPlanner(agent) {
|
|
3231
3251
|
return agent.role === "planner" || agent.id === "architect";
|
|
3232
3252
|
}
|
|
3233
|
-
|
|
3253
|
+
/**
|
|
3254
|
+
* Read and validate `task-plan.json`, one entry per agent id.
|
|
3255
|
+
*
|
|
3256
|
+
* `undefined` means "there is no usable plan" — no file, unparseable JSON, or
|
|
3257
|
+
* no `tasks` array. Each of those is reported rather than treated as an empty
|
|
3258
|
+
* plan: a broken plan must not be indistinguishable from a missing one.
|
|
3259
|
+
*
|
|
3260
|
+
* Validation stops at "is this entry well-formed and unique". Whether an id
|
|
3261
|
+
* belongs to the run is the caller's question, because a normal turn and a
|
|
3262
|
+
* resume answer it differently — a resume must not warn about every teammate
|
|
3263
|
+
* it deliberately left out.
|
|
3264
|
+
*/
|
|
3265
|
+
readTaskPlan() {
|
|
3234
3266
|
const taskPlanPath = path5.join(this.workDir, "task-plan.json");
|
|
3235
|
-
if (!fs4.existsSync(taskPlanPath)) return;
|
|
3267
|
+
if (!fs4.existsSync(taskPlanPath)) return void 0;
|
|
3236
3268
|
let taskPlan;
|
|
3237
3269
|
try {
|
|
3238
3270
|
taskPlan = JSON.parse(fs4.readFileSync(taskPlanPath, "utf-8"));
|
|
@@ -3240,14 +3272,13 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3240
3272
|
this.warn(
|
|
3241
3273
|
`Could not parse task-plan.json \u2014 continuing with the original agent set: ${errorMessage2(err)}`
|
|
3242
3274
|
);
|
|
3243
|
-
return;
|
|
3275
|
+
return void 0;
|
|
3244
3276
|
}
|
|
3245
3277
|
if (typeof taskPlan !== "object" || taskPlan === null || !("tasks" in taskPlan) || !Array.isArray(taskPlan.tasks)) {
|
|
3246
3278
|
this.warn("task-plan.json has no `tasks` array \u2014 ignoring it.");
|
|
3247
|
-
return;
|
|
3279
|
+
return void 0;
|
|
3248
3280
|
}
|
|
3249
3281
|
const taskMap = /* @__PURE__ */ new Map();
|
|
3250
|
-
const selected = new Set(agents2.map((a) => a.id));
|
|
3251
3282
|
for (const [index, entry3] of taskPlan.tasks.entries()) {
|
|
3252
3283
|
if (typeof entry3 !== "object" || entry3 === null || Array.isArray(entry3)) {
|
|
3253
3284
|
this.warn(`task-plan.json contains a malformed task at index ${index} \u2014 ignoring it.`);
|
|
@@ -3258,18 +3289,23 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3258
3289
|
this.warn(`task-plan.json contains a malformed task at index ${index} \u2014 ignoring it.`);
|
|
3259
3290
|
continue;
|
|
3260
3291
|
}
|
|
3261
|
-
if (!selected.has(task.id)) {
|
|
3262
|
-
this.warn(
|
|
3263
|
-
`The task plan names "${task.id}", which is not in this run \u2014 ignoring that task.`
|
|
3264
|
-
);
|
|
3265
|
-
continue;
|
|
3266
|
-
}
|
|
3267
3292
|
if (taskMap.has(task.id)) {
|
|
3268
3293
|
this.warn(`The task plan contains a duplicate task for "${task.id}" \u2014 ignoring that task.`);
|
|
3269
3294
|
continue;
|
|
3270
3295
|
}
|
|
3271
3296
|
taskMap.set(task.id, task);
|
|
3272
3297
|
}
|
|
3298
|
+
return taskMap;
|
|
3299
|
+
}
|
|
3300
|
+
async applyTaskPlan(agents2, executedIds) {
|
|
3301
|
+
const taskMap = this.readTaskPlan();
|
|
3302
|
+
if (!taskMap) return;
|
|
3303
|
+
const selected = new Set(agents2.map((a) => a.id));
|
|
3304
|
+
for (const id of taskMap.keys()) {
|
|
3305
|
+
if (!selected.has(id)) {
|
|
3306
|
+
this.warn(`The task plan names "${id}", which is not in this run \u2014 ignoring that task.`);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3273
3309
|
for (const agent of agents2) {
|
|
3274
3310
|
const task = taskMap.get(agent.id);
|
|
3275
3311
|
if (task) {
|
|
@@ -3291,6 +3327,27 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3291
3327
|
}
|
|
3292
3328
|
}
|
|
3293
3329
|
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Hand a resumed agent the plan entry it was originally given.
|
|
3332
|
+
*
|
|
3333
|
+
* `dependsOn` is deliberately not applied: `selectAgents` has already pruned
|
|
3334
|
+
* the stage graph for this run, and re-applying the plan's dependencies would
|
|
3335
|
+
* undo that pruning — including the one that lets a single-agent retry run
|
|
3336
|
+
* without the teammate it depends on.
|
|
3337
|
+
*/
|
|
3338
|
+
applyResumeTaskPlan(agents2) {
|
|
3339
|
+
const taskMap = this.readTaskPlan();
|
|
3340
|
+
if (!taskMap) return;
|
|
3341
|
+
for (const agent of agents2) {
|
|
3342
|
+
const task = taskMap.get(agent.id);
|
|
3343
|
+
if (!task) continue;
|
|
3344
|
+
agent.taskTitle = task.title;
|
|
3345
|
+
agent.agentScope = task.scope;
|
|
3346
|
+
if (task.plan?.length) {
|
|
3347
|
+
agent.agentPlan = task.plan.map((s, i) => `${i + 1}. ${s}`).join("\n");
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3294
3351
|
/**
|
|
3295
3352
|
* The `requirement` field for a result, present only when the caller set one.
|
|
3296
3353
|
*
|
|
@@ -16,6 +16,15 @@ export declare class AgentSessionManager {
|
|
|
16
16
|
private closing?;
|
|
17
17
|
private closed;
|
|
18
18
|
execute(runtime: AgentRuntime, task: AgentTask): Promise<AgentOutput>;
|
|
19
|
+
/**
|
|
20
|
+
* Whether this manager still holds a session for `agentId`.
|
|
21
|
+
*
|
|
22
|
+
* A caller about to continue a past conversation uses this to avoid naming a
|
|
23
|
+
* session it already has open: passing the id would change the entry's
|
|
24
|
+
* fingerprint, and the manager would close a perfectly good session and
|
|
25
|
+
* `session/load` it straight back.
|
|
26
|
+
*/
|
|
27
|
+
has(agentId: string): boolean;
|
|
19
28
|
/**
|
|
20
29
|
* Deliver a message into every session that is running a turn right now.
|
|
21
30
|
*
|
|
@@ -28,14 +28,19 @@ export declare class Orchestrator {
|
|
|
28
28
|
*/
|
|
29
29
|
private buildStagesOrFallback;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Decide what to do with last run's task plan before this turn starts.
|
|
32
32
|
*
|
|
33
33
|
* The console runs every turn against the same workspace, so the plan file
|
|
34
34
|
* survives between turns. If a turn's architect fails to write a new one,
|
|
35
35
|
* `applyTaskPlan` would silently re-apply the previous turn's titles, scopes
|
|
36
36
|
* and dependencies to a completely different requirement.
|
|
37
|
+
*
|
|
38
|
+
* A resumed turn is the exception: the plan in the workspace describes the
|
|
39
|
+
* task being continued, and it is the only record of what each resumed agent
|
|
40
|
+
* was asked to do. Deleting it would leave every retried agent with the
|
|
41
|
+
* generic "no plan was provided" fallback.
|
|
37
42
|
*/
|
|
38
|
-
private
|
|
43
|
+
private prepareTaskPlan;
|
|
39
44
|
/**
|
|
40
45
|
* Decide what this turn is: a pipeline, an answer, or a question.
|
|
41
46
|
*
|
|
@@ -51,7 +56,7 @@ export declare class Orchestrator {
|
|
|
51
56
|
* `applyTaskPlan` writes `taskTitle` / `dependsOn` / `agentScope` onto whatever
|
|
52
57
|
* objects it is handed. The console reuses a single config across turns, so
|
|
53
58
|
* returning the config's own objects would leak turn 1's dependency graph into
|
|
54
|
-
* turn 2 — the exact cross-turn contamination `
|
|
59
|
+
* turn 2 — the exact cross-turn contamination `prepareTaskPlan` exists to
|
|
55
60
|
* prevent, but for the in-memory fields rather than the file.
|
|
56
61
|
*/
|
|
57
62
|
private configuredAgents;
|
|
@@ -76,7 +81,29 @@ export declare class Orchestrator {
|
|
|
76
81
|
* before `role` existed keeps working unchanged.
|
|
77
82
|
*/
|
|
78
83
|
private isPlanner;
|
|
84
|
+
/**
|
|
85
|
+
* Read and validate `task-plan.json`, one entry per agent id.
|
|
86
|
+
*
|
|
87
|
+
* `undefined` means "there is no usable plan" — no file, unparseable JSON, or
|
|
88
|
+
* no `tasks` array. Each of those is reported rather than treated as an empty
|
|
89
|
+
* plan: a broken plan must not be indistinguishable from a missing one.
|
|
90
|
+
*
|
|
91
|
+
* Validation stops at "is this entry well-formed and unique". Whether an id
|
|
92
|
+
* belongs to the run is the caller's question, because a normal turn and a
|
|
93
|
+
* resume answer it differently — a resume must not warn about every teammate
|
|
94
|
+
* it deliberately left out.
|
|
95
|
+
*/
|
|
96
|
+
private readTaskPlan;
|
|
79
97
|
private applyTaskPlan;
|
|
98
|
+
/**
|
|
99
|
+
* Hand a resumed agent the plan entry it was originally given.
|
|
100
|
+
*
|
|
101
|
+
* `dependsOn` is deliberately not applied: `selectAgents` has already pruned
|
|
102
|
+
* the stage graph for this run, and re-applying the plan's dependencies would
|
|
103
|
+
* undo that pruning — including the one that lets a single-agent retry run
|
|
104
|
+
* without the teammate it depends on.
|
|
105
|
+
*/
|
|
106
|
+
private applyResumeTaskPlan;
|
|
80
107
|
/**
|
|
81
108
|
* The `requirement` field for a result, present only when the caller set one.
|
|
82
109
|
*
|
package/dist/types/config.d.ts
CHANGED
|
@@ -66,17 +66,26 @@ export interface MultiClawConfig {
|
|
|
66
66
|
*/
|
|
67
67
|
history?: TurnRecord[];
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
69
|
+
* Continue known agent sessions, and optionally re-run a fixed set of agents
|
|
70
|
+
* instead of letting the leader route.
|
|
71
71
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
72
|
+
* Two shapes, and the difference is who decides what runs:
|
|
73
|
+
*
|
|
74
|
+
* - `agents` set — the CLI's `--resume`, the console's `/retry`: exactly those
|
|
75
|
+
* agents run, in that order. A `dependsOn` outside the set is pruned, which
|
|
76
|
+
* is what lets a single-agent retry run without its absent dependency.
|
|
77
|
+
* - `agents` absent — the console continuing an earlier session: the turn
|
|
78
|
+
* routes as usual (leader, or the whole team) and the sessions are there for
|
|
79
|
+
* whichever agents the route happens to pick. This is what makes a new
|
|
80
|
+
* instruction land in a conversation the agents already remember.
|
|
81
|
+
*
|
|
82
|
+
* Like `history`, it is per-run state the caller carries: core has no session
|
|
83
|
+
* of its own, and a config that never resumes leaves it unset and behaves
|
|
84
|
+
* exactly as before.
|
|
76
85
|
*/
|
|
77
86
|
resume?: {
|
|
78
87
|
/** Agent ids to run, in the order they were originally selected. */
|
|
79
|
-
agents
|
|
88
|
+
agents?: string[];
|
|
80
89
|
/** agentId → session id, for agents whose session is worth continuing. */
|
|
81
90
|
sessions?: Record<string, string>;
|
|
82
91
|
};
|