@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.
Files changed (47) hide show
  1. package/README.md +198 -188
  2. package/package.json +2 -2
  3. package/src/agents/built-ins.ts +13 -66
  4. package/src/agents/catalog.ts +19 -2
  5. package/src/agents/discovery.ts +31 -15
  6. package/src/auto-transport.ts +7 -1
  7. package/src/child-peer-bridge.ts +124 -0
  8. package/src/child-peer-tools.ts +132 -0
  9. package/src/completion-delivery.ts +19 -5
  10. package/src/completion-render.ts +189 -0
  11. package/src/completion-routing.ts +24 -0
  12. package/src/config-ui.ts +11 -17
  13. package/src/consult-registration.ts +3 -2
  14. package/src/create-stateful-transport.ts +15 -2
  15. package/src/execution-ui.ts +0 -72
  16. package/src/in-process-transport.ts +39 -7
  17. package/src/inspect-tool.ts +3 -1
  18. package/src/peer-communication.ts +352 -0
  19. package/src/peer-transport.ts +49 -0
  20. package/src/persistence.ts +26 -1
  21. package/src/pi-args.ts +2 -0
  22. package/src/registry-types.ts +7 -0
  23. package/src/registry.ts +240 -41
  24. package/src/result-contract.ts +20 -5
  25. package/src/rpc-transport.ts +56 -26
  26. package/src/runner.ts +13 -1
  27. package/src/spawn-idempotency.ts +2 -0
  28. package/src/stateful-agent-view.ts +3 -1
  29. package/src/stateful-guidance.ts +11 -11
  30. package/src/stateful-safety.ts +0 -45
  31. package/src/stateful-tool-params.ts +11 -3
  32. package/src/stateful.ts +119 -47
  33. package/src/subagents.ts +6 -8
  34. package/src/subprocess-transport.ts +49 -28
  35. package/src/task-path.ts +65 -0
  36. package/src/transport-ui.ts +0 -6
  37. package/src/transport.ts +2 -1
  38. package/src/workflow-ui.ts +4 -4
  39. package/src/automation-contract.ts +0 -709
  40. package/src/automation-planner.ts +0 -65
  41. package/src/automation-registration.ts +0 -137
  42. package/src/automation-tool.ts +0 -40
  43. package/src/automation.ts +0 -435
  44. package/src/execution-profiles.ts +0 -95
  45. package/src/workflow-plan-compiler.ts +0 -618
  46. package/src/workflow-plan-patch.ts +0 -636
  47. package/src/workflow-planning-benchmark.ts +0 -95
@@ -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
- const client = this.createClient({
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.stop().catch(() => undefined);
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
- if (agentConfig.tools.length > 0) args.push("--tools", agentConfig.tools.join(","));
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: effectiveTools,
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
  ),
@@ -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,
@@ -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
- "Use a single subagent_spawn only for a concrete bounded subtask that can run independently and has an isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation.",
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
- noLocalWorkGuidance,
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
  ];
@@ -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({ minLength: 1, description: "Required for interrupt and close." }),
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({ minLength: 1, description: "Mailbox owner or message recipient." }),
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(Type.String({ description: "Optional same-tree sender identity." })),
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
  ),