pi-subagents 0.43.0 → 0.45.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.
@@ -36,6 +36,7 @@ import {
36
36
  getStepAgents,
37
37
  isParallelStep,
38
38
  isDynamicParallelStep,
39
+ resolveChainPath,
39
40
  resolveStepBehavior,
40
41
  suppressProgressForReadOnlyTask,
41
42
  taskDisallowsFileUpdates,
@@ -91,11 +92,12 @@ import { inspectSubagentStatus } from "../background/run-status.ts";
91
92
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
92
93
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
93
94
  import { attachMissionToLaunchResult, prepareMissionLaunch, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
95
+ import { updateMission } from "../../missions/store.ts";
94
96
  import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
95
97
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
96
98
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
97
99
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
98
- import { runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
100
+ import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
99
101
  import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
100
102
  import {
101
103
  cleanupWorktrees,
@@ -255,6 +257,8 @@ export interface SubagentParamsLike {
255
257
  focus?: boolean;
256
258
  skill?: string | string[] | boolean;
257
259
  output?: string | boolean;
260
+ /** Internal-only; not part of the public tool schema. Wired for single-run reads (chain steps use their own field). */
261
+ reads?: string[] | false;
258
262
  outputMode?: "inline" | "file-only";
259
263
  outputSchema?: JsonSchemaObject;
260
264
  agentScope?: unknown;
@@ -798,6 +802,20 @@ function getAsyncInterruptTarget(
798
802
  return newest ? { asyncId: newest.asyncId, asyncDir: newest.asyncDir } : undefined;
799
803
  }
800
804
 
805
+ function isStaleExtensionContextError(error: unknown): boolean {
806
+ if (!(error instanceof Error)) return false;
807
+ return /extension ctx is stale|stale after session replacement or reload/i.test(error.message);
808
+ }
809
+
810
+ function emitAdvisoryControlEvent(pi: ExtensionAPI, channel: string, payload: unknown): void {
811
+ try {
812
+ pi.events.emit(channel, payload);
813
+ } catch (error) {
814
+ if (isStaleExtensionContextError(error)) return;
815
+ throw error;
816
+ }
817
+ }
818
+
801
819
  function emitControlNotification(input: {
802
820
  pi: ExtensionAPI;
803
821
  controlConfig: ResolvedControlConfig;
@@ -815,10 +833,10 @@ function emitControlNotification(input: {
815
833
  noticeText: formatControlNoticeMessage(input.event, childIntercomTarget),
816
834
  };
817
835
  if (input.controlConfig.notifyChannels.includes("event")) {
818
- input.pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
836
+ emitAdvisoryControlEvent(input.pi, SUBAGENT_CONTROL_EVENT, payload);
819
837
  }
820
838
  if (input.event.type !== "active_long_running" && input.controlConfig.notifyChannels.includes("intercom") && input.intercomBridge.active && input.intercomBridge.orchestratorTarget) {
821
- input.pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
839
+ emitAdvisoryControlEvent(input.pi, SUBAGENT_CONTROL_INTERCOM_EVENT, {
822
840
  ...payload,
823
841
  to: input.intercomBridge.orchestratorTarget,
824
842
  message: formatControlIntercomMessage(input.event, childIntercomTarget),
@@ -1865,6 +1883,19 @@ function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1865
1883
  return "single";
1866
1884
  }
1867
1885
 
1886
+ function formatStatusTargetLabel(params: Pick<SubagentParamsLike, "dir" | "index" | "view">, targetRunId: string | undefined): string {
1887
+ let target: string;
1888
+ if (targetRunId) {
1889
+ target = `run ${targetRunId}`;
1890
+ } else if (params.dir) {
1891
+ target = `dir ${params.dir}`;
1892
+ } else {
1893
+ target = params.view === "transcript" ? "active run" : "active runs";
1894
+ }
1895
+ if (params.view !== "transcript") return `Status target: ${target}`;
1896
+ return `Transcript target: ${target}${params.index !== undefined ? ` · child ${params.index}` : ""}`;
1897
+ }
1898
+
1868
1899
  interface AgentDefaultContextPolicy {
1869
1900
  params: SubagentParamsLike;
1870
1901
  contextForAgent(agentName: string): ContextMode;
@@ -2534,6 +2565,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2534
2565
  skills,
2535
2566
  output: effectiveOutput,
2536
2567
  outputMode: effectiveOutputMode,
2568
+ ...(params.reads !== undefined ? { reads: params.reads } : {}),
2537
2569
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2538
2570
  modelOverride,
2539
2571
  thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
@@ -3323,10 +3355,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3323
3355
  );
3324
3356
  if (errorResult) return errorResult;
3325
3357
 
3326
- let worktreeFinalized = false;
3358
+ let worktreeCleanupHandled = false;
3359
+ let pendingHandoff: Details["parallelHandoff"];
3327
3360
  try {
3328
3361
  if (worktreeSetup) {
3329
- writePendingParallelHandoff({
3362
+ pendingHandoff = writePendingParallelHandoff({
3330
3363
  manifestPath: parallelHandoffPath(artifactsDir, runId),
3331
3364
  runId,
3332
3365
  mode: "parallel",
@@ -3429,10 +3462,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3429
3462
  updateForegroundNestedProjection(foregroundControl);
3430
3463
  attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
3431
3464
  }
3465
+ const detached = results.find((result) => result.detached);
3432
3466
  let handoff: ReturnType<typeof finalizeParallelWorktreeHandoff> | undefined;
3433
3467
  if (worktreeSetup) {
3434
- worktreeFinalized = true;
3435
- handoff = finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
3468
+ worktreeCleanupHandled = true;
3469
+ handoff = detached
3470
+ ? { suffix: pendingHandoff ? formatParallelHandoffReference(pendingHandoff) : "", reference: pendingHandoff }
3471
+ : finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
3436
3472
  }
3437
3473
  const interrupted = results.find((result) => result.interrupted);
3438
3474
  const totalCost = sumResultsCost(results);
@@ -3455,11 +3491,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3455
3491
  details,
3456
3492
  };
3457
3493
  }
3458
- const detachedIndex = results.findIndex((result) => result.detached);
3459
- const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
3460
3494
  if (detached) {
3495
+ const handoffSuffix = handoff?.suffix ? `\n\n${handoff.suffix}` : "";
3461
3496
  return {
3462
- content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }],
3497
+ content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.${handoffSuffix}` }],
3463
3498
  details,
3464
3499
  };
3465
3500
  }
@@ -3508,7 +3543,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3508
3543
  details,
3509
3544
  };
3510
3545
  } finally {
3511
- if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
3546
+ if (worktreeSetup && !worktreeCleanupHandled) cleanupWorktrees(worktreeSetup);
3512
3547
  }
3513
3548
  }
3514
3549
 
@@ -3559,6 +3594,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3559
3594
  data.modelScope === undefined ? {} : { scope: data.modelScope },
3560
3595
  );
3561
3596
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
3597
+ let readsOverride: string[] | false | undefined = params.reads;
3562
3598
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
3563
3599
  let effectiveOutput = normalizeSingleOutputOverride(rawOutput, agentConfig.output);
3564
3600
  const effectiveOutputMode = params.outputMode ?? "inline";
@@ -3596,6 +3632,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3596
3632
  if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3597
3633
  if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
3598
3634
  if (override?.skills !== undefined) skillOverride = override.skills;
3635
+ if (override?.reads !== undefined) readsOverride = override.reads;
3599
3636
 
3600
3637
  if (result.runInBackground) {
3601
3638
  if (!isAsyncAvailable()) {
@@ -3635,6 +3672,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3635
3672
  skills: skillOverride === false ? [] : skillOverride,
3636
3673
  output: effectiveOutput,
3637
3674
  outputMode: effectiveOutputMode,
3675
+ ...(readsOverride !== undefined ? { reads: readsOverride } : {}),
3638
3676
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
3639
3677
  modelOverride,
3640
3678
  thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
@@ -3671,6 +3709,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3671
3709
  const structuredRuntime = params.outputSchema
3672
3710
  ? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined)
3673
3711
  : undefined;
3712
+ // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
3713
+ // absolute paths pass through; relative paths resolve against the child cwd.
3714
+ const reads = readsOverride !== undefined ? readsOverride : agentConfig.defaultReads ?? false;
3715
+ const readsInstruction = Array.isArray(reads) && reads.length > 0
3716
+ ? `[Read from: ${reads.map((f) => resolveChainPath(f, effectiveCwd)).join(", ")}]\n\n`
3717
+ : "";
3718
+ task = readsInstruction + task;
3674
3719
  task = injectSingleOutputInstruction(task, outputPath, agentConfig);
3675
3720
 
3676
3721
  let effectiveSkills: string[] | undefined;
@@ -3895,6 +3940,8 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
3895
3940
  const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
3896
3941
  ? result.details.results[0].finalOutput
3897
3942
  : receiptOutput;
3943
+ const detached = result.details.results.some((child) => child.detached);
3944
+ const ok = result.isError !== true && !detached;
3898
3945
  const artifactPaths = new Set<string>();
3899
3946
  if (result.details.asyncDir) artifactPaths.add(result.details.asyncDir);
3900
3947
  if (result.details.parallelHandoff?.path) artifactPaths.add(result.details.parallelHandoff.path);
@@ -3906,10 +3953,10 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
3906
3953
  const structured = result.details.results.map((child) => child.structuredOutput).filter((value) => value !== undefined);
3907
3954
  return {
3908
3955
  key,
3909
- ok: result.isError !== true,
3956
+ ok,
3910
3957
  ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
3911
3958
  output,
3912
- ...(result.isError === true ? { error: receiptOutput || output || "Child run failed." } : {}),
3959
+ ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
3913
3960
  ...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
3914
3961
  artifactPaths: [...artifactPaths],
3915
3962
  results: result.details.results,
@@ -3983,6 +4030,7 @@ function prepareWorkflowChildParams(params: SubagentParamsLike): SubagentParamsL
3983
4030
  acceptance,
3984
4031
  agentContract,
3985
4032
  toolBudget,
4033
+ reads,
3986
4034
  ...runParams
3987
4035
  } = params;
3988
4036
  return {
@@ -3994,6 +4042,7 @@ function prepareWorkflowChildParams(params: SubagentParamsLike): SubagentParamsL
3994
4042
  ...(model !== undefined ? { model } : {}),
3995
4043
  ...(skill !== undefined ? { skill } : {}),
3996
4044
  ...(output !== undefined ? { output } : {}),
4045
+ ...(reads !== undefined ? { reads } : {}),
3997
4046
  ...(outputMode !== undefined ? { outputMode } : {}),
3998
4047
  ...(outputSchema !== undefined ? { outputSchema } : {}),
3999
4048
  ...(acceptance !== undefined ? { acceptance } : {}),
@@ -4123,11 +4172,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4123
4172
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
4124
4173
  const chatProgress = chatProgressResult.projection!;
4125
4174
  const explicitMission = requestParams.missionId !== undefined || requestParams.mission !== undefined;
4175
+ const autoMission = !explicitMission;
4176
+ const workflowPreview = autoMission ? previewSimpleWorkflowRun(requestParams.workflowScript) : undefined;
4177
+ const previewTask = workflowPreview?.task?.trim() || undefined;
4178
+ const previewAgent = workflowPreview?.agent?.trim() || undefined;
4179
+ const scriptFirstLine = requestParams.workflowScript.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "Workflow";
4180
+ const boundedScriptPreview = scriptFirstLine.length > 100 ? `${scriptFirstLine.slice(0, 97)}...` : scriptFirstLine;
4181
+ const derivedObjective = previewTask || (previewAgent ? `Workflow: ${previewAgent}` : boundedScriptPreview);
4126
4182
  let missionBinding: MissionLaunchBinding | undefined;
4127
4183
  let missionWarning: string | undefined;
4128
4184
  try {
4129
4185
  missionBinding = prepareMissionLaunch({
4130
- params: requestParams,
4186
+ params: autoMission ? { ...requestParams, task: derivedObjective } : requestParams,
4131
4187
  projectRoot: workflowCwd,
4132
4188
  ...(deps.config.missions ? { config: deps.config.missions } : {}),
4133
4189
  ownerSessionId: resolveCurrentSessionId(ctx.sessionManager),
@@ -4136,6 +4192,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4136
4192
  if (explicitMission) return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4137
4193
  missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
4138
4194
  }
4195
+ let shouldPatchMissionObjective = autoMission && previewTask === undefined && missionBinding !== undefined;
4196
+ const patchMissionObjective = (task: unknown): void => {
4197
+ if (!shouldPatchMissionObjective || !missionBinding || typeof task !== "string" || !task.trim()) return;
4198
+ shouldPatchMissionObjective = false;
4199
+ const objective = task.trim();
4200
+ const firstLine = objective.split(/\r?\n/, 1)[0]?.trim() || objective;
4201
+ const title = firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
4202
+ try {
4203
+ updateMission(missionBinding.location, missionBinding.missionId, { title, objective });
4204
+ } catch (error) {
4205
+ console.warn(`[pi-subagents] Failed to update automatic mission objective: ${error instanceof Error ? error.message : String(error)}`);
4206
+ }
4207
+ };
4208
+ const detachWorkflowChildMissions = autoMission || missionBinding !== undefined || requestParams.mission === false;
4139
4209
  const workflowState = missionBinding ? createMissionWorkflowState(missionBinding.location, missionBinding.missionId) : undefined;
4140
4210
  const attachWorkflowMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
4141
4211
  if (!missionBinding) return missionWarning ? { ...result, details: { ...result.details, missionWarning } } : result;
@@ -4149,7 +4219,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4149
4219
  }
4150
4220
  };
4151
4221
  if (requestParams.async !== false) {
4152
- const workflowRunId = _id;
4222
+ const toolCallId = _id;
4223
+ const workflowRunId = randomUUID();
4153
4224
  const asyncDir = path.join(DIRS.async, workflowRunId);
4154
4225
  const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4155
4226
  const statusPath = path.join(asyncDir, "status.json");
@@ -4163,13 +4234,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4163
4234
  deps.state.workflowControllers.set(workflowRunId, controller);
4164
4235
  let status: AsyncStatus = {
4165
4236
  runId: workflowRunId,
4237
+ toolCallId,
4166
4238
  sessionId: currentSessionId ?? undefined,
4167
4239
  mode: "workflow",
4168
4240
  state: "running",
4169
4241
  startedAt,
4170
4242
  lastUpdate: startedAt,
4171
4243
  ...(timeout !== undefined ? { deadlineAt: startedAt + timeout, timeoutMs: timeout } : {}),
4172
- cwd: parentCwd,
4244
+ cwd: workflowCwd,
4173
4245
  pid: process.pid,
4174
4246
  steps: [],
4175
4247
  workflow: { trace: [], emits: [], console: [] },
@@ -4182,6 +4254,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4182
4254
  if (job) {
4183
4255
  job.status = status.state;
4184
4256
  job.updatedAt = status.lastUpdate;
4257
+ job.activityState = status.activityState;
4258
+ job.lastActivityAt = status.lastActivityAt;
4259
+ job.currentTool = status.currentTool;
4260
+ job.currentToolStartedAt = status.currentToolStartedAt;
4261
+ job.currentPath = status.currentPath;
4262
+ job.turnCount = status.turnCount;
4263
+ job.toolCount = status.toolCount;
4264
+ job.currentStep = status.currentStep;
4185
4265
  if (status.steps) {
4186
4266
  job.steps = status.steps.map((step, index) => ({ ...step, index }));
4187
4267
  job.agents = status.steps.map((step) => step.agent);
@@ -4192,7 +4272,27 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4192
4272
  job.workflow = status.workflow;
4193
4273
  }
4194
4274
  };
4195
- const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: parentCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
4275
+ const projectWorkflowActivity = () => {
4276
+ const runningSteps = (status.steps ?? []).filter((step) => step.status === "running");
4277
+ const lastActivityAt = runningSteps.reduce<number | undefined>((latest, step) => step.lastActivityAt === undefined ? latest : Math.max(latest ?? step.lastActivityAt, step.lastActivityAt), undefined);
4278
+ const activeToolStep = runningSteps
4279
+ .filter((step) => step.currentTool)
4280
+ .sort((left, right) => (left.lastActivityAt ?? 0) - (right.lastActivityAt ?? 0))
4281
+ .at(-1);
4282
+ status.activityState = runningSteps.some((step) => step.activityState === "needs_attention")
4283
+ ? "needs_attention"
4284
+ : runningSteps.some((step) => step.activityState === "active_long_running") ? "active_long_running" : undefined;
4285
+ status.lastActivityAt = lastActivityAt;
4286
+ status.currentTool = activeToolStep?.currentTool;
4287
+ status.currentToolStartedAt = activeToolStep?.currentToolStartedAt;
4288
+ status.currentPath = activeToolStep?.currentPath;
4289
+ const turnCounts = (status.steps ?? []).flatMap((step) => step.turnCount === undefined ? [] : [step.turnCount]);
4290
+ const toolCounts = (status.steps ?? []).flatMap((step) => step.toolCount === undefined ? [] : [step.toolCount]);
4291
+ status.turnCount = turnCounts.length > 0 ? turnCounts.reduce((total, count) => total + count, 0) : undefined;
4292
+ status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
4293
+ status.currentStep = runningSteps.length === 1 ? status.steps?.indexOf(runningSteps[0]!) : undefined;
4294
+ };
4295
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
4196
4296
  deps.state.asyncJobs.set(workflowRunId, workflowJob);
4197
4297
  deps.state.fleetJobs ??= new Map();
4198
4298
  deps.state.fleetJobs.set(workflowRunId, workflowJob);
@@ -4215,9 +4315,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4215
4315
  if (entry.durationMs === undefined) delete existing.durationMs;
4216
4316
  else existing.durationMs = entry.durationMs;
4217
4317
  } else {
4218
- status.steps?.push({ agent: entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped });
4318
+ status.steps?.push({ agent: entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() });
4219
4319
  }
4220
4320
  }
4321
+ projectWorkflowActivity();
4221
4322
  persist();
4222
4323
  appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
4223
4324
  };
@@ -4238,8 +4339,29 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4238
4339
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4239
4340
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4240
4341
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4241
- const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: missionBinding !== undefined || requestParams.mission === false });
4242
- const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4342
+ patchMissionObjective(childParams.task);
4343
+ const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions });
4344
+ const result = await execute(randomUUID(), childRequest, workflowSignal, (update) => {
4345
+ const progress = update.details.progress?.[0];
4346
+ const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4347
+ if (!progress || !step) return;
4348
+ step.status = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4349
+ step.activityState = progress.activityState;
4350
+ step.lastActivityAt = progress.lastActivityAt;
4351
+ step.currentTool = progress.currentTool;
4352
+ step.currentToolArgs = progress.currentToolArgs;
4353
+ step.currentToolStartedAt = progress.currentToolStartedAt;
4354
+ step.currentPath = progress.currentPath;
4355
+ step.recentTools = progress.recentTools.map((tool) => ({ ...tool }));
4356
+ step.recentOutput = [...progress.recentOutput];
4357
+ step.turnCount = progress.turnCount;
4358
+ step.toolCount = progress.toolCount;
4359
+ step.model = progress.model;
4360
+ step.thinking = progress.thinking;
4361
+ step.error = progress.error;
4362
+ projectWorkflowActivity();
4363
+ persist();
4364
+ }, ctx, preserveActiveSession);
4243
4365
  workflowResults.push(...result.details.results);
4244
4366
  const child = workflowChildResult(key, result);
4245
4367
  if (result.details.asyncId) {
@@ -4255,23 +4377,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4255
4377
  const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
4256
4378
  const workflowUsage = sumResultsUsage(workflowResults);
4257
4379
  status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4380
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ agent: child.key, ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4258
4381
  persist();
4259
4382
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4260
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: parentCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4261
4383
  } catch (error) {
4262
4384
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4263
4385
  const stopped = controller.signal.aborted;
4264
4386
  status = compactOptional<AsyncStatus>({ ...status, state: stopped ? "stopped" : "failed", stopped: stopped || undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
4387
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ agent: child.key, ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4265
4388
  persist();
4266
4389
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4267
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: parentCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4268
4390
  } finally {
4269
4391
  deps.state.workflowControllers?.delete(workflowRunId);
4270
4392
  }
4271
4393
  });
4272
4394
  return attachWorkflowMission({
4273
4395
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
4274
- details: { mode: "workflow", runId: workflowRunId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4396
+ details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4275
4397
  });
4276
4398
  }
4277
4399
  const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = requestParams;
@@ -4299,7 +4421,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4299
4421
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4300
4422
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4301
4423
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4302
- const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: missionBinding !== undefined || requestParams.mission === false, suppressRoutineResultIntercom: chatProgress.mode === "live-card" });
4424
+ patchMissionObjective(childParams.task);
4425
+ const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" });
4303
4426
  const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4304
4427
  workflowResults.push(...result.details.results);
4305
4428
  return workflowChildResult(key, result);
@@ -4572,13 +4695,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4572
4695
  }
4573
4696
  if (action === "status") {
4574
4697
  if (!preserveActiveSession) deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4575
- const withBudget = (result: AgentToolResult<Details>) => withSpawnBudgetStatus(
4576
- result,
4577
- deps.state,
4578
- deps.config,
4579
- deps.state.currentSessionId,
4580
- );
4581
4698
  const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
4699
+ const hasDirectoryTarget = Boolean(paramsWithResolvedCwd.dir);
4700
+ const targetLabel = formatStatusTargetLabel(paramsWithResolvedCwd, targetRunId);
4701
+ const withBudget = (result: AgentToolResult<Details>) => {
4702
+ const budgeted = withSpawnBudgetStatus(result, deps.state, deps.config, deps.state.currentSessionId);
4703
+ return {
4704
+ ...budgeted,
4705
+ content: budgeted.content.map((item, index) => index === 0 && item.type === "text"
4706
+ ? { ...item, text: `${targetLabel}\n${item.text}` }
4707
+ : item),
4708
+ };
4709
+ };
4582
4710
  const nestedScope = nestedResolutionScopeForExecutor(deps);
4583
4711
  const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
4584
4712
  if (paramsWithResolvedCwd.view === "fleet") {
@@ -4603,7 +4731,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4603
4731
  const message = error instanceof Error ? error.message : String(error);
4604
4732
  return withBudget({ content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } });
4605
4733
  }
4606
- } else {
4734
+ } else if (!hasDirectoryTarget) {
4607
4735
  const foreground = getForegroundControl(deps.state, undefined);
4608
4736
  if (foreground && paramsWithResolvedCwd.view !== "transcript") return withBudget(foregroundStatusResult(foreground));
4609
4737
  if (foreground && paramsWithResolvedCwd.view === "transcript") {
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
@@ -206,16 +207,18 @@ function isSubagentToolCallBlock(block: unknown): boolean {
206
207
  }
207
208
 
208
209
  const PORTABLE_TOOL_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
210
+ const MAX_PORTABLE_TOOL_ID_LENGTH = 64;
209
211
  const COMPOSITE_TOOL_ID_APIS = new Set([
210
212
  "azure-openai-responses",
211
- "openai-codex-responses",
212
213
  "openai-completions",
213
214
  "openai-responses",
214
215
  ]);
215
216
 
216
217
  function portableToolId(id: string): string {
217
- if (PORTABLE_TOOL_ID_PATTERN.test(id)) return id;
218
- return `tool_${Buffer.from(id).toString("base64url") || "empty"}`;
218
+ if (PORTABLE_TOOL_ID_PATTERN.test(id) && id.length <= MAX_PORTABLE_TOOL_ID_LENGTH) return id;
219
+ const encoded = `tool_${Buffer.from(id).toString("base64url") || "empty"}`;
220
+ if (encoded.length <= MAX_PORTABLE_TOOL_ID_LENGTH) return encoded;
221
+ return `tool_${createHash("sha256").update(id).digest("base64url")}`;
219
222
  }
220
223
 
221
224
  function sanitizeToolHistoryMessage(message: unknown): unknown {
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import * as fs from "node:fs";
6
+ import * as os from "node:os";
6
7
  import * as path from "node:path";
7
8
  import type { AgentConfig } from "../agents/agents.ts";
8
9
  import { normalizeSkillInput } from "../agents/skills.ts";
@@ -23,8 +24,10 @@ export interface ResolvedStepBehavior {
23
24
  model?: string;
24
25
  }
25
26
 
27
+ export type OutputOverrideInput = string | boolean;
28
+
26
29
  export interface StepOverrides {
27
- output?: string | false;
30
+ output?: OutputOverrideInput;
28
31
  outputMode?: OutputMode;
29
32
  reads?: string[] | false;
30
33
  progress?: boolean;
@@ -32,8 +35,10 @@ export interface StepOverrides {
32
35
  model?: string;
33
36
  }
34
37
 
35
- function normalizeOutputOverride(output: string | false | undefined): string | false | undefined {
36
- return output === "false" ? false : output;
38
+ function normalizeOutputOverride(output: unknown): string | false | undefined {
39
+ if (output === false || output === "false") return false;
40
+ if (output === true || output === "true") return undefined;
41
+ return typeof output === "string" && output.length > 0 ? output : undefined;
37
42
  }
38
43
 
39
44
  // =============================================================================
@@ -49,7 +54,7 @@ export interface SequentialStep {
49
54
  as?: string;
50
55
  outputSchema?: JsonSchemaObject;
51
56
  cwd?: string;
52
- output?: string | false;
57
+ output?: OutputOverrideInput;
53
58
  outputMode?: OutputMode;
54
59
  reads?: string[] | false;
55
60
  progress?: boolean;
@@ -71,7 +76,7 @@ export interface ParallelTaskItem {
71
76
  outputSchema?: JsonSchemaObject;
72
77
  cwd?: string;
73
78
  count?: number;
74
- output?: string | false;
79
+ output?: OutputOverrideInput;
75
80
  outputMode?: OutputMode;
76
81
  reads?: string[] | false;
77
82
  progress?: boolean;
@@ -123,7 +128,7 @@ export interface CheckpointStep {
123
128
  agent?: string;
124
129
  task?: string;
125
130
  as?: string;
126
- output?: string | false;
131
+ output?: OutputOverrideInput;
127
132
  outputMode?: OutputMode;
128
133
  reads?: string[] | false;
129
134
  progress?: boolean;
@@ -330,10 +335,22 @@ export function suppressProgressForReadOnlyTask(behavior: ResolvedStepBehavior,
330
335
  // =============================================================================
331
336
 
332
337
  /**
333
- * Resolve a file path: absolute paths pass through, relative paths get chainDir prepended.
338
+ * Expand a leading `~`/`~/` to the user's home directory. Other forms (relative,
339
+ * absolute, `~user/`) pass through unchanged.
340
+ */
341
+ export function expandHomePath(filePath: string): string {
342
+ if (filePath === "~") return os.homedir();
343
+ if (filePath.startsWith("~/")) return path.join(os.homedir(), filePath.slice(2));
344
+ return filePath;
345
+ }
346
+
347
+ /**
348
+ * Resolve a file path: `~`/`~/` expand to home first, then absolute paths pass
349
+ * through and relative paths get chainDir prepended.
334
350
  */
335
- function resolveChainPath(filePath: string, chainDir: string): string {
336
- return path.isAbsolute(filePath) ? filePath : path.join(chainDir, filePath);
351
+ export function resolveChainPath(filePath: string, chainDir: string): string {
352
+ const expanded = expandHomePath(filePath);
353
+ return path.isAbsolute(expanded) ? expanded : path.join(chainDir, expanded);
337
354
  }
338
355
 
339
356
  /**
@@ -947,12 +947,46 @@ export interface SpawnBudgetSnapshot {
947
947
  grantHistory: SpawnBudgetGrant[];
948
948
  }
949
949
 
950
+ /** Slim per-child projection of a terminal result payload, safe to surface in tool_result details. */
951
+ export interface WaitCompletionChild {
952
+ agent?: string;
953
+ /** Child run identity where the producer records one (workflow children); artifact files are keyed by it. */
954
+ runId?: string;
955
+ success?: boolean;
956
+ outputState?: SubagentOutputState;
957
+ error?: string;
958
+ model?: string;
959
+ artifactPaths?: Partial<ArtifactPaths>;
960
+ }
961
+
962
+ /**
963
+ * Terminal completion observed for a run a subagent_wait call covered. Carries run
964
+ * identity and the artifact trail; output text stays in the tool result content.
965
+ */
966
+ export interface WaitCompletion {
967
+ runId: string;
968
+ agent?: string;
969
+ mode?: string;
970
+ state?: string;
971
+ success?: boolean;
972
+ results?: WaitCompletionChild[];
973
+ }
974
+
950
975
  export interface Details {
951
976
  mode: SubagentResultMode | "management";
952
977
  runId?: string;
978
+ /** Host tool-call id retained when it differs from the internal run id. */
979
+ toolCallId?: string;
953
980
  /** Run-level context summary. "mixed" when children resolved to different modes. */
954
981
  context?: "fresh" | "fork" | "mixed";
955
982
  results: SingleResult[];
983
+ /**
984
+ * Terminal completion payloads for runs this subagent_wait call observed
985
+ * finishing. Async completions travel as result files that are consumed and
986
+ * deleted after text delivery, so without this field their run and artifact
987
+ * identity never reaches tool_result details.
988
+ */
989
+ completions?: WaitCompletion[];
956
990
  controlEvents?: ControlEvent[];
957
991
  steering?: SteerActionResult;
958
992
  asyncId?: string;
@@ -1246,6 +1280,8 @@ export interface ExternalProcessStatus {
1246
1280
  export interface AsyncStatus {
1247
1281
  lifecycleArtifactVersion?: SubagentLifecycleArtifactVersion;
1248
1282
  runId: string;
1283
+ /** Host tool-call id retained when it differs from the internal run id. */
1284
+ toolCallId?: string;
1249
1285
  sessionId?: string;
1250
1286
  mode: SubagentRunMode;
1251
1287
  context?: "fresh" | "fork" | "mixed";
@@ -1578,6 +1614,8 @@ export interface SubagentState {
1578
1614
  lastUiContext: ExtensionContext | null;
1579
1615
  poller: NodeJS.Timeout | null;
1580
1616
  completionSeen: Map<string, number>;
1617
+ /** Terminal result payloads observed by the result watcher, keyed by run id and pruned by the completion TTL. */
1618
+ completedResults?: Map<string, { seenAt: number; completion: WaitCompletion }>;
1581
1619
  watcher: FSWatcher | null;
1582
1620
  watcherRestartTimer: ReturnType<typeof setTimeout> | null;
1583
1621
  resultFileCoalescer: {
@@ -1736,6 +1774,8 @@ export type InlineToolDisplay = "rich" | "summary";
1736
1774
  export interface ScheduledRunsConfig {
1737
1775
  enabled?: boolean;
1738
1776
  maxPending?: number;
1777
+ /** Absolute or `~/` root for per-project durable schedules. */
1778
+ storeRoot?: string;
1739
1779
  }
1740
1780
 
1741
1781
  export type FleetViewPlacement = "aboveEditor" | "belowEditor";
@@ -385,8 +385,12 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
385
385
 
386
386
  const respond = (promise: Promise<unknown>) => {
387
387
  void promise.then(
388
- (value) => worker.postMessage({ type: "response", callId: message.callId, ok: true, value: omitUndefinedWorkflowValues(value) }),
389
- (error: unknown) => worker.postMessage({ type: "response", callId: message.callId, ok: false, error: error instanceof Error ? error.message : String(error) }),
388
+ (value) => {
389
+ if (!settled) worker.postMessage({ type: "response", callId: message.callId, ok: true, value: omitUndefinedWorkflowValues(value) });
390
+ },
391
+ (error: unknown) => {
392
+ if (!settled) worker.postMessage({ type: "response", callId: message.callId, ok: false, error: error instanceof Error ? error.message : String(error) });
393
+ },
390
394
  );
391
395
  };
392
396