@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 = new Set(tools.map((tool) => tool.toLowerCase()));
344
+ const allowed = tools.map((tool) => tool.toLowerCase());
310
345
  const name = params.toolCall.name ?? void 0;
311
- return name !== void 0 ? allowed.has(name.toLowerCase()) : (KIND_TO_TOOLS[params.toolCall.kind ?? ""] ?? []).some(
312
- (tool) => allowed.has(tool.toLowerCase())
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
- emit({
645
- type: "tool_call",
646
- id: update.toolCallId,
647
- name: update.name ?? update.title,
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
- case "tool_call_update":
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 (rejected) {
695
- warn("Tool permission denied by the configured tools policy.");
696
- emit({
697
- type: "raw",
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
- if (mode === "auto") return response;
704
- if (mode !== "ask" || !turn.options.onPermission) {
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" ? "Tool permission cancelled: no approval handler is available." : "Tool permission denied by the configured approval mode."
755
+ mode === "ask" ? `Tool permission cancelled: no approval handler is available (${label}).` : `Tool permission denied by the configured tools policy: ${label}.`
707
756
  );
708
- return cancelled;
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
- return optionId !== void 0 && offered.has(optionId) ? { outcome: { outcome: "selected", optionId } } : cancelled;
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
- const summary = summarizeToolInput(e.input);
2496
- this.emitOutput(
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.clearStaleTaskPlan();
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.clearStaleTaskPlan();
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
- * Drop last run's task plan before the architect gets a chance to write one.
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
- clearStaleTaskPlan() {
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 `clearStaleTaskPlan` exists to
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
- async applyTaskPlan(agents2, executedIds) {
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
- * Drop last run's task plan before the architect gets a chance to write one.
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 clearStaleTaskPlan;
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 `clearStaleTaskPlan` exists to
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
  *
@@ -66,17 +66,26 @@ export interface MultiClawConfig {
66
66
  */
67
67
  history?: TurnRecord[];
68
68
  /**
69
- * Re-run a known set of agents instead of letting the leader route, continuing
70
- * their sessions where one is known.
69
+ * Continue known agent sessions, and optionally re-run a fixed set of agents
70
+ * instead of letting the leader route.
71
71
  *
72
- * Set by a resume/retry driver — the CLI's `--resume`, the console's `/retry` —
73
- * which already knows which agents to redo. Like `history`, it is per-run state
74
- * the caller carries: core has no session of its own, and a config that never
75
- * resumes leaves it unset and behaves exactly as before.
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: string[];
88
+ agents?: string[];
80
89
  /** agentId → session id, for agents whose session is worth continuing. */
81
90
  sessions?: Record<string, string>;
82
91
  };
@@ -28,7 +28,14 @@ export type RuntimeEvent = (RuntimeEventBase & {
28
28
  type: "message";
29
29
  role: "agent" | "thought" | "user";
30
30
  text: string;
31
- }) | (RuntimeEventBase & {
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiclawcli/core",
3
- "version": "0.6.3",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",