@narumitw/pi-subagents 1.0.2 → 2.0.1
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 +198 -188
- package/package.json +2 -2
- package/src/agents/built-ins.ts +13 -66
- package/src/agents/catalog.ts +19 -2
- package/src/agents/discovery.ts +31 -15
- package/src/auto-transport.ts +7 -1
- package/src/child-peer-bridge.ts +124 -0
- package/src/child-peer-tools.ts +132 -0
- package/src/completion-delivery.ts +19 -5
- package/src/completion-render.ts +189 -0
- package/src/completion-routing.ts +24 -0
- package/src/config-ui.ts +11 -17
- package/src/consult-registration.ts +3 -2
- package/src/create-stateful-transport.ts +15 -2
- package/src/execution-ui.ts +0 -72
- package/src/in-process-transport.ts +39 -7
- package/src/inspect-tool.ts +3 -1
- package/src/peer-communication.ts +352 -0
- package/src/peer-transport.ts +49 -0
- package/src/persistence.ts +26 -1
- package/src/pi-args.ts +2 -0
- package/src/registry-types.ts +7 -0
- package/src/registry.ts +240 -41
- package/src/result-contract.ts +20 -5
- package/src/rpc-transport.ts +56 -26
- package/src/runner.ts +13 -1
- package/src/spawn-idempotency.ts +2 -0
- package/src/stateful-agent-view.ts +3 -1
- package/src/stateful-guidance.ts +11 -11
- package/src/stateful-safety.ts +0 -45
- package/src/stateful-tool-params.ts +11 -3
- package/src/stateful.ts +119 -47
- package/src/subagents.ts +6 -8
- package/src/subprocess-transport.ts +49 -28
- package/src/task-path.ts +65 -0
- package/src/transport-ui.ts +0 -6
- package/src/transport.ts +2 -1
- package/src/workflow-ui.ts +4 -4
- package/src/automation-contract.ts +0 -709
- package/src/automation-planner.ts +0 -65
- package/src/automation-registration.ts +0 -137
- package/src/automation-tool.ts +0 -40
- package/src/automation.ts +0 -435
- package/src/execution-profiles.ts +0 -95
- package/src/workflow-plan-compiler.ts +0 -618
- package/src/workflow-plan-patch.ts +0 -636
- package/src/workflow-planning-benchmark.ts +0 -95
package/src/rpc-transport.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import type { RpcSessionState } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { discoverAgents } from "./agents/discovery.js";
|
|
7
7
|
import type { AgentConfig, SubagentSettings } from "./agents/types.js";
|
|
8
|
+
import { formatPeerMessage } from "./child-peer-tools.js";
|
|
8
9
|
import {
|
|
9
10
|
buildCurrentTurnPrompt,
|
|
10
11
|
type ParentRuntimeSnapshot,
|
|
@@ -16,10 +17,16 @@ import {
|
|
|
16
17
|
DEFAULT_MAX_STDERR_BYTES,
|
|
17
18
|
truncateUtf8,
|
|
18
19
|
} from "./limits.js";
|
|
20
|
+
import {
|
|
21
|
+
CHILD_PEER_TOOL_NAMES,
|
|
22
|
+
childPeerBridgePath,
|
|
23
|
+
type PeerTransportRuntime,
|
|
24
|
+
peerBridgeEnvironment,
|
|
25
|
+
} from "./peer-transport.js";
|
|
19
26
|
import { type PiInvocation, resolvePiInvocation } from "./pi-invocation.js";
|
|
20
27
|
import { resolvePiPromptResources } from "./prompt-resources.js";
|
|
21
28
|
import { JsonLineDecoder } from "./protocol.js";
|
|
22
|
-
import type { ManagedAgent, TurnOutcome } from "./registry.js";
|
|
29
|
+
import type { AgentMailboxMessage, ManagedAgent, TurnOutcome } from "./registry.js";
|
|
23
30
|
import { finalizeTimedOutRpcTurn } from "./rpc-timeout-finalization.js";
|
|
24
31
|
import {
|
|
25
32
|
boundedError,
|
|
@@ -197,6 +204,10 @@ export class RpcProtocolClient {
|
|
|
197
204
|
await this.request({ type: "prompt", message }, timeoutMs);
|
|
198
205
|
}
|
|
199
206
|
|
|
207
|
+
async steer(message: string, timeoutMs?: number): Promise<void> {
|
|
208
|
+
await this.request({ type: "steer", message }, timeoutMs);
|
|
209
|
+
}
|
|
210
|
+
|
|
200
211
|
async abort(): Promise<void> {
|
|
201
212
|
await this.request(
|
|
202
213
|
{ type: "abort" },
|
|
@@ -365,6 +376,7 @@ export interface RpcTransportOptions {
|
|
|
365
376
|
defaultTimeoutMs?: number;
|
|
366
377
|
abortGraceMs?: number;
|
|
367
378
|
timeoutFinalizationMs?: number;
|
|
379
|
+
peerRuntime?: PeerTransportRuntime;
|
|
368
380
|
}
|
|
369
381
|
|
|
370
382
|
interface RpcChildRecord {
|
|
@@ -694,6 +706,13 @@ export class RpcTransport implements SubagentTransport {
|
|
|
694
706
|
}
|
|
695
707
|
}
|
|
696
708
|
|
|
709
|
+
async deliverMessage(agent: ManagedAgent, message: AgentMailboxMessage): Promise<boolean> {
|
|
710
|
+
const child = this.children.get(agent.id);
|
|
711
|
+
if (!child) return false;
|
|
712
|
+
await child.client.steer(formatPeerMessage(message));
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
|
|
697
716
|
async release(agent: ManagedAgent): Promise<void> {
|
|
698
717
|
await this.releaseById(agent.id);
|
|
699
718
|
}
|
|
@@ -732,41 +751,48 @@ export class RpcTransport implements SubagentTransport {
|
|
|
732
751
|
cleanupRolePrompt(temporaryPrompt);
|
|
733
752
|
throw abortError("RPC subagent start aborted");
|
|
734
753
|
}
|
|
735
|
-
|
|
736
|
-
cwd: agent.cwd,
|
|
737
|
-
abortTimeoutMs: this.options.abortGraceMs ?? ABORT_GRACE_MS,
|
|
738
|
-
terminationGraceMs: this.options.abortGraceMs ?? ABORT_GRACE_MS,
|
|
739
|
-
args: buildRpcArgs(
|
|
740
|
-
agent,
|
|
741
|
-
agentConfig,
|
|
742
|
-
this.options.getParentRuntime(),
|
|
743
|
-
temporaryPrompt?.filePath,
|
|
744
|
-
resources.appendSystemPromptPaths,
|
|
745
|
-
),
|
|
746
|
-
env: {
|
|
747
|
-
PI_SUBAGENT_DEPTH: String(
|
|
748
|
-
(Number.parseInt(process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1,
|
|
749
|
-
),
|
|
750
|
-
PI_SUBAGENT_RPC_PROTOCOL: PI_SUBAGENTS_RPC_PROTOCOL,
|
|
751
|
-
},
|
|
752
|
-
});
|
|
754
|
+
let client: RpcProtocolClient | undefined;
|
|
753
755
|
try {
|
|
756
|
+
const credentials = this.options.peerRuntime
|
|
757
|
+
? await this.options.peerRuntime.issueCredentials(
|
|
758
|
+
agent.id,
|
|
759
|
+
agent.currentTurnGeneration ?? agent.turnGeneration ?? 1,
|
|
760
|
+
)
|
|
761
|
+
: undefined;
|
|
762
|
+
if (signal.aborted) throw abortError("RPC subagent start aborted");
|
|
763
|
+
client = this.createClient({
|
|
764
|
+
cwd: agent.cwd,
|
|
765
|
+
abortTimeoutMs: this.options.abortGraceMs ?? ABORT_GRACE_MS,
|
|
766
|
+
terminationGraceMs: this.options.abortGraceMs ?? ABORT_GRACE_MS,
|
|
767
|
+
args: buildRpcArgs(
|
|
768
|
+
agent,
|
|
769
|
+
agentConfig,
|
|
770
|
+
this.options.getParentRuntime(),
|
|
771
|
+
temporaryPrompt?.filePath,
|
|
772
|
+
resources.appendSystemPromptPaths,
|
|
773
|
+
credentials !== undefined,
|
|
774
|
+
),
|
|
775
|
+
env: {
|
|
776
|
+
PI_SUBAGENT_DEPTH: String(
|
|
777
|
+
(Number.parseInt(process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1,
|
|
778
|
+
),
|
|
779
|
+
PI_SUBAGENT_RPC_PROTOCOL: PI_SUBAGENTS_RPC_PROTOCOL,
|
|
780
|
+
...(credentials ? peerBridgeEnvironment(credentials) : {}),
|
|
781
|
+
},
|
|
782
|
+
});
|
|
754
783
|
const snapshot = await client.start(signal);
|
|
784
|
+
if (signal.aborted) throw abortError("RPC subagent start aborted");
|
|
755
785
|
const record: RpcChildRecord = {
|
|
756
786
|
client,
|
|
757
787
|
state: snapshot.state,
|
|
758
788
|
started: false,
|
|
759
789
|
temporaryPrompt,
|
|
760
790
|
};
|
|
761
|
-
if (signal.aborted) {
|
|
762
|
-
await client.stop();
|
|
763
|
-
cleanupRolePrompt(temporaryPrompt);
|
|
764
|
-
throw abortError("RPC subagent start aborted");
|
|
765
|
-
}
|
|
766
791
|
this.children.set(agent.id, record);
|
|
767
792
|
return record;
|
|
768
793
|
} catch (error) {
|
|
769
|
-
await client
|
|
794
|
+
await client?.stop().catch(() => undefined);
|
|
795
|
+
this.options.peerRuntime?.revoke(agent.id);
|
|
770
796
|
cleanupRolePrompt(temporaryPrompt);
|
|
771
797
|
throw error;
|
|
772
798
|
}
|
|
@@ -776,6 +802,7 @@ export class RpcTransport implements SubagentTransport {
|
|
|
776
802
|
const child = this.children.get(agentId);
|
|
777
803
|
if (!child) return;
|
|
778
804
|
this.children.delete(agentId);
|
|
805
|
+
this.options.peerRuntime?.revoke(agentId);
|
|
779
806
|
try {
|
|
780
807
|
await child.client.abort().catch(() => undefined);
|
|
781
808
|
await child.client.stop();
|
|
@@ -799,8 +826,10 @@ export function buildRpcArgs(
|
|
|
799
826
|
parentRuntime: ParentRuntimeSnapshot,
|
|
800
827
|
rolePromptPath?: string,
|
|
801
828
|
appendSystemPromptPaths: readonly string[] = [],
|
|
829
|
+
peerBridge = false,
|
|
802
830
|
): string[] {
|
|
803
831
|
const args = ["--mode", "rpc", "--no-session", "--no-extensions"];
|
|
832
|
+
if (peerBridge) args.push("-e", childPeerBridgePath());
|
|
804
833
|
const model =
|
|
805
834
|
agentConfig.model ??
|
|
806
835
|
(parentRuntime.model ? `${parentRuntime.model.provider}/${parentRuntime.model.id}` : undefined);
|
|
@@ -816,7 +845,8 @@ export function buildRpcArgs(
|
|
|
816
845
|
if (agent.target?.trust.projectTrusted ?? false) args.push("--approve");
|
|
817
846
|
else args.push("--no-approve");
|
|
818
847
|
if (Array.isArray(agentConfig.tools)) {
|
|
819
|
-
|
|
848
|
+
const tools = [...agentConfig.tools, ...(peerBridge ? CHILD_PEER_TOOL_NAMES : [])];
|
|
849
|
+
if (tools.length > 0) args.push("--tools", [...new Set(tools)].join(","));
|
|
820
850
|
else args.push("--no-tools");
|
|
821
851
|
}
|
|
822
852
|
for (const promptPath of appendSystemPromptPaths) {
|
package/src/runner.ts
CHANGED
|
@@ -389,6 +389,12 @@ export interface ChildLaunchPolicy {
|
|
|
389
389
|
projectTrust?: boolean;
|
|
390
390
|
baseSystemPrompt?: string;
|
|
391
391
|
appendSystemPromptPaths?: string[];
|
|
392
|
+
/** Package-owned explicit child extensions loaded even when unrelated extensions are disabled. */
|
|
393
|
+
extensionPaths?: string[];
|
|
394
|
+
/** Package-owned tools added to the child allowlist without changing delegated execution tools. */
|
|
395
|
+
additionalTools?: string[];
|
|
396
|
+
/** Ephemeral child-process environment consumed and cleared by a package-owned bridge. */
|
|
397
|
+
env?: NodeJS.ProcessEnv;
|
|
392
398
|
/** Internal timeout recovery control; omitted means enabled. */
|
|
393
399
|
finalizeOnTimeout?: boolean;
|
|
394
400
|
/** Internal hard deadline for the summary attempt. */
|
|
@@ -560,10 +566,14 @@ export async function runSingleAgent(
|
|
|
560
566
|
|
|
561
567
|
const effectiveTools =
|
|
562
568
|
launchPolicy && Object.hasOwn(launchPolicy, "tools") ? launchPolicy.tools : agent.tools;
|
|
569
|
+
const selectedTools =
|
|
570
|
+
effectiveTools === undefined
|
|
571
|
+
? undefined
|
|
572
|
+
: [...new Set([...effectiveTools, ...(launchPolicy?.additionalTools ?? [])])];
|
|
563
573
|
const args = buildPiArgs({
|
|
564
574
|
model: agent.model,
|
|
565
575
|
thinkingLevel,
|
|
566
|
-
tools:
|
|
576
|
+
tools: selectedTools,
|
|
567
577
|
disableExtensions: launchPolicy?.disableExtensions,
|
|
568
578
|
disableSkills: launchPolicy?.disableSkills,
|
|
569
579
|
disablePromptTemplates: launchPolicy?.disablePromptTemplates,
|
|
@@ -571,6 +581,7 @@ export async function runSingleAgent(
|
|
|
571
581
|
projectTrust: launchPolicy?.projectTrust,
|
|
572
582
|
baseSystemPromptPath: baseSystemPromptPath ?? undefined,
|
|
573
583
|
appendSystemPromptPaths: launchPolicy?.appendSystemPromptPaths,
|
|
584
|
+
extensionPaths: launchPolicy?.extensionPaths,
|
|
574
585
|
systemPromptPath: tmpPromptPath ?? undefined,
|
|
575
586
|
task,
|
|
576
587
|
});
|
|
@@ -623,6 +634,7 @@ export async function runSingleAgent(
|
|
|
623
634
|
stdio: ["ignore", "pipe", "pipe"],
|
|
624
635
|
env: {
|
|
625
636
|
...process.env,
|
|
637
|
+
...launchPolicy?.env,
|
|
626
638
|
PI_SUBAGENT_DEPTH: String(
|
|
627
639
|
(Number.parseInt(process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1,
|
|
628
640
|
),
|
package/src/spawn-idempotency.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const MAX_SPAWN_IDEMPOTENCY_KEY_LENGTH = 256;
|
|
|
7
7
|
|
|
8
8
|
export interface CanonicalSpawnRequest {
|
|
9
9
|
agent: string;
|
|
10
|
+
taskName?: string;
|
|
10
11
|
task: string;
|
|
11
12
|
cwd: string;
|
|
12
13
|
agentScope: AgentScope;
|
|
@@ -29,6 +30,7 @@ export function hashSpawnRequest(request: CanonicalSpawnRequest): string {
|
|
|
29
30
|
.update(
|
|
30
31
|
JSON.stringify({
|
|
31
32
|
agent: request.agent,
|
|
33
|
+
...(request.taskName === undefined ? {} : { taskName: request.taskName }),
|
|
32
34
|
task: request.task,
|
|
33
35
|
cwd: request.cwd,
|
|
34
36
|
agentScope: request.agentScope,
|
|
@@ -24,12 +24,14 @@ export function formatStatefulAgentLine(agent: ManagedAgent, now = Date.now()):
|
|
|
24
24
|
const transport = agent.telemetry?.transport ? ` transport:${agent.telemetry.transport}` : "";
|
|
25
25
|
const phase = agent.telemetry?.phase ? ` phase:${agent.telemetry.phase}` : "";
|
|
26
26
|
const queued = agent.telemetry?.queuePosition ? ` queue:${agent.telemetry.queuePosition}` : "";
|
|
27
|
-
return `${indent}${sanitizeStatusLine(agent.id, 128)} ${sanitizeStatusLine(agent.agent, 128)} ${agent.state} ${elapsedSeconds}s${thinking}${timeoutText}${idleText}${turnsText}${toolsText}${transport}${phase}${queued} unread:${unread} [${actions}]${task}`;
|
|
27
|
+
return `${indent}${sanitizeStatusLine(agent.taskPath ?? agent.id, 256)} (${sanitizeStatusLine(agent.id, 128)}) ${sanitizeStatusLine(agent.agent, 128)} ${agent.state} ${elapsedSeconds}s${thinking}${timeoutText}${idleText}${turnsText}${toolsText}${transport}${phase}${queued} unread:${unread} [${actions}]${task}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function summarizeStatefulAgent(agent: ManagedAgent) {
|
|
31
31
|
return {
|
|
32
32
|
id: agent.id,
|
|
33
|
+
taskName: agent.taskName,
|
|
34
|
+
taskPath: agent.taskPath,
|
|
33
35
|
agent: agent.agent,
|
|
34
36
|
parentId: agent.parentId,
|
|
35
37
|
rootId: agent.rootId,
|
package/src/stateful-guidance.ts
CHANGED
|
@@ -7,29 +7,29 @@ export function createSpawnPromptGuidelines(
|
|
|
7
7
|
const deliveryGuidance =
|
|
8
8
|
completionDelivery === "auto-resume"
|
|
9
9
|
? blockingEnabled
|
|
10
|
-
? "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or review that covers related branches even when the final answer depends on its result; do not choose blocking parallel fan-out merely to keep delegation in the same turn."
|
|
11
|
-
: "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or review that covers related branches even when the final answer depends on its result."
|
|
10
|
+
? "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or consequential independent review that covers related branches even when the final answer depends on its result; do not choose blocking parallel fan-out merely to keep delegation in the same turn."
|
|
11
|
+
: "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or consequential independent review that covers related branches even when the final answer depends on its result."
|
|
12
12
|
: blockingEnabled
|
|
13
|
-
? "With subagent_spawn completion delivery set to next-turn (the default), prefer one subagent_spawn for broad asynchronous research or review only when the current response does not depend on its result; use the blocking subagent when the final answer depends on the detached result."
|
|
13
|
+
? "With subagent_spawn completion delivery set to next-turn (the default), prefer one subagent_spawn for broad asynchronous research or consequential independent review only when the current response does not depend on its result; use the blocking subagent when the final answer depends on the detached result."
|
|
14
14
|
: "With subagent_spawn completion delivery set to next-turn (the default), use subagent_spawn only when the current response does not depend on its result; complete final-answer-dependent work directly because an idle root is not awakened.";
|
|
15
|
-
const noLocalWorkGuidance =
|
|
16
|
-
completionDelivery === "auto-resume"
|
|
17
|
-
? "After subagent_spawn returns, do useful non-overlapping local work immediately. If none remains, briefly tell the user what subagent_spawn launched and end the response; auto-resume will request a synthesis turn after completion."
|
|
18
|
-
: "After subagent_spawn returns, do useful non-overlapping local work immediately. If none remains, briefly tell the user what subagent_spawn launched and end the response only when the current response does not depend on its result; next-turn delivery will not wake an idle root.";
|
|
19
15
|
return [
|
|
20
|
-
"Do not use subagent_spawn for simple or critical-path work that the main agent can perform directly.",
|
|
16
|
+
"Do not use subagent_spawn for simple or critical-path work that the main agent can perform directly. The main agent retains overall planning, immediate critical-path work, integration, final verification, and the final answer.",
|
|
17
|
+
"Before one ordinary subagent_spawn, identify concrete useful non-overlapping main-agent work you can start immediately and a supported completion integration path. If none exists, perform the task directly instead of calling subagent_spawn.",
|
|
18
|
+
"Give subagent_spawn a concise unique taskName using lowercase letters, digits, and underscores so the retained agent has a stable canonical task path.",
|
|
21
19
|
"Set subagent_spawn thinkingLevel to the lowest sufficient thinking level for the delegated task: use off or minimal for extraction, formatting, or mechanical work; low for straightforward bounded work; medium for ordinary multi-step research or implementation; high for complex debugging, design, review, or cross-file analysis; xhigh for highly ambiguous, cross-system, or high-risk analysis; and max only for the hardest tasks when quality clearly outweighs latency and cost. Omit subagent_spawn thinkingLevel only to preserve the agent or child default.",
|
|
22
20
|
"Set subagent_spawn timeoutMs to the shortest realistic work deadline for the task difficulty; use idleTimeoutMs for stalled work and maxTurns or maxToolCalls to stop repeated work without progress. Split oversized tasks instead of extending budgets merely to compensate for broad scope. Omit these fields only to preserve the retained agent or configured defaults.",
|
|
23
21
|
deliveryGuidance,
|
|
24
|
-
"
|
|
22
|
+
"Keep ordinary review in the main agent with a review skill and deterministic checks; use subagent_spawn for detached review only when consequential independent verification has concrete parallel value.",
|
|
23
|
+
"Use a single subagent_spawn for a bounded implementation slice with clear ownership only when it can run beside the identified main-agent work.",
|
|
24
|
+
"Use a single subagent_spawn without concurrent main-agent work only for an explicit user-requested specialist model, tool profile, or isolation boundary.",
|
|
25
25
|
...(blockingEnabled
|
|
26
26
|
? [
|
|
27
27
|
"Use the blocking subagent instead of subagent_spawn when synchronous output is required before the main agent can continue and waiting is intentional; queued steering cannot be processed until that blocking call returns.",
|
|
28
28
|
"When subagent_spawn fits the completion-delivery policy, do not choose a blocking parallel subagent merely to keep delegation in the same turn.",
|
|
29
29
|
]
|
|
30
30
|
: []),
|
|
31
|
-
"Add another subagent_spawn only for truly independent work with safe workspace concurrency.",
|
|
32
|
-
|
|
31
|
+
"Add another subagent_spawn only for truly independent work with safe workspace concurrency and disjoint write ownership; shared workspaces permit concurrent writes by default, so use workspaceMode worktree when repository isolation is required. The main agent still owns integration.",
|
|
32
|
+
"After subagent_spawn returns, immediately continue the identified local task; do not merely announce the spawn, wait, poll, or end the response while useful local work remains.",
|
|
33
33
|
'Consume and synthesize available subagent_spawn completion messages; use subagent_manage with action "interrupt" or "close" for agents that are no longer needed.',
|
|
34
34
|
'Completion from subagent_spawn is delivered automatically. Do not poll with subagent_inspect or subagent_mailbox action "read", repeatedly check progress, or duplicate the delegated work.',
|
|
35
35
|
];
|
package/src/stateful-safety.ts
CHANGED
|
@@ -3,54 +3,9 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { discoverAgents } from "./agents/discovery.js";
|
|
5
5
|
import type { AgentScope, SubagentSettings } from "./agents/types.js";
|
|
6
|
-
import type { AgentRegistry, ManagedAgent } from "./registry.js";
|
|
7
6
|
import { safeTerminalLine } from "./safe-text.js";
|
|
8
7
|
import { readSubagentSettings } from "./settings.js";
|
|
9
8
|
|
|
10
|
-
export function assertNoSharedWriteConflict(
|
|
11
|
-
registry: AgentRegistry,
|
|
12
|
-
agentName: string,
|
|
13
|
-
cwd: string,
|
|
14
|
-
scope: AgentScope,
|
|
15
|
-
settings?: SubagentSettings,
|
|
16
|
-
): void {
|
|
17
|
-
const agents = discoverAgents(cwd, scope, settings ?? readSubagentSettings()).agents;
|
|
18
|
-
const requested = agents.find((agent) => agent.name === agentName);
|
|
19
|
-
if (!isWriteCapable(requested?.tools)) return;
|
|
20
|
-
for (const active of registry.list()) {
|
|
21
|
-
if (
|
|
22
|
-
!isSameCwd(active.cwd, cwd) ||
|
|
23
|
-
(active.state !== "running" && active.state !== "starting")
|
|
24
|
-
) {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
const activeConfig = agents.find((agent) => agent.name === active.agent);
|
|
28
|
-
if (isWriteCapable(activeConfig?.tools)) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
`Write-capable subagent ${active.id} is already active in shared workspace ${cwd}. ` +
|
|
31
|
-
"Prefer one subagent_spawn covering combined asynchronous work. Use the blocking subagent parallel mode only when concurrent synchronous outputs justify making the main agent unavailable. Otherwise let the active agent finish or close it; set allowConcurrentWrites only when overlapping writes are knowingly safe, or use workspaceMode worktree when repository isolation is needed.",
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function assertFollowUpWriteAllowed(
|
|
38
|
-
registry: AgentRegistry,
|
|
39
|
-
agent: ManagedAgent,
|
|
40
|
-
allowConcurrentWrites: boolean,
|
|
41
|
-
isolatedWorkspace: boolean,
|
|
42
|
-
settings?: SubagentSettings,
|
|
43
|
-
): void {
|
|
44
|
-
if (allowConcurrentWrites || isolatedWorkspace) return;
|
|
45
|
-
assertNoSharedWriteConflict(
|
|
46
|
-
registry,
|
|
47
|
-
agent.agent,
|
|
48
|
-
agent.cwd,
|
|
49
|
-
agent.agentScope ?? "user",
|
|
50
|
-
settings,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
9
|
export function isWriteCapable(tools: string[] | undefined): boolean {
|
|
55
10
|
if (!tools) return true;
|
|
56
11
|
return tools.some((tool) => ["bash", "write", "edit"].includes(tool));
|
|
@@ -11,7 +11,10 @@ export const ManageParamsSchema = Type.Object(
|
|
|
11
11
|
description: "Interrupt active work or close an agent and release its resources.",
|
|
12
12
|
}),
|
|
13
13
|
agentId: Type.Optional(
|
|
14
|
-
Type.String({
|
|
14
|
+
Type.String({
|
|
15
|
+
minLength: 1,
|
|
16
|
+
description: "Required agent ID or canonical task path for interrupt and close.",
|
|
17
|
+
}),
|
|
15
18
|
),
|
|
16
19
|
subtree: Type.Optional(
|
|
17
20
|
Type.Boolean({
|
|
@@ -28,7 +31,10 @@ export const MailboxParamsSchema = Type.Object(
|
|
|
28
31
|
action: StringEnum(MAILBOX_ACTIONS, {
|
|
29
32
|
description: "Use send for queue-only delivery or read to inspect unread mailbox messages.",
|
|
30
33
|
}),
|
|
31
|
-
agentId: Type.String({
|
|
34
|
+
agentId: Type.String({
|
|
35
|
+
minLength: 1,
|
|
36
|
+
description: "Mailbox owner or recipient agent ID or canonical task path.",
|
|
37
|
+
}),
|
|
32
38
|
message: Type.Optional(
|
|
33
39
|
Type.String({
|
|
34
40
|
minLength: 1,
|
|
@@ -36,7 +42,9 @@ export const MailboxParamsSchema = Type.Object(
|
|
|
36
42
|
description: "Message content required by send; sending does not start a turn.",
|
|
37
43
|
}),
|
|
38
44
|
),
|
|
39
|
-
senderId: Type.Optional(
|
|
45
|
+
senderId: Type.Optional(
|
|
46
|
+
Type.String({ description: "Optional sender agent ID or canonical task path." }),
|
|
47
|
+
),
|
|
40
48
|
deduplicationKey: Type.Optional(
|
|
41
49
|
Type.String({ maxLength: 256, description: "Optional idempotency key for send." }),
|
|
42
50
|
),
|