@multiclawcli/core 0.6.3 → 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
|
@@ -241,6 +241,36 @@ function isPermanent(err) {
|
|
|
241
241
|
return err instanceof PermanentError;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
// src/utils/toolSummary.ts
|
|
245
|
+
var MAX_SUMMARY_COLUMNS = 120;
|
|
246
|
+
function stringifySafe(value) {
|
|
247
|
+
if (typeof value === "string") return value;
|
|
248
|
+
if (value === void 0 || value === null) return "";
|
|
249
|
+
try {
|
|
250
|
+
return JSON.stringify(value) ?? "";
|
|
251
|
+
} catch {
|
|
252
|
+
return String(value);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function firstLineTruncated(value) {
|
|
256
|
+
const text = stringifySafe(value);
|
|
257
|
+
const newline = text.indexOf("\n");
|
|
258
|
+
const firstLine = newline === -1 ? text : text.slice(0, newline);
|
|
259
|
+
if (firstLine.length <= MAX_SUMMARY_COLUMNS) return firstLine;
|
|
260
|
+
return `${firstLine.slice(0, MAX_SUMMARY_COLUMNS - 1)}\u2026`;
|
|
261
|
+
}
|
|
262
|
+
function hasToolArgs(input) {
|
|
263
|
+
if (input === void 0 || input === null) return false;
|
|
264
|
+
if (typeof input !== "object" || Array.isArray(input)) return true;
|
|
265
|
+
return Object.keys(input).length > 0;
|
|
266
|
+
}
|
|
267
|
+
function summarizeToolInput(input) {
|
|
268
|
+
return hasToolArgs(input) ? firstLineTruncated(input) : "";
|
|
269
|
+
}
|
|
270
|
+
function summarizeToolOutput(output) {
|
|
271
|
+
return firstLineTruncated(output);
|
|
272
|
+
}
|
|
273
|
+
|
|
244
274
|
// src/runtime/RuntimeBase.ts
|
|
245
275
|
var RuntimeBase = class {
|
|
246
276
|
};
|
|
@@ -305,16 +335,26 @@ var KIND_TO_TOOLS = {
|
|
|
305
335
|
search: ["Grep", "Glob"],
|
|
306
336
|
fetch: ["WebFetch"]
|
|
307
337
|
};
|
|
338
|
+
function matchesTool(pattern, name) {
|
|
339
|
+
if (!pattern.includes("*")) return pattern === name;
|
|
340
|
+
const source = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
341
|
+
return new RegExp(`^${source}$`).test(name);
|
|
342
|
+
}
|
|
308
343
|
function toolPermitted(params, tools) {
|
|
309
|
-
const allowed =
|
|
344
|
+
const allowed = tools.map((tool) => tool.toLowerCase());
|
|
310
345
|
const name = params.toolCall.name ?? void 0;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
346
|
+
if (name !== void 0) {
|
|
347
|
+
const candidate = name.toLowerCase();
|
|
348
|
+
return allowed.some((pattern) => matchesTool(pattern, candidate));
|
|
349
|
+
}
|
|
350
|
+
return (KIND_TO_TOOLS[params.toolCall.kind ?? ""] ?? []).some((tool) => {
|
|
351
|
+
const candidate = tool.toLowerCase();
|
|
352
|
+
return allowed.some((pattern) => matchesTool(pattern, candidate));
|
|
353
|
+
});
|
|
314
354
|
}
|
|
315
355
|
function decidePermission(params, tools, sessionId) {
|
|
316
356
|
if (sessionId !== void 0 && params.sessionId !== sessionId) {
|
|
317
|
-
return { response: { outcome: { outcome: "cancelled" } } };
|
|
357
|
+
return { response: { outcome: { outcome: "cancelled" } }, approved: false };
|
|
318
358
|
}
|
|
319
359
|
const name = params.toolCall.name ?? void 0;
|
|
320
360
|
const kind = params.toolCall.kind ?? void 0;
|
|
@@ -323,9 +363,10 @@ function decidePermission(params, tools, sessionId) {
|
|
|
323
363
|
(o) => approved ? o.kind === "allow_once" : o.kind === "reject_once"
|
|
324
364
|
);
|
|
325
365
|
const response = option ? { outcome: { outcome: "selected", optionId: option.optionId } } : { outcome: { outcome: "cancelled" } };
|
|
326
|
-
if (approved) return { response };
|
|
366
|
+
if (approved) return { response, approved };
|
|
327
367
|
return {
|
|
328
368
|
response,
|
|
369
|
+
approved,
|
|
329
370
|
rejected: {
|
|
330
371
|
toolCallId: params.toolCall.toolCallId,
|
|
331
372
|
name,
|
|
@@ -506,6 +547,8 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
506
547
|
let closeResolve;
|
|
507
548
|
let closeReject;
|
|
508
549
|
let connection;
|
|
550
|
+
const argsSurfaced = /* @__PURE__ */ new Set();
|
|
551
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
509
552
|
const killTree = (signal) => {
|
|
510
553
|
if (child.pid !== void 0) {
|
|
511
554
|
try {
|
|
@@ -640,15 +683,23 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
640
683
|
}
|
|
641
684
|
break;
|
|
642
685
|
}
|
|
643
|
-
case "tool_call":
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
input: update.rawInput
|
|
649
|
-
});
|
|
686
|
+
case "tool_call": {
|
|
687
|
+
const name = update.name ?? update.title;
|
|
688
|
+
toolNames.set(update.toolCallId, name);
|
|
689
|
+
if (hasToolArgs(update.rawInput)) argsSurfaced.add(update.toolCallId);
|
|
690
|
+
emit({ type: "tool_call", id: update.toolCallId, name, input: update.rawInput });
|
|
650
691
|
break;
|
|
651
|
-
|
|
692
|
+
}
|
|
693
|
+
case "tool_call_update": {
|
|
694
|
+
if (!argsSurfaced.has(update.toolCallId) && hasToolArgs(update.rawInput)) {
|
|
695
|
+
argsSurfaced.add(update.toolCallId);
|
|
696
|
+
emit({
|
|
697
|
+
type: "tool_call",
|
|
698
|
+
id: update.toolCallId,
|
|
699
|
+
name: update.name ?? toolNames.get(update.toolCallId) ?? update.title ?? "",
|
|
700
|
+
input: update.rawInput
|
|
701
|
+
});
|
|
702
|
+
}
|
|
652
703
|
emit({
|
|
653
704
|
type: "tool_result",
|
|
654
705
|
id: update.toolCallId,
|
|
@@ -656,6 +707,7 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
656
707
|
isError: update.status === "failed"
|
|
657
708
|
});
|
|
658
709
|
break;
|
|
710
|
+
}
|
|
659
711
|
case "plan":
|
|
660
712
|
emit({ type: "plan", entries: update.entries.map((entry3) => entry3.content) });
|
|
661
713
|
break;
|
|
@@ -690,22 +742,26 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
690
742
|
if (state !== "running" || !turn || params.sessionId !== sessionId || context.signal.aborted)
|
|
691
743
|
return cancelled;
|
|
692
744
|
const mode = turn.options.permissionMode ?? "auto";
|
|
693
|
-
const { response, rejected } = decidePermission(params, options.tools, sessionId);
|
|
694
|
-
if (
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
vendor: "acp",
|
|
699
|
-
payload: { type: "permission_rejected", ...rejected }
|
|
700
|
-
});
|
|
701
|
-
return response;
|
|
745
|
+
const { response, approved, rejected } = decidePermission(params, options.tools, sessionId);
|
|
746
|
+
if (approved) {
|
|
747
|
+
if (mode !== "deny") return response;
|
|
748
|
+
warn("Tool permission denied by the configured approval mode.");
|
|
749
|
+
return cancelled;
|
|
702
750
|
}
|
|
703
|
-
|
|
704
|
-
if (
|
|
751
|
+
const askable = mode === "ask" && turn.options.onPermission !== void 0;
|
|
752
|
+
if (!askable) {
|
|
753
|
+
const label = rejected?.name ?? rejected?.kind ?? rejected?.toolCallId ?? "an unnamed tool";
|
|
705
754
|
warn(
|
|
706
|
-
mode === "ask" ?
|
|
755
|
+
mode === "ask" ? `Tool permission cancelled: no approval handler is available (${label}).` : `Tool permission denied by the configured tools policy: ${label}.`
|
|
707
756
|
);
|
|
708
|
-
|
|
757
|
+
if (rejected) {
|
|
758
|
+
emit({
|
|
759
|
+
type: "raw",
|
|
760
|
+
vendor: "acp",
|
|
761
|
+
payload: { type: "permission_rejected", ...rejected }
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return mode === "deny" ? cancelled : response;
|
|
709
765
|
}
|
|
710
766
|
const signal = AbortSignal.any([turn.decisions.signal, context.signal]);
|
|
711
767
|
const offered = new Set(params.options.map((option) => option.optionId));
|
|
@@ -736,7 +792,18 @@ var AcpRuntime = class extends RuntimeBase {
|
|
|
736
792
|
aborted
|
|
737
793
|
]);
|
|
738
794
|
if (signal.aborted || active !== turn || state !== "running") return cancelled;
|
|
739
|
-
|
|
795
|
+
const chosen = params.options.find((option) => option.optionId === optionId);
|
|
796
|
+
if (optionId === void 0 || chosen === void 0 || !offered.has(optionId)) {
|
|
797
|
+
return cancelled;
|
|
798
|
+
}
|
|
799
|
+
if (chosen.kind.startsWith("reject") && rejected) {
|
|
800
|
+
emit({
|
|
801
|
+
type: "raw",
|
|
802
|
+
vendor: "acp",
|
|
803
|
+
payload: { type: "permission_rejected", ...rejected, source: "user" }
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
740
807
|
} catch {
|
|
741
808
|
return cancelled;
|
|
742
809
|
} finally {
|
|
@@ -1987,6 +2054,19 @@ var AgentSessionManager = class {
|
|
|
1987
2054
|
}
|
|
1988
2055
|
}
|
|
1989
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
|
+
}
|
|
1990
2070
|
/**
|
|
1991
2071
|
* Deliver a message into every session that is running a turn right now.
|
|
1992
2072
|
*
|
|
@@ -2366,31 +2446,6 @@ function withTimeout(factory, ms, label, signal, options) {
|
|
|
2366
2446
|
});
|
|
2367
2447
|
}
|
|
2368
2448
|
|
|
2369
|
-
// src/utils/toolSummary.ts
|
|
2370
|
-
var MAX_SUMMARY_COLUMNS = 120;
|
|
2371
|
-
function stringifySafe(value) {
|
|
2372
|
-
if (typeof value === "string") return value;
|
|
2373
|
-
if (value === void 0 || value === null) return "";
|
|
2374
|
-
try {
|
|
2375
|
-
return JSON.stringify(value) ?? "";
|
|
2376
|
-
} catch {
|
|
2377
|
-
return String(value);
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
function firstLineTruncated(value) {
|
|
2381
|
-
const text = stringifySafe(value);
|
|
2382
|
-
const newline = text.indexOf("\n");
|
|
2383
|
-
const firstLine = newline === -1 ? text : text.slice(0, newline);
|
|
2384
|
-
if (firstLine.length <= MAX_SUMMARY_COLUMNS) return firstLine;
|
|
2385
|
-
return `${firstLine.slice(0, MAX_SUMMARY_COLUMNS - 1)}\u2026`;
|
|
2386
|
-
}
|
|
2387
|
-
function summarizeToolInput(input) {
|
|
2388
|
-
return firstLineTruncated(input);
|
|
2389
|
-
}
|
|
2390
|
-
function summarizeToolOutput(output) {
|
|
2391
|
-
return firstLineTruncated(output);
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
2449
|
// src/orchestrator/AgentRunner.ts
|
|
2395
2450
|
var AgentRunner = class {
|
|
2396
2451
|
constructor(eventBus, contextManager, globalWorkDir, config, sessionManager) {
|
|
@@ -2451,6 +2506,16 @@ ${prompt}`;
|
|
|
2451
2506
|
if (instruction) prompt += `
|
|
2452
2507
|
|
|
2453
2508
|
${instruction}`;
|
|
2509
|
+
const toolRows = /* @__PURE__ */ new Set();
|
|
2510
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
2511
|
+
const printToolRow = (id, name, input) => {
|
|
2512
|
+
if (toolRows.has(id)) return;
|
|
2513
|
+
toolRows.add(id);
|
|
2514
|
+
const summary = summarizeToolInput(input);
|
|
2515
|
+
this.emitOutput(agent.id, `
|
|
2516
|
+
\u203A ${name}${summary ? ` ${summary}` : ""}
|
|
2517
|
+
`, "tool");
|
|
2518
|
+
};
|
|
2454
2519
|
const task = {
|
|
2455
2520
|
agentId: agent.id,
|
|
2456
2521
|
systemPrompt: agent.systemPrompt,
|
|
@@ -2492,17 +2557,13 @@ ${instruction}`;
|
|
|
2492
2557
|
return;
|
|
2493
2558
|
}
|
|
2494
2559
|
if (e.type === "tool_call") {
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
agent.id,
|
|
2498
|
-
`
|
|
2499
|
-
\u203A ${e.name}${summary ? ` ${summary}` : ""}
|
|
2500
|
-
`,
|
|
2501
|
-
"tool"
|
|
2502
|
-
);
|
|
2560
|
+
toolNames.set(e.id, e.name);
|
|
2561
|
+
if (summarizeToolInput(e.input)) printToolRow(e.id, e.name, e.input);
|
|
2503
2562
|
return;
|
|
2504
2563
|
}
|
|
2505
2564
|
if (e.type === "tool_result") {
|
|
2565
|
+
const name = toolNames.get(e.id);
|
|
2566
|
+
if (name !== void 0) printToolRow(e.id, name, void 0);
|
|
2506
2567
|
const summary = summarizeToolOutput(e.output);
|
|
2507
2568
|
this.emitOutput(
|
|
2508
2569
|
agent.id,
|
|
@@ -2687,7 +2748,7 @@ var Orchestrator = class {
|
|
|
2687
2748
|
}
|
|
2688
2749
|
if (options.signal?.aborted)
|
|
2689
2750
|
return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
|
|
2690
|
-
this.
|
|
2751
|
+
this.prepareTaskPlan();
|
|
2691
2752
|
const resolution = await this.resolveTurn(options.signal);
|
|
2692
2753
|
if (resolution.kind === "cancelled" || options.signal?.aborted) {
|
|
2693
2754
|
return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
|
|
@@ -2738,7 +2799,7 @@ var Orchestrator = class {
|
|
|
2738
2799
|
return this.finishEmpty(startTime, void 0, false);
|
|
2739
2800
|
}
|
|
2740
2801
|
if (options.signal?.aborted) return this.finishCancelled(startTime);
|
|
2741
|
-
this.
|
|
2802
|
+
this.prepareTaskPlan();
|
|
2742
2803
|
const resolution = await this.resolveTurn(options.signal);
|
|
2743
2804
|
if (resolution.kind === "cancelled" || options.signal?.aborted) {
|
|
2744
2805
|
return this.finishCancelled(startTime);
|
|
@@ -2747,6 +2808,7 @@ var Orchestrator = class {
|
|
|
2747
2808
|
return this.finishWithMessage(resolution.mode, resolution.message, startTime);
|
|
2748
2809
|
}
|
|
2749
2810
|
const agents2 = resolution.agents;
|
|
2811
|
+
if (this.config.resume) this.applyResumeTaskPlan(agents2);
|
|
2750
2812
|
if (agents2.length === 0)
|
|
2751
2813
|
return this.finishEmpty(
|
|
2752
2814
|
startTime,
|
|
@@ -3045,14 +3107,20 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3045
3107
|
}
|
|
3046
3108
|
}
|
|
3047
3109
|
/**
|
|
3048
|
-
*
|
|
3110
|
+
* Decide what to do with last run's task plan before this turn starts.
|
|
3049
3111
|
*
|
|
3050
3112
|
* The console runs every turn against the same workspace, so the plan file
|
|
3051
3113
|
* survives between turns. If a turn's architect fails to write a new one,
|
|
3052
3114
|
* `applyTaskPlan` would silently re-apply the previous turn's titles, scopes
|
|
3053
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.
|
|
3054
3121
|
*/
|
|
3055
|
-
|
|
3122
|
+
prepareTaskPlan() {
|
|
3123
|
+
if (this.config.resume) return;
|
|
3056
3124
|
fs4.rmSync(path5.join(this.workDir, "task-plan.json"), { force: true });
|
|
3057
3125
|
}
|
|
3058
3126
|
/**
|
|
@@ -3065,7 +3133,7 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3065
3133
|
*/
|
|
3066
3134
|
async resolveTurn(signal) {
|
|
3067
3135
|
if (signal?.aborted) return { kind: "cancelled" };
|
|
3068
|
-
if (this.config.resume) {
|
|
3136
|
+
if (this.config.resume?.agents) {
|
|
3069
3137
|
return {
|
|
3070
3138
|
kind: "run",
|
|
3071
3139
|
agents: this.selectAgents(this.config.resume.agents, "The resume list")
|
|
@@ -3105,7 +3173,7 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3105
3173
|
* `applyTaskPlan` writes `taskTitle` / `dependsOn` / `agentScope` onto whatever
|
|
3106
3174
|
* objects it is handed. The console reuses a single config across turns, so
|
|
3107
3175
|
* returning the config's own objects would leak turn 1's dependency graph into
|
|
3108
|
-
* turn 2 — the exact cross-turn contamination `
|
|
3176
|
+
* turn 2 — the exact cross-turn contamination `prepareTaskPlan` exists to
|
|
3109
3177
|
* prevent, but for the in-memory fields rather than the file.
|
|
3110
3178
|
*/
|
|
3111
3179
|
configuredAgents() {
|
|
@@ -3182,9 +3250,21 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3182
3250
|
isPlanner(agent) {
|
|
3183
3251
|
return agent.role === "planner" || agent.id === "architect";
|
|
3184
3252
|
}
|
|
3185
|
-
|
|
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() {
|
|
3186
3266
|
const taskPlanPath = path5.join(this.workDir, "task-plan.json");
|
|
3187
|
-
if (!fs4.existsSync(taskPlanPath)) return;
|
|
3267
|
+
if (!fs4.existsSync(taskPlanPath)) return void 0;
|
|
3188
3268
|
let taskPlan;
|
|
3189
3269
|
try {
|
|
3190
3270
|
taskPlan = JSON.parse(fs4.readFileSync(taskPlanPath, "utf-8"));
|
|
@@ -3192,14 +3272,13 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3192
3272
|
this.warn(
|
|
3193
3273
|
`Could not parse task-plan.json \u2014 continuing with the original agent set: ${errorMessage2(err)}`
|
|
3194
3274
|
);
|
|
3195
|
-
return;
|
|
3275
|
+
return void 0;
|
|
3196
3276
|
}
|
|
3197
3277
|
if (typeof taskPlan !== "object" || taskPlan === null || !("tasks" in taskPlan) || !Array.isArray(taskPlan.tasks)) {
|
|
3198
3278
|
this.warn("task-plan.json has no `tasks` array \u2014 ignoring it.");
|
|
3199
|
-
return;
|
|
3279
|
+
return void 0;
|
|
3200
3280
|
}
|
|
3201
3281
|
const taskMap = /* @__PURE__ */ new Map();
|
|
3202
|
-
const selected = new Set(agents2.map((a) => a.id));
|
|
3203
3282
|
for (const [index, entry3] of taskPlan.tasks.entries()) {
|
|
3204
3283
|
if (typeof entry3 !== "object" || entry3 === null || Array.isArray(entry3)) {
|
|
3205
3284
|
this.warn(`task-plan.json contains a malformed task at index ${index} \u2014 ignoring it.`);
|
|
@@ -3210,18 +3289,23 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3210
3289
|
this.warn(`task-plan.json contains a malformed task at index ${index} \u2014 ignoring it.`);
|
|
3211
3290
|
continue;
|
|
3212
3291
|
}
|
|
3213
|
-
if (!selected.has(task.id)) {
|
|
3214
|
-
this.warn(
|
|
3215
|
-
`The task plan names "${task.id}", which is not in this run \u2014 ignoring that task.`
|
|
3216
|
-
);
|
|
3217
|
-
continue;
|
|
3218
|
-
}
|
|
3219
3292
|
if (taskMap.has(task.id)) {
|
|
3220
3293
|
this.warn(`The task plan contains a duplicate task for "${task.id}" \u2014 ignoring that task.`);
|
|
3221
3294
|
continue;
|
|
3222
3295
|
}
|
|
3223
3296
|
taskMap.set(task.id, task);
|
|
3224
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
|
+
}
|
|
3225
3309
|
for (const agent of agents2) {
|
|
3226
3310
|
const task = taskMap.get(agent.id);
|
|
3227
3311
|
if (task) {
|
|
@@ -3243,6 +3327,27 @@ Address these findings within your assigned scope, preserve the complete task re
|
|
|
3243
3327
|
}
|
|
3244
3328
|
}
|
|
3245
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
|
+
}
|
|
3246
3351
|
/**
|
|
3247
3352
|
* The `requirement` field for a result, present only when the caller set one.
|
|
3248
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
|
};
|
package/dist/types/runtime.d.ts
CHANGED
|
@@ -28,7 +28,14 @@ export type RuntimeEvent = (RuntimeEventBase & {
|
|
|
28
28
|
type: "message";
|
|
29
29
|
role: "agent" | "thought" | "user";
|
|
30
30
|
text: string;
|
|
31
|
-
})
|
|
31
|
+
})
|
|
32
|
+
/**
|
|
33
|
+
* A tool call. The same `id` may arrive more than once: providers announce a
|
|
34
|
+
* call before its arguments have streamed, so the first event can carry an
|
|
35
|
+
* empty `input` and a later one the completed arguments. Consumers should key
|
|
36
|
+
* on `id` and render a row once.
|
|
37
|
+
*/
|
|
38
|
+
| (RuntimeEventBase & {
|
|
32
39
|
type: "tool_call";
|
|
33
40
|
id: string;
|
|
34
41
|
name: string;
|
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
* truncated line and are deliberately total — a summary is diagnostics and must
|
|
8
8
|
* never be the reason a run fails.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Whether a tool call was given any arguments.
|
|
12
|
+
*
|
|
13
|
+
* "No arguments yet" reaches us as `{}` (or missing) rather than as
|
|
14
|
+
* `undefined`: ACP announces a call before its input has finished streaming.
|
|
15
|
+
* Both mean the same thing to a reader — there is nothing to show yet — so the
|
|
16
|
+
* two are collapsed here instead of at every call site.
|
|
17
|
+
*/
|
|
18
|
+
export declare function hasToolArgs(input: unknown): boolean;
|
|
10
19
|
/** One-line summary of a tool call's input. `input` is untrusted and may be circular. */
|
|
11
20
|
export declare function summarizeToolInput(input: unknown): string;
|
|
12
21
|
/** One-line summary of a tool result's output, which may be a string or a value. */
|