@selesai/code 0.5.29 → 0.6.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/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +18 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/system-prompt.test.d.ts +2 -0
- package/dist/core/system-prompt.test.d.ts.map +1 -0
- package/dist/core/system-prompt.test.js +89 -0
- package/dist/core/system-prompt.test.js.map +1 -0
- package/dist/defaults/models.json +13 -45
- package/dist/defaults/settings.json +1 -2
- package/dist/extensions/copy-turn.test.ts +131 -0
- package/dist/extensions/copy-turn.ts +6 -1
- package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/dist/extensions/package.json +0 -1
- package/dist/extensions/pi-subagents/CHANGELOG.md +3 -0
- package/dist/extensions/pi-subagents/README.md +27 -32
- package/dist/extensions/pi-subagents/agents/architect.md +4 -4
- package/dist/extensions/pi-subagents/agents/builder.md +5 -4
- package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
- package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
- package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +10 -9
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +12 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +10 -11
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +56 -9
- package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +24 -7
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +23 -5
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +27 -1
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +64 -6
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +16 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +72 -18
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +19 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +127 -31
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +4 -6
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +41 -2
- package/dist/extensions/pi-subagents/src/shared/utils.ts +29 -1
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -1
- package/dist/extensions/pi-subagents/src/tui/render.ts +28 -6
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +111 -6
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +74 -43
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +36 -21
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +5 -3
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -8
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +14 -7
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +81 -5
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +49 -10
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
- package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +29 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
- package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +23 -1
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +60 -9
- package/dist/skills/ponytail/SKILL.md +1 -3
- package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
- package/package.json +2 -2
- package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
- package/dist/extensions/caveman/index.js +0 -118
- package/dist/extensions/caveman/package.json +0 -8
- package/dist/extensions/caveman/test/extension.test.js +0 -203
- package/dist/extensions/caveman/test/helpers.test.js +0 -58
- package/dist/skills/caveman/SKILL.md +0 -50
|
@@ -91,6 +91,34 @@ import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-bu
|
|
|
91
91
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
92
92
|
import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Resolve a chain step's output path and effective output mode.
|
|
96
|
+
*
|
|
97
|
+
* Explicit `output` paths pass through unchanged (absolute as-is, relative under
|
|
98
|
+
* the chain dir). Omitted output generates a collision-safe per-child durable
|
|
99
|
+
* path `<chainDir>/outputs/<flat-index>-<agent>.md` unless the caller explicitly
|
|
100
|
+
* used `output: false`. Omitted `outputMode` resolves to `file-only` when an
|
|
101
|
+
* output path is active; explicit `outputMode: "inline"` is preserved exactly.
|
|
102
|
+
*/
|
|
103
|
+
function resolveChainStepOutput(input: {
|
|
104
|
+
behaviorOutput: string | false;
|
|
105
|
+
rawOutput: string | false | undefined;
|
|
106
|
+
rawOutputMode: "inline" | "file-only" | undefined;
|
|
107
|
+
chainDir: string;
|
|
108
|
+
flatIndex: number;
|
|
109
|
+
agent: string;
|
|
110
|
+
}): { outputPath?: string; outputMode: "inline" | "file-only" } {
|
|
111
|
+
const explicit = typeof input.behaviorOutput === "string" ? input.behaviorOutput : undefined;
|
|
112
|
+
let outputPath: string | undefined;
|
|
113
|
+
if (explicit) {
|
|
114
|
+
outputPath = path.isAbsolute(explicit) ? explicit : path.join(input.chainDir, explicit);
|
|
115
|
+
} else if (input.behaviorOutput === false && input.rawOutput !== false && input.rawOutput !== "false") {
|
|
116
|
+
outputPath = path.join(input.chainDir, "outputs", `${input.flatIndex}-${input.agent}.md`);
|
|
117
|
+
}
|
|
118
|
+
return { outputPath, outputMode: input.rawOutputMode ?? (outputPath ? "file-only" : "inline") };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
94
122
|
interface ChainExecutionDetailsInput {
|
|
95
123
|
results: SingleResult[];
|
|
96
124
|
includeProgress?: boolean;
|
|
@@ -353,9 +381,16 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
353
381
|
? input.worktreeSetup.worktrees[taskIndex]!.agentCwd
|
|
354
382
|
: resolveChildCwd(input.cwd ?? input.ctx.cwd, task.cwd);
|
|
355
383
|
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
:
|
|
384
|
+
const resolvedTaskOutput = resolveChainStepOutput({
|
|
385
|
+
behaviorOutput: behavior.output,
|
|
386
|
+
rawOutput: task.output,
|
|
387
|
+
rawOutputMode: task.outputMode,
|
|
388
|
+
chainDir: input.chainDir,
|
|
389
|
+
flatIndex: childIndex,
|
|
390
|
+
agent: task.agent,
|
|
391
|
+
});
|
|
392
|
+
const outputPath = resolvedTaskOutput.outputPath;
|
|
393
|
+
const effectiveOutputMode = resolvedTaskOutput.outputMode;
|
|
359
394
|
taskStr = injectSingleOutputInstruction(taskStr, outputPath, taskAgentConfig);
|
|
360
395
|
const interruptController = new AbortController();
|
|
361
396
|
if (input.foregroundControl) {
|
|
@@ -399,7 +434,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
399
434
|
artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
|
|
400
435
|
artifactConfig: input.artifactConfig,
|
|
401
436
|
outputPath,
|
|
402
|
-
outputMode:
|
|
437
|
+
outputMode: effectiveOutputMode,
|
|
403
438
|
maxSubagentDepth,
|
|
404
439
|
controlConfig: input.controlConfig,
|
|
405
440
|
onControlEvent: input.onControlEvent,
|
|
@@ -793,10 +828,16 @@ ${step.message}` : ""}` }],
|
|
|
793
828
|
.map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
|
|
794
829
|
for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
|
|
795
830
|
const behavior = parallelBehaviors[taskIndex]!;
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
:
|
|
799
|
-
|
|
831
|
+
const task = step.parallel[taskIndex]!;
|
|
832
|
+
const resolvedOutput = resolveChainStepOutput({
|
|
833
|
+
behaviorOutput: behavior.output,
|
|
834
|
+
rawOutput: task.output,
|
|
835
|
+
rawOutputMode: task.outputMode,
|
|
836
|
+
chainDir,
|
|
837
|
+
flatIndex: globalTaskIndex + taskIndex,
|
|
838
|
+
agent: task.agent,
|
|
839
|
+
});
|
|
840
|
+
const validationError = validateFileOnlyOutputMode(resolvedOutput.outputMode, resolvedOutput.outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${task.agent})`);
|
|
800
841
|
if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
|
|
801
842
|
}
|
|
802
843
|
progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
|
|
@@ -1049,10 +1090,16 @@ ${step.message}` : ""}` }],
|
|
|
1049
1090
|
|
|
1050
1091
|
for (let taskIndex = 0; taskIndex < dynamicParallelStep.parallel.length; taskIndex++) {
|
|
1051
1092
|
const behavior = parallelBehaviors[taskIndex]!;
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
:
|
|
1055
|
-
|
|
1093
|
+
const task = dynamicParallelStep.parallel[taskIndex]!;
|
|
1094
|
+
const resolvedOutput = resolveChainStepOutput({
|
|
1095
|
+
behaviorOutput: behavior.output,
|
|
1096
|
+
rawOutput: task.output,
|
|
1097
|
+
rawOutputMode: task.outputMode,
|
|
1098
|
+
chainDir,
|
|
1099
|
+
flatIndex: globalTaskIndex + taskIndex,
|
|
1100
|
+
agent: task.agent,
|
|
1101
|
+
});
|
|
1102
|
+
const validationError = validateFileOnlyOutputMode(resolvedOutput.outputMode, resolvedOutput.outputPath, `Dynamic chain step ${stepIndex + 1} item ${taskIndex + 1} (${task.agent})`);
|
|
1056
1103
|
if (validationError) {
|
|
1057
1104
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: validationError };
|
|
1058
1105
|
return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
|
|
@@ -1289,11 +1336,18 @@ ${step.message}` : ""}` }],
|
|
|
1289
1336
|
{ scope: modelScope },
|
|
1290
1337
|
);
|
|
1291
1338
|
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
:
|
|
1339
|
+
const resolvedStepOutput = resolveChainStepOutput({
|
|
1340
|
+
behaviorOutput: behavior.output,
|
|
1341
|
+
rawOutput: stepOverride.output,
|
|
1342
|
+
rawOutputMode: seqStep.outputMode,
|
|
1343
|
+
chainDir,
|
|
1344
|
+
flatIndex: globalTaskIndex,
|
|
1345
|
+
agent: seqStep.agent,
|
|
1346
|
+
});
|
|
1347
|
+
const outputPath = resolvedStepOutput.outputPath;
|
|
1348
|
+
const effectiveOutputMode = resolvedStepOutput.outputMode;
|
|
1295
1349
|
stepTask = injectSingleOutputInstruction(stepTask, outputPath, agentConfig);
|
|
1296
|
-
const validationError = validateFileOnlyOutputMode(
|
|
1350
|
+
const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
|
|
1297
1351
|
if (validationError) {
|
|
1298
1352
|
return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
|
|
1299
1353
|
}
|
|
@@ -1339,7 +1393,7 @@ ${step.message}` : ""}` }],
|
|
|
1339
1393
|
dynamicChildren,
|
|
1340
1394
|
dynamicGroupStatuses,
|
|
1341
1395
|
});
|
|
1342
|
-
|
|
1396
|
+
r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
1343
1397
|
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
1344
1398
|
capabilityCeiling: params.capabilityCeiling,
|
|
1345
1399
|
context: params.contextForAgent?.(seqStep.agent),
|
|
@@ -1358,7 +1412,7 @@ ${step.message}` : ""}` }],
|
|
|
1358
1412
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
1359
1413
|
artifactConfig,
|
|
1360
1414
|
outputPath,
|
|
1361
|
-
outputMode:
|
|
1415
|
+
outputMode: effectiveOutputMode,
|
|
1362
1416
|
maxSubagentDepth,
|
|
1363
1417
|
controlConfig,
|
|
1364
1418
|
onControlEvent,
|
|
@@ -62,7 +62,7 @@ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
|
62
62
|
import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
|
|
63
63
|
import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
|
|
64
64
|
import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
65
|
-
import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
65
|
+
import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatBoundedPersistenceFallback, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
66
66
|
import {
|
|
67
67
|
buildModelCandidates,
|
|
68
68
|
formatModelAttemptNote,
|
|
@@ -1282,7 +1282,12 @@ async function runSingleAttempt(
|
|
|
1282
1282
|
reason: "completion_guard",
|
|
1283
1283
|
}));
|
|
1284
1284
|
}
|
|
1285
|
-
|
|
1285
|
+
// Persist whenever an output path is configured and meaningful child output
|
|
1286
|
+
// exists, not only for successful runs: failed children with output keep a
|
|
1287
|
+
// readable result file and a saved-output reference instead of raw inline
|
|
1288
|
+
// output. Synthetic failures without output (startup errors, empty responses)
|
|
1289
|
+
// stay outputState "absent" and are not promoted to a bogus saved reference.
|
|
1290
|
+
if (options.outputPath && (fullOutput.trim() || result.exitCode === 0)) {
|
|
1286
1291
|
const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
|
|
1287
1292
|
fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
|
|
1288
1293
|
result.savedOutputPath = resolvedOutput.savedPath;
|
|
@@ -1295,9 +1300,18 @@ async function runSingleAttempt(
|
|
|
1295
1300
|
artifactOutputByResult.set(result, fullOutput);
|
|
1296
1301
|
acceptanceOutputByResult.set(result, acceptanceOutput);
|
|
1297
1302
|
result.outputMode = options.outputMode ?? "inline";
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1303
|
+
const hasSavedReference = result.savedOutputPath && result.outputReference ? true : false;
|
|
1304
|
+
result.finalOutput = hasSavedReference && (options.outputMode === "file-only" || result.exitCode !== 0)
|
|
1305
|
+
? result.outputReference!.message
|
|
1306
|
+
: result.outputSaveError && options.outputPath
|
|
1307
|
+
? formatBoundedPersistenceFallback({
|
|
1308
|
+
error: result.outputSaveError,
|
|
1309
|
+
outputPath: options.outputPath,
|
|
1310
|
+
fullOutput,
|
|
1311
|
+
exitCode: result.exitCode,
|
|
1312
|
+
processError: result.error,
|
|
1313
|
+
})
|
|
1314
|
+
: fullOutput;
|
|
1301
1315
|
result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
|
|
1302
1316
|
if (options.onUpdate) {
|
|
1303
1317
|
const finalText = result.finalOutput || result.error || "(no output)";
|
|
@@ -25,7 +25,6 @@ import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/
|
|
|
25
25
|
import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
|
|
26
26
|
import { buildModelCandidates, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
|
|
27
27
|
import type { ModelScopeConfig } from "../shared/model-scope.ts";
|
|
28
|
-
import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
|
|
29
28
|
import { recordRun } from "../shared/run-history.ts";
|
|
30
29
|
import {
|
|
31
30
|
buildChainInstructions,
|
|
@@ -57,7 +56,7 @@ import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
|
57
56
|
import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
|
|
58
57
|
import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
59
58
|
import { isAgentContractV1 } from "../shared/agent-contract.ts";
|
|
60
|
-
import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
59
|
+
import { finalizeSingleOutput, formatBoundedPersistenceFallback, formatSavedOutputReference, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
61
60
|
import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
62
61
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
|
|
63
62
|
import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
|
|
@@ -521,7 +520,12 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
|
|
|
521
520
|
updatedAt,
|
|
522
521
|
...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}),
|
|
523
522
|
...(input.result.error ? { error: input.result.error } : {}),
|
|
524
|
-
|
|
523
|
+
// Reference-first remembered state: when an authoritative saved output path
|
|
524
|
+
// exists, do not retain the full final output in memory; status/transcript
|
|
525
|
+
// views recover it from `savedOutputPath` via `rememberedForegroundChildOutput`
|
|
526
|
+
// (runs/background/run-status.ts) and the TUI via `resultOutputForUi`
|
|
527
|
+
// (tui/render.ts).
|
|
528
|
+
...(input.result.finalOutput && !input.result.savedOutputPath ? { finalOutput: input.result.finalOutput } : {}),
|
|
525
529
|
outputState: input.result.outputState,
|
|
526
530
|
outputMode: input.result.outputMode,
|
|
527
531
|
savedOutputPath: input.result.savedOutputPath,
|
|
@@ -541,10 +545,11 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
|
|
|
541
545
|
...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
|
|
542
546
|
};
|
|
543
547
|
trimRememberedForegroundRuns(state);
|
|
544
|
-
const
|
|
548
|
+
const reference = input.result.outputReference?.message;
|
|
549
|
+
const output = reference ?? getSingleResultOutput(input.result).trim();
|
|
545
550
|
const success = terminalStatus === "completed";
|
|
546
551
|
const summary = !success && input.result.error
|
|
547
|
-
? `${input.result.error}${output ? `\n\
|
|
552
|
+
? `${input.result.error}${output ? `\n\n${output}` : ""}`
|
|
548
553
|
: output || input.result.error || "Detached child exited without final output.";
|
|
549
554
|
// A detached callback may outlive its extension runtime. Stale sessions are
|
|
550
555
|
// intentionally dropped rather than routed through a replacement runtime.
|
|
@@ -1288,7 +1293,7 @@ async function resumeAsyncRun(input: {
|
|
|
1288
1293
|
};
|
|
1289
1294
|
}
|
|
1290
1295
|
const runId = randomUUID().slice(0, 8);
|
|
1291
|
-
const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts
|
|
1296
|
+
const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts === true, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
|
|
1292
1297
|
const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
1293
1298
|
const contextPolicy = resolveExplicitContextPolicy(input.params);
|
|
1294
1299
|
const workflowTask = (input.params.task ?? followUp) || undefined;
|
|
@@ -1354,7 +1359,7 @@ async function resumeAsyncRun(input: {
|
|
|
1354
1359
|
|
|
1355
1360
|
const runId = randomUUID().slice(0, 8);
|
|
1356
1361
|
const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
|
|
1357
|
-
const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts
|
|
1362
|
+
const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts === true, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
|
|
1358
1363
|
const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
|
|
1359
1364
|
const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
1360
1365
|
const parentModel = input.parentModel;
|
|
@@ -1435,14 +1440,82 @@ async function resumeAsyncRun(input: {
|
|
|
1435
1440
|
}
|
|
1436
1441
|
|
|
1437
1442
|
function resultSummaryForIntercom(result: SingleResult): string {
|
|
1438
|
-
const
|
|
1439
|
-
if (
|
|
1440
|
-
return
|
|
1443
|
+
const reference = result.outputReference?.message;
|
|
1444
|
+
if (reference) {
|
|
1445
|
+
return result.exitCode !== 0 && result.error ? `${result.error}\n\n${reference}` : reference;
|
|
1441
1446
|
}
|
|
1442
|
-
|
|
1447
|
+
if (result.savedOutputPath) {
|
|
1448
|
+
try {
|
|
1449
|
+
const ref = formatSavedOutputReference(result.savedOutputPath, fs.readFileSync(result.savedOutputPath, "utf-8"));
|
|
1450
|
+
return result.exitCode !== 0 && result.error ? `${result.error}\n\n${ref.message}` : ref.message;
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
const fallback = formatBoundedPersistenceFallback({
|
|
1453
|
+
error: `Failed to read saved output: ${error instanceof Error ? error.message : String(error)}`,
|
|
1454
|
+
outputPath: result.savedOutputPath,
|
|
1455
|
+
fullOutput: getSingleResultOutput(result),
|
|
1456
|
+
exitCode: result.exitCode,
|
|
1457
|
+
processError: result.error,
|
|
1458
|
+
});
|
|
1459
|
+
return result.exitCode !== 0 && result.error ? `${result.error}\n\n${fallback}` : fallback;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (result.outputSaveError) {
|
|
1463
|
+
return formatBoundedPersistenceFallback({
|
|
1464
|
+
error: result.outputSaveError,
|
|
1465
|
+
outputPath: result.savedOutputPath,
|
|
1466
|
+
fullOutput: getSingleResultOutput(result),
|
|
1467
|
+
exitCode: result.exitCode,
|
|
1468
|
+
processError: result.error,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
// Legacy / `output: false` child without a durable saved path: bounded fallback
|
|
1472
|
+
// only, never the raw full output.
|
|
1473
|
+
return formatBoundedPersistenceFallback({
|
|
1474
|
+
error: "No saved output path is available for this child.",
|
|
1475
|
+
fullOutput: getSingleResultOutput(result),
|
|
1476
|
+
exitCode: result.exitCode,
|
|
1477
|
+
processError: result.error,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Compact terminal per-child line for bridge-off foreground parallel content:
|
|
1483
|
+
* status plus either the saved-output reference (default/file-only), the legacy
|
|
1484
|
+
* inline full output when explicitly requested, or the bounded fallback.
|
|
1485
|
+
*/
|
|
1486
|
+
function formatParallelChildResult(result: SingleResult, index: number): string {
|
|
1487
|
+
const status = resolveSubagentResultStatus({
|
|
1488
|
+
exitCode: result.exitCode,
|
|
1489
|
+
interrupted: result.interrupted,
|
|
1490
|
+
detached: result.detached,
|
|
1491
|
+
processSignal: result.processSignal,
|
|
1492
|
+
timedOut: result.timedOut,
|
|
1493
|
+
stopped: result.stopped,
|
|
1494
|
+
turnBudgetExceeded: result.turnBudgetExceeded,
|
|
1495
|
+
});
|
|
1496
|
+
const header = `=== Task ${index + 1}: ${result.agent} [${status}] ===`;
|
|
1497
|
+
if (result.outputMode === "inline") {
|
|
1498
|
+
const output = getSingleResultOutput(result).trim();
|
|
1499
|
+
return `${header}\n${output || "(no output)"}`;
|
|
1500
|
+
}
|
|
1501
|
+
const reference = result.outputReference?.message;
|
|
1502
|
+
if (reference) {
|
|
1503
|
+
const body = result.exitCode !== 0 && result.error ? `${result.error}\n${reference}` : reference;
|
|
1504
|
+
return `${header}\n${body}`;
|
|
1505
|
+
}
|
|
1506
|
+
return `${header}\n${formatBoundedPersistenceFallback({
|
|
1507
|
+
error: result.outputSaveError ?? "No saved output path is available for this child.",
|
|
1508
|
+
outputPath: result.savedOutputPath,
|
|
1509
|
+
fullOutput: getSingleResultOutput(result),
|
|
1510
|
+
exitCode: result.exitCode,
|
|
1511
|
+
processError: result.error,
|
|
1512
|
+
})}`;
|
|
1443
1513
|
}
|
|
1444
1514
|
|
|
1445
1515
|
function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string): string {
|
|
1516
|
+
// Reference-first and persistence-failure fallbacks already carry error/status
|
|
1517
|
+
// and must not be re-wrapped as raw "Output:" text.
|
|
1518
|
+
if (result.savedOutputPath || result.outputSaveError) return displayOutput;
|
|
1446
1519
|
const error = result.error || "Failed";
|
|
1447
1520
|
const output = displayOutput.trim();
|
|
1448
1521
|
const lines = [error];
|
|
@@ -2287,7 +2360,6 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2287
2360
|
}
|
|
2288
2361
|
const rawOutput = params.output !== undefined ? params.output : a.output;
|
|
2289
2362
|
const effectiveOutput = normalizeSingleOutputOverride(rawOutput, a.output);
|
|
2290
|
-
const effectiveOutputMode = params.outputMode ?? "inline";
|
|
2291
2363
|
const normalizedSkills = normalizeSkillInput(params.skill);
|
|
2292
2364
|
const skills = normalizedSkills === false ? [] : normalizedSkills;
|
|
2293
2365
|
const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
|
|
@@ -2309,7 +2381,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2309
2381
|
context: contextPolicy.contextForAgent(params.agent!),
|
|
2310
2382
|
skills,
|
|
2311
2383
|
output: effectiveOutput,
|
|
2312
|
-
outputMode:
|
|
2384
|
+
outputMode: params.outputMode,
|
|
2313
2385
|
outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
|
|
2314
2386
|
modelOverride,
|
|
2315
2387
|
thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
|
|
@@ -2610,6 +2682,19 @@ function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string,
|
|
|
2610
2682
|
: path.join(artifactsDir, "outputs", runId);
|
|
2611
2683
|
}
|
|
2612
2684
|
|
|
2685
|
+
/**
|
|
2686
|
+
* Generated per-run durable output path for foreground single runs when the
|
|
2687
|
+
* caller omits `output` (and does not set `output: false`):
|
|
2688
|
+
* `<singleRunOutputBaseDir>/<runId>/result.md`. The base is the configured
|
|
2689
|
+
* `singleRunOutputBaseDir` when present, otherwise `{artifactsDir}/outputs`.
|
|
2690
|
+
*/
|
|
2691
|
+
function resolveGeneratedSingleOutputPath(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
|
|
2692
|
+
const base = deps.config.singleRunOutputBaseDir
|
|
2693
|
+
? path.resolve(deps.expandTilde(deps.config.singleRunOutputBaseDir))
|
|
2694
|
+
: path.join(artifactsDir, "outputs");
|
|
2695
|
+
return path.join(base, runId, "result.md");
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2613
2698
|
function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined {
|
|
2614
2699
|
for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
|
|
2615
2700
|
const step = chain[stepIndex]!;
|
|
@@ -3068,8 +3153,19 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3068
3153
|
|
|
3069
3154
|
const behaviors = tasks.map((task, index) => {
|
|
3070
3155
|
let behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfigs[index]!, behaviorOverrides[index]!), taskTexts[index]);
|
|
3071
|
-
if (behaviorOverrides[index]?.output === undefined
|
|
3072
|
-
|
|
3156
|
+
if (behaviorOverrides[index]?.output === undefined) {
|
|
3157
|
+
if (typeof behavior.output === "string" && !path.isAbsolute(behavior.output)) {
|
|
3158
|
+
behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, behavior.output) };
|
|
3159
|
+
} else if (behavior.output === false && behaviorOverrides[index]?.output !== false && behaviorOverrides[index]?.output !== "false") {
|
|
3160
|
+
// Omitted output: generate a collision-safe per-task durable path in the
|
|
3161
|
+
// existing per-task namespace (parallel-0/<index>-<agent>/result.md).
|
|
3162
|
+
behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, "result.md") };
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
// Omitted outputMode resolves to file-only whenever an output path is active;
|
|
3166
|
+
// explicit `outputMode: "inline"` is preserved exactly.
|
|
3167
|
+
if (behaviorOverrides[index]?.outputMode === undefined && typeof behavior.output === "string") {
|
|
3168
|
+
behavior = { ...behavior, outputMode: "file-only" };
|
|
3073
3169
|
}
|
|
3074
3170
|
return behavior;
|
|
3075
3171
|
});
|
|
@@ -3235,21 +3331,12 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3235
3331
|
const worktreeSuffix = handoff?.suffix ?? "";
|
|
3236
3332
|
const ok = results.filter((result) => result.exitCode === 0).length;
|
|
3237
3333
|
const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
|
|
3238
|
-
const
|
|
3239
|
-
results.map((result) => ({
|
|
3240
|
-
agent: result.agent,
|
|
3241
|
-
output: result.truncation?.text || getSingleResultOutput(result),
|
|
3242
|
-
exitCode: result.exitCode,
|
|
3243
|
-
error: result.error,
|
|
3244
|
-
timedOut: result.timedOut,
|
|
3245
|
-
})),
|
|
3246
|
-
(i, agent) => `=== Task ${i + 1}: ${agent} ===`,
|
|
3247
|
-
);
|
|
3334
|
+
const perChildResults = results.map((result, i) => formatParallelChildResult(result, i)).join("\n\n");
|
|
3248
3335
|
|
|
3249
3336
|
const summary = `${ok}/${results.length} succeeded${downgradeNote}`;
|
|
3250
3337
|
const fullContent = worktreeSuffix
|
|
3251
|
-
? `${summary}\n\n${
|
|
3252
|
-
: `${summary}\n\n${
|
|
3338
|
+
? `${summary}\n\n${perChildResults}\n\n${worktreeSuffix}`
|
|
3339
|
+
: `${summary}\n\n${perChildResults}`;
|
|
3253
3340
|
|
|
3254
3341
|
return {
|
|
3255
3342
|
content: [{ type: "text", text: fullContent }],
|
|
@@ -3309,7 +3396,6 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3309
3396
|
let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
|
|
3310
3397
|
const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
|
|
3311
3398
|
let effectiveOutput = normalizeSingleOutputOverride(rawOutput, agentConfig.output);
|
|
3312
|
-
const effectiveOutputMode = params.outputMode ?? "inline";
|
|
3313
3399
|
const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
|
|
3314
3400
|
const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
|
|
3315
3401
|
|
|
@@ -3381,7 +3467,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3381
3467
|
context: contextPolicy.contextForAgent(params.agent!),
|
|
3382
3468
|
skills: skillOverride === false ? [] : skillOverride,
|
|
3383
3469
|
output: effectiveOutput,
|
|
3384
|
-
outputMode:
|
|
3470
|
+
outputMode: params.outputMode,
|
|
3385
3471
|
outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
|
|
3386
3472
|
modelOverride,
|
|
3387
3473
|
thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
|
|
@@ -3410,7 +3496,16 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3410
3496
|
task = wrapForkTask(task);
|
|
3411
3497
|
}
|
|
3412
3498
|
const cleanTask = task;
|
|
3413
|
-
|
|
3499
|
+
// Omitted output resolves to a generated per-run durable path
|
|
3500
|
+
// (`<singleRunOutputBaseDir>/<runId>/result.md`); omitted outputMode resolves to
|
|
3501
|
+
// file-only whenever an output path is active. Explicit paths, `output: false`,
|
|
3502
|
+
// and explicit `outputMode: "inline"` are preserved exactly.
|
|
3503
|
+
const outputPath = typeof effectiveOutput === "string"
|
|
3504
|
+
? resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId))
|
|
3505
|
+
: effectiveOutput === false
|
|
3506
|
+
? undefined
|
|
3507
|
+
: resolveGeneratedSingleOutputPath(deps, artifactsDir, runId);
|
|
3508
|
+
const effectiveOutputMode = params.outputMode ?? (outputPath ? "file-only" : "inline");
|
|
3414
3509
|
const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Single run (${params.agent})`);
|
|
3415
3510
|
if (validationError) {
|
|
3416
3511
|
return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
|
|
@@ -3549,6 +3644,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3549
3644
|
savedPath: r.savedOutputPath,
|
|
3550
3645
|
outputReference: r.outputReference,
|
|
3551
3646
|
saveError: r.outputSaveError,
|
|
3647
|
+
error: r.error,
|
|
3552
3648
|
});
|
|
3553
3649
|
if (foregroundControl) {
|
|
3554
3650
|
updateForegroundNestedProjection(foregroundControl);
|
|
@@ -4225,7 +4321,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4225
4321
|
|
|
4226
4322
|
const artifactConfig: ArtifactConfig = {
|
|
4227
4323
|
...DEFAULT_ARTIFACT_CONFIG,
|
|
4228
|
-
enabled: effectiveParams.artifacts
|
|
4324
|
+
enabled: effectiveParams.artifacts === true,
|
|
4229
4325
|
dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
|
|
4230
4326
|
};
|
|
4231
4327
|
const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
|
|
@@ -22,7 +22,7 @@ import type {
|
|
|
22
22
|
SubagentRunMode,
|
|
23
23
|
} from "../../shared/types.ts";
|
|
24
24
|
import { isAgentContractV1 } from "./agent-contract.ts";
|
|
25
|
-
import { classifyTaskMutationIntent, taskMayMutate } from "./task-intent.ts";
|
|
25
|
+
import { classifyTaskMutationIntent, resolveAgentRoutingRole, taskMayMutate } from "./task-intent.ts";
|
|
26
26
|
|
|
27
27
|
const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
|
|
28
28
|
none: 0,
|
|
@@ -82,7 +82,6 @@ function inferLevel(input: {
|
|
|
82
82
|
dynamic?: boolean;
|
|
83
83
|
dynamicGroup?: boolean;
|
|
84
84
|
}): { level: Exclude<AcceptanceLevel, "auto">; reasons: string[]; criteria: string[]; evidence: AcceptanceEvidenceKind[]; review?: { agent?: string; required?: boolean } } {
|
|
85
|
-
const agent = input.agentName.toLowerCase();
|
|
86
85
|
const task = input.task?.toLowerCase() ?? "";
|
|
87
86
|
const reasons: string[] = [];
|
|
88
87
|
// Declared roles replace name heuristics, so use the full writer grammar to detect explicit mutation independently of the actual agent name.
|
|
@@ -94,11 +93,10 @@ function inferLevel(input: {
|
|
|
94
93
|
&& !/\b(?:do not|don't|must not)\s+patch\b/.test(task)
|
|
95
94
|
&& /\bpatch\s+(?:(?:\.{0,2}[\\/])?(?:[\w.-]+[\\/])+[\w.-]+|[\w.-]+\.[a-z0-9]+\b|(?:the\s+)?parser\b)/.test(task);
|
|
96
95
|
const taskMayWrite = readOnlyTask ? false : taskMayMutate(input.task ?? "") || intent.kind === "implementation" || rolePatchTask;
|
|
97
|
-
const
|
|
98
|
-
|
|
96
|
+
const routingRole = resolveAgentRoutingRole(input.agentName, input.acceptanceRole);
|
|
97
|
+
const readOnlyAgent = routingRole === "read-only";
|
|
99
98
|
const writeTask = taskMayWrite
|
|
100
|
-
|| (
|
|
101
|
-
|| (input.acceptanceRole === undefined && /\bbuilder\b/.test(agent) && !readOnlyTask);
|
|
99
|
+
|| (routingRole === "writer" && !readOnlyTask);
|
|
102
100
|
const inferredReadOnly = readOnlyTask || (input.acceptanceRole === "read-only" && !taskMayWrite);
|
|
103
101
|
const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
|
|
104
102
|
const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { Message } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { OutputMode, SavedOutputReference } from "../../shared/types.ts";
|
|
4
|
+
import type { MaxOutputConfig, OutputMode, SavedOutputReference } from "../../shared/types.ts";
|
|
5
|
+
import { truncateOutput } from "../../shared/types.ts";
|
|
5
6
|
import { hasMutationToolCapability } from "./completion-guard.ts";
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Fixed internal context-fallback cap used when persisted output is unavailable
|
|
10
|
+
* (persistence/read failure, `output: false`, or legacy results without a saved
|
|
11
|
+
* path). Deliberately not exposed as a public parameter; reuse `truncateOutput()`
|
|
12
|
+
* so the excerpt is bounded by both lines and bytes and never falls back to
|
|
13
|
+
* unbounded output.
|
|
14
|
+
*/
|
|
15
|
+
export const CONTEXT_FALLBACK_LIMIT: Required<MaxOutputConfig> = { bytes: 4096, lines: 80 };
|
|
16
|
+
|
|
7
17
|
export interface SingleOutputSnapshot {
|
|
8
18
|
exists: boolean;
|
|
9
19
|
mtimeMs?: number;
|
|
@@ -125,6 +135,34 @@ function formatByteSize(bytes: number): string {
|
|
|
125
135
|
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Bounded fallback for deliveries where the durable full output cannot be
|
|
140
|
+
* referenced: persistence/write errors, unreadable saved paths, or no saved
|
|
141
|
+
* path at all (legacy/`output: false`). Contains process status, the intended
|
|
142
|
+
* output path, the concrete error, and a bounded 80-line/4-KiB excerpt of the
|
|
143
|
+
* available output. Never claims a full artifact exists.
|
|
144
|
+
*/
|
|
145
|
+
export function formatBoundedPersistenceFallback(input: {
|
|
146
|
+
error: string;
|
|
147
|
+
outputPath?: string;
|
|
148
|
+
fullOutput: string;
|
|
149
|
+
exitCode: number;
|
|
150
|
+
processError?: string;
|
|
151
|
+
}): string {
|
|
152
|
+
const status = input.processError?.trim()
|
|
153
|
+
? `Process status: ${input.processError}`
|
|
154
|
+
: `Process status: ${input.exitCode === 0 ? "completed" : `failed (exit ${input.exitCode})`}`;
|
|
155
|
+
const bounded = truncateOutput(input.fullOutput, CONTEXT_FALLBACK_LIMIT);
|
|
156
|
+
const lines = [
|
|
157
|
+
`[Full output unavailable] ${input.error}`,
|
|
158
|
+
status,
|
|
159
|
+
...(input.outputPath ? [`Intended output path: ${input.outputPath}`] : []),
|
|
160
|
+
"Full output is unavailable; showing a bounded excerpt (first 80 lines / 4 KiB).",
|
|
161
|
+
bounded.text,
|
|
162
|
+
];
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
128
166
|
export function formatSavedOutputReference(savedPath: string, fullOutput: string): SavedOutputReference {
|
|
129
167
|
const absolutePath = path.resolve(savedPath);
|
|
130
168
|
const bytes = Buffer.byteLength(fullOutput, "utf-8");
|
|
@@ -217,19 +255,35 @@ export function finalizeSingleOutput(params: {
|
|
|
217
255
|
savedPath?: string;
|
|
218
256
|
outputReference?: SavedOutputReference;
|
|
219
257
|
saveError?: string;
|
|
258
|
+
error?: string;
|
|
220
259
|
}): { displayOutput: string; savedPath?: string; outputReference?: SavedOutputReference; saveError?: string } {
|
|
221
|
-
|
|
222
|
-
if (params.exitCode === 0 && params.savedPath) {
|
|
260
|
+
if (params.savedPath) {
|
|
223
261
|
const outputReference = params.outputReference ?? formatSavedOutputReference(params.savedPath, params.fullOutput);
|
|
224
|
-
if (params.outputMode === "file-only") {
|
|
262
|
+
if (params.exitCode === 0 && params.outputMode === "file-only") {
|
|
225
263
|
return { displayOutput: outputReference.message, savedPath: params.savedPath, outputReference };
|
|
226
264
|
}
|
|
227
|
-
|
|
265
|
+
if (params.exitCode !== 0) {
|
|
266
|
+
// Failed runs with a successfully persisted result surface the error/status
|
|
267
|
+
// plus the saved-output reference, never raw child output.
|
|
268
|
+
const status = params.error?.trim() || `Subagent failed with exit code ${params.exitCode}`;
|
|
269
|
+
return { displayOutput: `${status}\n\n${outputReference.message}`, savedPath: params.savedPath, outputReference };
|
|
270
|
+
}
|
|
271
|
+
const displayOutput = `${params.truncatedOutput || params.fullOutput}\n\n${outputReference.message}`;
|
|
228
272
|
return { displayOutput, savedPath: params.savedPath, outputReference };
|
|
229
273
|
}
|
|
230
|
-
if (params.
|
|
231
|
-
|
|
232
|
-
|
|
274
|
+
if (params.saveError && params.outputPath) {
|
|
275
|
+
// Persistence/read failure: only the bounded fallback is delivered. It must
|
|
276
|
+
// not claim a full artifact exists.
|
|
277
|
+
return {
|
|
278
|
+
displayOutput: formatBoundedPersistenceFallback({
|
|
279
|
+
error: params.saveError,
|
|
280
|
+
outputPath: params.outputPath,
|
|
281
|
+
fullOutput: params.fullOutput,
|
|
282
|
+
exitCode: params.exitCode,
|
|
283
|
+
processError: params.error,
|
|
284
|
+
}),
|
|
285
|
+
saveError: params.saveError,
|
|
286
|
+
};
|
|
233
287
|
}
|
|
234
|
-
return { displayOutput };
|
|
288
|
+
return { displayOutput: params.truncatedOutput || params.fullOutput };
|
|
235
289
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AcceptanceRole } from "../../shared/types.ts";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Shared task mutation-intent classifier.
|
|
3
5
|
*
|
|
@@ -139,6 +141,25 @@ function isReviewerStyleAgent(agent: string): boolean {
|
|
|
139
141
|
return /\b(?:architect|commentator|explorer|recapper|researcher)\b/i.test(agent);
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Resolve an agent's acceptance routing role: a declared `acceptanceRole`
|
|
146
|
+
* always wins; otherwise the established name heuristics apply (builder-named
|
|
147
|
+
* agents are writers; architect/commentator/explorer/recapper/researcher/
|
|
148
|
+
* analyst-named agents are read-only). This is the single shared source of
|
|
149
|
+
* truth for acceptance inference and task-aware advisory routing so the two
|
|
150
|
+
* cannot drift. Alias values are never role signals.
|
|
151
|
+
*/
|
|
152
|
+
export function resolveAgentRoutingRole(
|
|
153
|
+
agentName: string,
|
|
154
|
+
acceptanceRole?: AcceptanceRole,
|
|
155
|
+
): "writer" | "read-only" | undefined {
|
|
156
|
+
if (acceptanceRole !== undefined) return acceptanceRole;
|
|
157
|
+
const agent = agentName.toLowerCase();
|
|
158
|
+
if (/\bbuilder\b/.test(agent)) return "writer";
|
|
159
|
+
if (/\b(?:architect|commentator|explorer|recapper|researcher|analyst)\b/.test(agent)) return "read-only";
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
142
163
|
function hasImplementationIntent(agent: string, taskText: string): boolean {
|
|
143
164
|
if (isReviewerStyleAgent(agent)) return REVIEWER_REQUIRED_EDIT_PATTERNS.some((pattern) => pattern.test(taskText));
|
|
144
165
|
if (agent === "builder") return WORKER_IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(taskText));
|