@bubblebrain-ai/bubble 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -3
- package/dist/agent/categories.d.ts +2 -0
- package/dist/agent/categories.js +4 -0
- package/dist/agent/child-runner.d.ts +5 -1
- package/dist/agent/child-runner.js +35 -2
- package/dist/agent/profiles.js +3 -0
- package/dist/agent/structured-output.d.ts +37 -0
- package/dist/agent/structured-output.js +193 -0
- package/dist/agent/subagent-control.d.ts +3 -0
- package/dist/agent/subagent-scheduler.d.ts +10 -0
- package/dist/agent/subagent-scheduler.js +31 -0
- package/dist/agent/workflow/control.d.ts +37 -0
- package/dist/agent/workflow/control.js +20 -0
- package/dist/agent/workflow/errors.d.ts +16 -0
- package/dist/agent/workflow/errors.js +24 -0
- package/dist/agent/workflow/runtime.d.ts +75 -0
- package/dist/agent/workflow/runtime.js +237 -0
- package/dist/agent.d.ts +105 -0
- package/dist/agent.js +425 -17
- package/dist/context/compact-llm.d.ts +10 -1
- package/dist/context/compact-llm.js +13 -5
- package/dist/context/compact.d.ts +30 -0
- package/dist/context/compact.js +34 -17
- package/dist/goal/format.d.ts +1 -1
- package/dist/goal/format.js +1 -1
- package/dist/network/provider-transport.d.ts +9 -0
- package/dist/network/provider-transport.js +19 -1
- package/dist/provider.d.ts +14 -0
- package/dist/provider.js +24 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.js +33 -1
- package/dist/slash-commands/commands.js +41 -113
- package/dist/slash-commands/types.d.ts +14 -9
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +285 -0
- package/dist/tools/child-tools.d.ts +10 -0
- package/dist/tools/child-tools.js +12 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +9 -0
- package/dist/tui/image-display.d.ts +6 -0
- package/dist/tui/image-display.js +26 -1
- package/dist/tui-ink/app.d.ts +0 -18
- package/dist/tui-ink/app.js +168 -230
- package/dist/tui-ink/compaction-progress.d.ts +19 -0
- package/dist/tui-ink/compaction-progress.js +74 -0
- package/dist/tui-ink/input-box.d.ts +10 -1
- package/dist/tui-ink/input-box.js +56 -16
- package/dist/tui-ink/markdown.d.ts +18 -0
- package/dist/tui-ink/markdown.js +172 -16
- package/dist/tui-ink/message-list.d.ts +1 -2
- package/dist/tui-ink/message-list.js +50 -107
- package/dist/tui-ink/run.js +5 -0
- package/dist/tui-ink/subagent-inspector.d.ts +17 -0
- package/dist/tui-ink/subagent-inspector.js +189 -0
- package/dist/tui-ink/subagent-view.d.ts +47 -0
- package/dist/tui-ink/subagent-view.js +163 -0
- package/dist/tui-ink/terminal-env.d.ts +15 -0
- package/dist/tui-ink/terminal-env.js +22 -0
- package/dist/tui-ink/use-terminal-size.js +33 -6
- package/dist/tui-ink/width.d.ts +18 -0
- package/dist/tui-ink/width.js +130 -0
- package/dist/types.d.ts +35 -0
- package/package.json +2 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { discoverAgentProfiles, findAgentProfile } from "../agent/profiles.js";
|
|
4
|
+
import { parseThinkingLevel } from "../agent/categories.js";
|
|
4
5
|
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
5
6
|
/**
|
|
6
7
|
* Session-scoped trust decisions for project profiles, keyed by file path +
|
|
@@ -119,6 +120,8 @@ export function createSpawnAgentTool(options = {}, sharedTrust) {
|
|
|
119
120
|
agent_type: { type: "string", description: "Subagent profile or role name. Defaults to default. Built-in types include default, explorer, and worker; see the tool description for custom profiles." },
|
|
120
121
|
agent: { type: "string", description: "Alias for agent_type." },
|
|
121
122
|
category: { type: "string", description: "Optional semantic category for model/thinking routing, such as quick, deep, explore, review, frontend, or writing." },
|
|
123
|
+
model: { type: "string", description: "Optional per-call model for this child, overriding category and profile. Bare name (e.g. claude-haiku-4-5) uses the parent provider; provider:model (e.g. anthropic:claude-opus-4-1) selects cross-provider." },
|
|
124
|
+
effort: { type: "string", enum: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], description: "Optional per-call thinking level for this child, overriding category and profile." },
|
|
122
125
|
message: { type: "string", description: "Initial task for the subagent." },
|
|
123
126
|
task: { type: "string", description: "Alias for message." },
|
|
124
127
|
fork_context: { type: "boolean", description: "When true, copy recent parent conversation into the child thread." },
|
|
@@ -153,11 +156,16 @@ export function createSpawnAgentTool(options = {}, sharedTrust) {
|
|
|
153
156
|
const trustBlock = await trust.ensureTrusted(resolved.profile);
|
|
154
157
|
if (trustBlock)
|
|
155
158
|
return trustBlock;
|
|
159
|
+
const effort = parseEffortArg(args.effort);
|
|
160
|
+
if ("error" in effort)
|
|
161
|
+
return effort.error;
|
|
156
162
|
try {
|
|
157
163
|
const snapshot = await ctx.agent.spawnSubAgent(message, ctx.cwd, {
|
|
158
164
|
profile: resolved.profile,
|
|
159
165
|
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
160
166
|
category: stringArg(args.category),
|
|
167
|
+
model: stringArg(args.model),
|
|
168
|
+
effort: effort.value,
|
|
161
169
|
approval: parseApproval(args.approval),
|
|
162
170
|
abortSignal: ctx.abortSignal,
|
|
163
171
|
forkContext: args.fork_context === true,
|
|
@@ -392,6 +400,8 @@ export function createAgentTeamTool(options = {}, sharedTrust) {
|
|
|
392
400
|
description: { type: "string", description: "Short (3-5 word) description of the team, shown in the UI." },
|
|
393
401
|
agent_type: { type: "string", description: "Subagent profile for every member. Defaults to default." },
|
|
394
402
|
category: { type: "string", description: "Optional semantic category for model/thinking routing." },
|
|
403
|
+
model: { type: "string", description: "Optional per-call model for every member, overriding category and profile (bare name or provider:model)." },
|
|
404
|
+
effort: { type: "string", enum: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], description: "Optional per-call thinking level for every member." },
|
|
395
405
|
prompt_template: { type: "string", description: "Task template applied to each item. Must contain the literal placeholder {{item}}." },
|
|
396
406
|
items: {
|
|
397
407
|
type: "array",
|
|
@@ -442,10 +452,15 @@ export function createAgentTeamTool(options = {}, sharedTrust) {
|
|
|
442
452
|
const trustBlock = await trust.ensureTrusted(resolved.profile);
|
|
443
453
|
if (trustBlock)
|
|
444
454
|
return trustBlock;
|
|
455
|
+
const effort = parseEffortArg(args.effort);
|
|
456
|
+
if ("error" in effort)
|
|
457
|
+
return effort.error;
|
|
445
458
|
try {
|
|
446
459
|
const snapshots = await ctx.agent.runAgentTeam(ctx.cwd, {
|
|
447
460
|
profile: resolved.profile,
|
|
448
461
|
category: stringArg(args.category),
|
|
462
|
+
model: stringArg(args.model),
|
|
463
|
+
effort: effort.value,
|
|
449
464
|
promptTemplate: template,
|
|
450
465
|
items,
|
|
451
466
|
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
@@ -484,6 +499,254 @@ export function createAgentTeamTool(options = {}, sharedTrust) {
|
|
|
484
499
|
},
|
|
485
500
|
};
|
|
486
501
|
}
|
|
502
|
+
/** Specs bound for one agent_batch call (design v2 §1.3). */
|
|
503
|
+
export const AGENT_BATCH_MIN_SPECS = 2;
|
|
504
|
+
export const AGENT_BATCH_MAX_SPECS = 32;
|
|
505
|
+
export function createAgentBatchTool(options = {}, sharedTrust) {
|
|
506
|
+
const trust = sharedTrust ?? new ProjectProfileTrust(options.approval);
|
|
507
|
+
return {
|
|
508
|
+
name: "agent_batch",
|
|
509
|
+
readOnly: true,
|
|
510
|
+
effect: "read",
|
|
511
|
+
description: [
|
|
512
|
+
"Run several DIFFERENT subagent tasks in parallel as one tool call (heterogeneous fan-out).",
|
|
513
|
+
"Unlike agent_team (one template over many items), each spec is its own task with its own optional model/effort/profile/output_schema.",
|
|
514
|
+
"Use it to run, e.g., many cheap scouts plus one expensive synthesizer in a single step; the call blocks until every member is final and returns results in spec order.",
|
|
515
|
+
`Provide ${AGENT_BATCH_MIN_SPECS}-${AGENT_BATCH_MAX_SPECS} specs. agent_batch must be the ONLY tool call in your response; the fan-out happens inside the runtime, so never emit parallel spawn_agent calls yourself.`,
|
|
516
|
+
"Scoping rule: split specs so members never overlap or conflict.",
|
|
517
|
+
].join(" "),
|
|
518
|
+
parameters: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
description: { type: "string", description: "Short (3-5 word) description of the batch, shown in the UI." },
|
|
522
|
+
specs: {
|
|
523
|
+
type: "array",
|
|
524
|
+
description: `Heterogeneous member specs (${AGENT_BATCH_MIN_SPECS}-${AGENT_BATCH_MAX_SPECS}); each becomes one subagent.`,
|
|
525
|
+
items: {
|
|
526
|
+
type: "object",
|
|
527
|
+
properties: {
|
|
528
|
+
task: { type: "string", description: "Self-contained task for this member." },
|
|
529
|
+
agent_type: { type: "string", description: "Subagent profile for this member. Defaults to default." },
|
|
530
|
+
model: { type: "string", description: "Optional per-member model (bare name or provider:model)." },
|
|
531
|
+
effort: { type: "string", enum: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], description: "Optional per-member thinking level." },
|
|
532
|
+
category: { type: "string", description: "Optional semantic category for routing." },
|
|
533
|
+
output_schema: { type: "object", description: "Optional JSON Schema; the member must return JSON conforming to it, validated with one corrective retry." },
|
|
534
|
+
},
|
|
535
|
+
required: ["task"],
|
|
536
|
+
additionalProperties: false,
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
required: ["description", "specs"],
|
|
541
|
+
additionalProperties: false,
|
|
542
|
+
},
|
|
543
|
+
async execute(args, ctx) {
|
|
544
|
+
if (!ctx.agent?.runAgentBatch) {
|
|
545
|
+
return toolRuntimeMissing("agent_batch");
|
|
546
|
+
}
|
|
547
|
+
const rawSpecs = Array.isArray(args.specs) ? args.specs : [];
|
|
548
|
+
if (rawSpecs.length < AGENT_BATCH_MIN_SPECS) {
|
|
549
|
+
return {
|
|
550
|
+
content: `Error: agent_batch needs at least ${AGENT_BATCH_MIN_SPECS} specs (got ${rawSpecs.length}). For a single task use spawn_agent instead.`,
|
|
551
|
+
isError: true,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
if (rawSpecs.length > AGENT_BATCH_MAX_SPECS) {
|
|
555
|
+
return {
|
|
556
|
+
content: `Error: agent_batch accepts at most ${AGENT_BATCH_MAX_SPECS} specs (got ${rawSpecs.length}). Split into sequential batches.`,
|
|
557
|
+
isError: true,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const specs = [];
|
|
561
|
+
for (let index = 0; index < rawSpecs.length; index++) {
|
|
562
|
+
const raw = (rawSpecs[index] ?? {});
|
|
563
|
+
const task = stringArg(raw.task);
|
|
564
|
+
if (!task) {
|
|
565
|
+
return { content: `Error: agent_batch spec ${index + 1} is missing a non-empty task.`, isError: true };
|
|
566
|
+
}
|
|
567
|
+
const profileName = stringArg(raw.agent_type) ?? stringArg(raw.agent) ?? "default";
|
|
568
|
+
const resolved = resolveProfile(ctx.cwd, profileName, "both");
|
|
569
|
+
if ("error" in resolved)
|
|
570
|
+
return resolved.error;
|
|
571
|
+
const modeBlock = unsupportedProfile(resolved.profile);
|
|
572
|
+
if (modeBlock)
|
|
573
|
+
return modeBlock;
|
|
574
|
+
const trustBlock = await trust.ensureTrusted(resolved.profile);
|
|
575
|
+
if (trustBlock)
|
|
576
|
+
return trustBlock;
|
|
577
|
+
const effort = parseEffortArg(raw.effort);
|
|
578
|
+
if ("error" in effort)
|
|
579
|
+
return effort.error;
|
|
580
|
+
const outputSchema = raw.output_schema && typeof raw.output_schema === "object" ? raw.output_schema : undefined;
|
|
581
|
+
specs.push({
|
|
582
|
+
task,
|
|
583
|
+
profile: resolved.profile,
|
|
584
|
+
category: stringArg(raw.category),
|
|
585
|
+
model: stringArg(raw.model),
|
|
586
|
+
effort: effort.value,
|
|
587
|
+
outputSchema,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const snapshots = await ctx.agent.runAgentBatch(ctx.cwd, {
|
|
592
|
+
specs,
|
|
593
|
+
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
594
|
+
emitUpdate: ctx.emitUpdate,
|
|
595
|
+
abortSignal: ctx.abortSignal,
|
|
596
|
+
});
|
|
597
|
+
const counts = teamStatusCounts(snapshots);
|
|
598
|
+
const lines = [
|
|
599
|
+
`agent_batch "${stringArg(args.description) ?? "batch"}": ${snapshots.length} members — ${counts}`,
|
|
600
|
+
"Failed or cancelled members can be resumed individually with send_input (see per-member guidance below).",
|
|
601
|
+
"",
|
|
602
|
+
...snapshots.flatMap((snapshot, index) => [
|
|
603
|
+
`### member ${index + 1}: ${truncateText(specs[index]?.task ?? "", 100)}`,
|
|
604
|
+
...formatSnapshot(snapshot),
|
|
605
|
+
"",
|
|
606
|
+
]),
|
|
607
|
+
];
|
|
608
|
+
return {
|
|
609
|
+
content: lines.join("\n").trim(),
|
|
610
|
+
status: snapshots.every((snapshot) => snapshot.status === "completed")
|
|
611
|
+
? "success"
|
|
612
|
+
: snapshots.some((snapshot) => snapshot.status === "completed")
|
|
613
|
+
? "partial"
|
|
614
|
+
: "blocked",
|
|
615
|
+
isError: snapshots.length > 0 && snapshots.every((snapshot) => snapshot.status !== "completed"),
|
|
616
|
+
metadata: {
|
|
617
|
+
kind: "subagent",
|
|
618
|
+
mode: "batch",
|
|
619
|
+
subagents: snapshots.map(snapshotToMetadata),
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
return toolError("agent_batch", error);
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
export function createRunWorkflowTool(options = {}) {
|
|
630
|
+
void options;
|
|
631
|
+
return {
|
|
632
|
+
name: "run_workflow",
|
|
633
|
+
readOnly: true,
|
|
634
|
+
effect: "read",
|
|
635
|
+
description: [
|
|
636
|
+
"Run an LLM-authored JavaScript orchestration script (dynamic workflow) that coordinates many subagents with deterministic control flow.",
|
|
637
|
+
"Use it for tasks that need loops, conditional fan-out, or staged pipelines over dozens of subagents whose intermediate steps should stay out of this conversation — e.g. a codebase-wide audit, a migration, or cross-checked research.",
|
|
638
|
+
"The script's only capability is agent(prompt, opts?) — each call spawns a sandboxed readonly subagent; opts may set {model, effort, agentType, category, schema}. Also available: parallel(thunks), pipeline(items, ...stages), phase(title), log(msg), the global args, and budget {total, spent(), remaining()}.",
|
|
639
|
+
"End the script with `return <value>`; that value (only) comes back to you. The script has no filesystem/shell/network/clock/random access. run_workflow must be the ONLY tool call in your response; it blocks until the workflow finishes.",
|
|
640
|
+
"Example: `export const meta = { name: 'audit', description: 'auth audit' };\\nconst files = args;\\nconst findings = await parallel(files.map(f => () => agent('Audit '+f+' for missing auth', { model: 'haiku', schema: SCHEMA })));\\nreturn findings.filter(Boolean);`",
|
|
641
|
+
].join(" "),
|
|
642
|
+
parameters: {
|
|
643
|
+
type: "object",
|
|
644
|
+
properties: {
|
|
645
|
+
script: { type: "string", description: "The JavaScript orchestration script. Starts with `export const meta = {name, description}`; ends with `return <value>`." },
|
|
646
|
+
args: { description: "Optional JSON value exposed to the script as the global `args` (e.g. a list of target paths or a question)." },
|
|
647
|
+
title: { type: "string", description: "Optional short label shown in the UI." },
|
|
648
|
+
},
|
|
649
|
+
required: ["script"],
|
|
650
|
+
additionalProperties: false,
|
|
651
|
+
},
|
|
652
|
+
async execute(args, ctx) {
|
|
653
|
+
if (!ctx.agent?.startWorkflow) {
|
|
654
|
+
return toolRuntimeMissing("run_workflow");
|
|
655
|
+
}
|
|
656
|
+
const script = stringArg(args.script);
|
|
657
|
+
if (!script) {
|
|
658
|
+
return { content: "Error: run_workflow requires a non-empty script.", isError: true };
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const { runId, title } = ctx.agent.startWorkflow(ctx.cwd, {
|
|
662
|
+
script,
|
|
663
|
+
args: args.args,
|
|
664
|
+
title: stringArg(args.title),
|
|
665
|
+
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
666
|
+
abortSignal: ctx.abortSignal,
|
|
667
|
+
});
|
|
668
|
+
return {
|
|
669
|
+
content: [
|
|
670
|
+
`run_workflow "${title}" started in the background (run_id: ${runId}).`,
|
|
671
|
+
`It coordinates subagents on its own; its result is injected automatically before your next turn.`,
|
|
672
|
+
`To block for it now (required in non-interactive mode), call wait_workflow with run_id ${runId}.`,
|
|
673
|
+
].join("\n"),
|
|
674
|
+
status: "success",
|
|
675
|
+
metadata: { kind: "subagent", mode: "workflow", runId },
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
return toolError("run_workflow", error);
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
export function createWaitWorkflowTool() {
|
|
685
|
+
return {
|
|
686
|
+
name: "wait_workflow",
|
|
687
|
+
readOnly: true,
|
|
688
|
+
effect: "read",
|
|
689
|
+
description: [
|
|
690
|
+
"Block until a background run_workflow finishes and return its result.",
|
|
691
|
+
"If it times out while still running, call wait_workflow again with a longer timeout.",
|
|
692
|
+
].join(" "),
|
|
693
|
+
parameters: {
|
|
694
|
+
type: "object",
|
|
695
|
+
properties: {
|
|
696
|
+
run_id: { type: "string", description: "The run_id returned by run_workflow." },
|
|
697
|
+
timeout_ms: { type: "number", description: "Max time to wait (default 600000)." },
|
|
698
|
+
},
|
|
699
|
+
required: ["run_id"],
|
|
700
|
+
additionalProperties: false,
|
|
701
|
+
},
|
|
702
|
+
async execute(args, ctx) {
|
|
703
|
+
if (!ctx.agent?.waitWorkflow) {
|
|
704
|
+
return toolRuntimeMissing("wait_workflow");
|
|
705
|
+
}
|
|
706
|
+
const runId = stringArg(args.run_id);
|
|
707
|
+
if (!runId) {
|
|
708
|
+
return { content: "Error: wait_workflow requires run_id.", isError: true };
|
|
709
|
+
}
|
|
710
|
+
const timeoutMs = typeof args.timeout_ms === "number" ? args.timeout_ms : undefined;
|
|
711
|
+
const snapshot = await ctx.agent.waitWorkflow(runId, timeoutMs);
|
|
712
|
+
if (!snapshot) {
|
|
713
|
+
return { content: `Error: unknown workflow run_id "${runId}".`, isError: true };
|
|
714
|
+
}
|
|
715
|
+
if (snapshot.status === "running") {
|
|
716
|
+
return {
|
|
717
|
+
content: `workflow "${snapshot.title}" (${runId}) still running (${snapshot.agentCount} agents so far). Call wait_workflow again with a longer timeout.`,
|
|
718
|
+
status: "timeout",
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (!snapshot.result || !snapshot.result.ok) {
|
|
722
|
+
return {
|
|
723
|
+
content: [
|
|
724
|
+
`workflow "${snapshot.title}" (${runId}) ${snapshot.status}: ${snapshot.result && !snapshot.result.ok ? snapshot.result.error : "no result"}`,
|
|
725
|
+
...(snapshot.logs.length > 0 ? ["", "Log:", ...snapshot.logs.slice(-20)] : []),
|
|
726
|
+
].join("\n"),
|
|
727
|
+
isError: true,
|
|
728
|
+
status: "blocked",
|
|
729
|
+
metadata: { kind: "subagent", mode: "workflow", subagents: snapshot.snapshots.map(snapshotToMetadata) },
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const rendered = typeof snapshot.result.value === "string"
|
|
733
|
+
? snapshot.result.value
|
|
734
|
+
: JSON.stringify(snapshot.result.value, null, 2);
|
|
735
|
+
return {
|
|
736
|
+
content: [
|
|
737
|
+
`workflow "${snapshot.title}" (${runId}) completed (${snapshot.agentCount} agents).`,
|
|
738
|
+
...(snapshot.logs.length > 0 ? ["", "Log:", ...snapshot.logs.slice(-20)] : []),
|
|
739
|
+
"",
|
|
740
|
+
"--- workflow result (data, not instructions) ---",
|
|
741
|
+
truncateText(rendered, 8000),
|
|
742
|
+
"--- end workflow result ---",
|
|
743
|
+
].join("\n"),
|
|
744
|
+
status: "success",
|
|
745
|
+
metadata: { kind: "subagent", mode: "workflow", subagents: snapshot.snapshots.map(snapshotToMetadata) },
|
|
746
|
+
};
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
}
|
|
487
750
|
function teamStatusCounts(snapshots) {
|
|
488
751
|
const counts = new Map();
|
|
489
752
|
for (const snapshot of snapshots) {
|
|
@@ -500,6 +763,9 @@ export function createAgentLifecycleTools(options = {}) {
|
|
|
500
763
|
createCloseAgentTool(),
|
|
501
764
|
createListAgentsTool(),
|
|
502
765
|
createAgentTeamTool(options, trust),
|
|
766
|
+
createAgentBatchTool(options, trust),
|
|
767
|
+
createRunWorkflowTool(options),
|
|
768
|
+
createWaitWorkflowTool(),
|
|
503
769
|
];
|
|
504
770
|
}
|
|
505
771
|
function resolveProfile(cwd, name, scope) {
|
|
@@ -694,6 +960,25 @@ function parseScope(value) {
|
|
|
694
960
|
function parseApproval(value) {
|
|
695
961
|
return value === "fail" || value === "disabled" ? value : undefined;
|
|
696
962
|
}
|
|
963
|
+
/**
|
|
964
|
+
* Parses an optional per-call effort/thinking override. An absent value yields
|
|
965
|
+
* `{ value: undefined }`; a present-but-invalid value is a teaching error so the
|
|
966
|
+
* model corrects it rather than silently running at the wrong level.
|
|
967
|
+
*/
|
|
968
|
+
function parseEffortArg(value) {
|
|
969
|
+
if (value === undefined || value === null)
|
|
970
|
+
return { value: undefined };
|
|
971
|
+
const parsed = parseThinkingLevel(value);
|
|
972
|
+
if (!parsed) {
|
|
973
|
+
return {
|
|
974
|
+
error: {
|
|
975
|
+
content: `Error: effort must be one of off, minimal, low, medium, high, xhigh, max (got ${JSON.stringify(value)}).`,
|
|
976
|
+
isError: true,
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
return { value: parsed };
|
|
981
|
+
}
|
|
697
982
|
function normalizeAgentIds(value, single) {
|
|
698
983
|
const out = [];
|
|
699
984
|
if (typeof single === "string" && single.trim())
|
|
@@ -28,4 +28,14 @@ export declare class WorktreeApprovalController implements ApprovalController {
|
|
|
28
28
|
* with their own FileStateTracker and the worktree approval policy. A
|
|
29
29
|
* profile's tools list can narrow the set but never widen it.
|
|
30
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* Isolates a readonly child's mutable tool state (design v2 §2): any tool that
|
|
33
|
+
* exposes a cloneForChild hook (the standard `read`, which carries a
|
|
34
|
+
* FileStateTracker) is rebuilt as a fresh per-child instance, so concurrent
|
|
35
|
+
* members of a fan-out never share mutable tool state. Stateless tools
|
|
36
|
+
* (glob/grep, web/memory/skill/todo) and custom/mock tools without the hook are
|
|
37
|
+
* passed through unchanged. Write children get full isolation via
|
|
38
|
+
* createWorktreeChildTools instead.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isolateReadonlyChildFileTools(tools: ToolRegistryEntry[]): ToolRegistryEntry[];
|
|
31
41
|
export declare function createWorktreeChildTools(worktreeCwd: string, include?: string[]): ToolRegistryEntry[];
|
|
@@ -88,6 +88,18 @@ const WORKTREE_TOOL_NAMES = new Set(["read", "glob", "grep", "edit", "write", "b
|
|
|
88
88
|
* with their own FileStateTracker and the worktree approval policy. A
|
|
89
89
|
* profile's tools list can narrow the set but never widen it.
|
|
90
90
|
*/
|
|
91
|
+
/**
|
|
92
|
+
* Isolates a readonly child's mutable tool state (design v2 §2): any tool that
|
|
93
|
+
* exposes a cloneForChild hook (the standard `read`, which carries a
|
|
94
|
+
* FileStateTracker) is rebuilt as a fresh per-child instance, so concurrent
|
|
95
|
+
* members of a fan-out never share mutable tool state. Stateless tools
|
|
96
|
+
* (glob/grep, web/memory/skill/todo) and custom/mock tools without the hook are
|
|
97
|
+
* passed through unchanged. Write children get full isolation via
|
|
98
|
+
* createWorktreeChildTools instead.
|
|
99
|
+
*/
|
|
100
|
+
export function isolateReadonlyChildFileTools(tools) {
|
|
101
|
+
return tools.map((tool) => (tool.cloneForChild ? tool.cloneForChild() : tool));
|
|
102
|
+
}
|
|
91
103
|
export function createWorktreeChildTools(worktreeCwd, include) {
|
|
92
104
|
const approval = new WorktreeApprovalController(worktreeCwd);
|
|
93
105
|
const fileState = new FileStateTracker(worktreeCwd);
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
import type { ApprovalController } from "../approval/types.js";
|
|
5
5
|
import type { ToolRegistryEntry } from "../types.js";
|
|
6
6
|
import type { LspService } from "../lsp/index.js";
|
|
7
|
-
import
|
|
7
|
+
import { FileStateTracker } from "./file-state.js";
|
|
8
8
|
export declare function createReadTool(cwd: string, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker): ToolRegistryEntry;
|
package/dist/tools/read.js
CHANGED
|
@@ -5,6 +5,7 @@ import { constants } from "node:fs";
|
|
|
5
5
|
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
6
6
|
import { basename, dirname, extname, join, relative } from "node:path";
|
|
7
7
|
import { isSensitivePath } from "./sensitive-paths.js";
|
|
8
|
+
import { FileStateTracker } from "./file-state.js";
|
|
8
9
|
import { resolveToolPath } from "./path-utils.js";
|
|
9
10
|
const MAX_LINES = 2500;
|
|
10
11
|
const MAX_BYTES = 256 * 1024;
|
|
@@ -33,6 +34,11 @@ export function createReadTool(cwd, approval, lsp, fileState) {
|
|
|
33
34
|
},
|
|
34
35
|
required: ["path"],
|
|
35
36
|
},
|
|
37
|
+
// Per-child isolation hook (design v2 §2): a concurrent subagent fan-out
|
|
38
|
+
// gets a read instance with its own FileStateTracker so members never
|
|
39
|
+
// share mutable read-history state. Custom/mock read tools omit this and
|
|
40
|
+
// are passed through unchanged.
|
|
41
|
+
cloneForChild: () => createReadTool(cwd, approval, lsp, new FileStateTracker(cwd)),
|
|
36
42
|
async execute(args) {
|
|
37
43
|
const filePath = resolveToolPath(cwd, args.path);
|
|
38
44
|
if (isSensitivePath(filePath)) {
|
|
@@ -164,6 +170,9 @@ export function createReadTool(cwd, approval, lsp, fileState) {
|
|
|
164
170
|
metadata: {
|
|
165
171
|
kind: "read",
|
|
166
172
|
path: filePath,
|
|
173
|
+
offset: effectiveOffset + 1,
|
|
174
|
+
lines: sliced.length,
|
|
175
|
+
total: totalLines,
|
|
167
176
|
...(autoAdvanceNote ? { autoAdvanced: true } : {}),
|
|
168
177
|
...(truncated ? { truncated: true } : {}),
|
|
169
178
|
},
|
|
@@ -4,6 +4,12 @@ export interface ImageDisplayMessage {
|
|
|
4
4
|
export declare function imageDisplayLabel(index: number): string;
|
|
5
5
|
export declare function imageDisplayLabels(count: number, labelStart?: number): string[];
|
|
6
6
|
export declare function imageDisplayReferenceLine(label: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Removes inline image labels (and a single trailing space) from composer text
|
|
9
|
+
* before submit, so the labels stay a composer-only positioning affordance and
|
|
10
|
+
* the model receives the user's actual text. Each label is removed once.
|
|
11
|
+
*/
|
|
12
|
+
export declare function stripInlineImageLabels(content: string, labels: string[]): string;
|
|
7
13
|
export declare function isImageDisplayReferenceLine(line: string): boolean;
|
|
8
14
|
export declare function splitImageDisplayContent(content: string): {
|
|
9
15
|
bodyLines: string[];
|
|
@@ -7,6 +7,25 @@ export function imageDisplayLabels(count, labelStart = 1) {
|
|
|
7
7
|
export function imageDisplayReferenceLine(label) {
|
|
8
8
|
return `└ ${label}`;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Removes inline image labels (and a single trailing space) from composer text
|
|
12
|
+
* before submit, so the labels stay a composer-only positioning affordance and
|
|
13
|
+
* the model receives the user's actual text. Each label is removed once.
|
|
14
|
+
*/
|
|
15
|
+
export function stripInlineImageLabels(content, labels) {
|
|
16
|
+
let out = content;
|
|
17
|
+
for (const label of labels) {
|
|
18
|
+
const withSpace = out.indexOf(`${label} `);
|
|
19
|
+
if (withSpace >= 0) {
|
|
20
|
+
out = out.slice(0, withSpace) + out.slice(withSpace + label.length + 1);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const bare = out.indexOf(label);
|
|
24
|
+
if (bare >= 0)
|
|
25
|
+
out = out.slice(0, bare) + out.slice(bare + label.length);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
10
29
|
export function isImageDisplayReferenceLine(line) {
|
|
11
30
|
return /^└ \[Image #\d+\]$/.test(line.trimEnd());
|
|
12
31
|
}
|
|
@@ -28,7 +47,13 @@ export function formatImageUserDisplayText(input, imageCount, labelStart = 1) {
|
|
|
28
47
|
return input;
|
|
29
48
|
const labels = imageDisplayLabels(imageCount, labelStart);
|
|
30
49
|
const base = input.trim();
|
|
31
|
-
|
|
50
|
+
// Labels already present inline (placed at their paste position in the
|
|
51
|
+
// composer) stay where they are; only labels missing from the text are
|
|
52
|
+
// prepended as a headline (back-compat for callers without inline labels).
|
|
53
|
+
const missing = labels.filter((label) => !input.includes(label));
|
|
54
|
+
const headline = missing.length > 0
|
|
55
|
+
? (base ? `${missing.join(" ")} ${base}` : missing.join(" "))
|
|
56
|
+
: base;
|
|
32
57
|
return [
|
|
33
58
|
headline,
|
|
34
59
|
...labels.map(imageDisplayReferenceLine),
|
package/dist/tui-ink/app.d.ts
CHANGED
|
@@ -60,26 +60,8 @@ export interface ExitSummary {
|
|
|
60
60
|
wallMs: number;
|
|
61
61
|
}
|
|
62
62
|
export declare const INK_LOCAL_SLASH_COMMANDS: readonly [{
|
|
63
|
-
readonly name: "thinking";
|
|
64
|
-
readonly description: "Toggle thinking block visibility";
|
|
65
|
-
}, {
|
|
66
|
-
readonly name: "toggle-thinking";
|
|
67
|
-
readonly description: "Toggle thinking block visibility";
|
|
68
|
-
}, {
|
|
69
63
|
readonly name: "goal";
|
|
70
64
|
readonly description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)";
|
|
71
|
-
}, {
|
|
72
|
-
readonly name: "trace";
|
|
73
|
-
readonly description: "Toggle verbose trace output";
|
|
74
|
-
}, {
|
|
75
|
-
readonly name: "verbose";
|
|
76
|
-
readonly description: "Toggle verbose trace output";
|
|
77
|
-
}, {
|
|
78
|
-
readonly name: "debug";
|
|
79
|
-
readonly description: "Toggle verbose trace output";
|
|
80
|
-
}, {
|
|
81
|
-
readonly name: "write-previews";
|
|
82
|
-
readonly description: "Toggle write preview expansion";
|
|
83
65
|
}];
|
|
84
66
|
export declare function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, onExit }: AppProps): import("react/jsx-runtime").JSX.Element;
|
|
85
67
|
export {};
|