@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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import type { Message } from "@earendil-works/pi-ai";
|
|
8
|
-
import type { AgentConfig } from "../agents/agents.ts";
|
|
8
|
+
import type { AgentConfig, AgentSource } from "../agents/agents.ts";
|
|
9
9
|
import type { FSWatcher } from "node:fs";
|
|
10
10
|
import type { ExtensionContext } from "@selesai/code";
|
|
11
11
|
import type { ModelScopeConfig } from "../runs/shared/model-scope.ts";
|
|
@@ -912,12 +912,51 @@ export interface SpawnBudgetSnapshot {
|
|
|
912
912
|
grantHistory: SpawnBudgetGrant[];
|
|
913
913
|
}
|
|
914
914
|
|
|
915
|
+
// ============================================================================
|
|
916
|
+
// Runtime catalog (action:list machine metadata)
|
|
917
|
+
// ============================================================================
|
|
918
|
+
|
|
919
|
+
/** Machine-readable catalog entry for one visible runtime agent. */
|
|
920
|
+
export interface CatalogAgentMetadata {
|
|
921
|
+
name: string;
|
|
922
|
+
source: AgentSource;
|
|
923
|
+
description: string;
|
|
924
|
+
/** false for capability-ceiling-restricted agents (still visible, not launchable). */
|
|
925
|
+
executable: boolean;
|
|
926
|
+
/** Present only when the agent is capability-ceiling-restricted. */
|
|
927
|
+
restrictionSources?: string[];
|
|
928
|
+
aliases?: string[];
|
|
929
|
+
/** Normalized to explicit "fresh" when the agent has no defaultContext. */
|
|
930
|
+
defaultContext: "fresh" | "fork";
|
|
931
|
+
acceptanceRole?: AcceptanceRole;
|
|
932
|
+
/** Effective declared tools: normal tools plus mcp:-prefixed direct MCP tools. */
|
|
933
|
+
tools?: string[];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** Machine-readable catalog entry for one visible chain. */
|
|
937
|
+
export interface CatalogChainMetadata {
|
|
938
|
+
name: string;
|
|
939
|
+
source: AgentSource;
|
|
940
|
+
description: string;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** Versioned action:list catalog mirroring the human list output. */
|
|
944
|
+
export interface CatalogMetadataV1 {
|
|
945
|
+
version: 1;
|
|
946
|
+
agents: CatalogAgentMetadata[];
|
|
947
|
+
chains: CatalogChainMetadata[];
|
|
948
|
+
/** Present only when a capability ceiling restricted visible agents. */
|
|
949
|
+
capabilityCeilingSources?: string[];
|
|
950
|
+
}
|
|
951
|
+
|
|
915
952
|
export interface Details {
|
|
916
953
|
mode: SubagentRunMode | "management";
|
|
917
954
|
runId?: string;
|
|
918
955
|
/** Run-level context summary. "mixed" when children resolved to different modes. */
|
|
919
956
|
context?: "fresh" | "fork" | "mixed";
|
|
920
957
|
results: SingleResult[];
|
|
958
|
+
/** Runtime-resolved human+machine catalog for { action: "list" } results. */
|
|
959
|
+
catalog?: CatalogMetadataV1;
|
|
921
960
|
controlEvents?: ControlEvent[];
|
|
922
961
|
steering?: SteerActionResult;
|
|
923
962
|
asyncId?: string;
|
|
@@ -1645,7 +1684,7 @@ export const DEFAULT_MAX_OUTPUT: Required<MaxOutputConfig> = {
|
|
|
1645
1684
|
};
|
|
1646
1685
|
|
|
1647
1686
|
export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
|
|
1648
|
-
enabled:
|
|
1687
|
+
enabled: false,
|
|
1649
1688
|
dir: "project",
|
|
1650
1689
|
includeInput: true,
|
|
1651
1690
|
includeOutput: true,
|
|
@@ -7,7 +7,7 @@ import * as os from "node:os";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
9
|
import { formatToolCall } from "./formatters.ts";
|
|
10
|
-
import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
|
|
10
|
+
import type { AgentProgress, AsyncStatus, ChainOutputMap, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
|
|
11
11
|
|
|
12
12
|
// ============================================================================
|
|
13
13
|
// File System Utilities
|
|
@@ -414,12 +414,40 @@ export function compactForegroundResult(result: SingleResult): SingleResult {
|
|
|
414
414
|
messages: undefined,
|
|
415
415
|
progress: undefined,
|
|
416
416
|
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
417
|
+
// Reference-first terminal details: once an authoritative saved output path
|
|
418
|
+
// exists, drop the raw final output and truncation marker from the model-
|
|
419
|
+
// visible projection; consumers recover the file from `savedOutputPath`.
|
|
420
|
+
// Explicit `outputMode: "inline"` is the sole legacy full-text opt-out and
|
|
421
|
+
// keeps its final output in the terminal projection (e.g. delegation v1
|
|
422
|
+
// `response.output` stays populated).
|
|
423
|
+
finalOutput: result.savedOutputPath && result.outputMode !== "inline" ? undefined : result.finalOutput,
|
|
424
|
+
truncation: result.savedOutputPath && result.outputMode !== "inline" ? undefined : result.truncation,
|
|
417
425
|
};
|
|
418
426
|
}
|
|
419
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Strip chain `details.outputs` text/structured payloads from the terminal
|
|
430
|
+
* projection while retaining the output names and step metadata. Chain output
|
|
431
|
+
* bindings themselves remain reference-first in the completion content and
|
|
432
|
+
* `{outputs.name}` interpolation (see outputEntryFromResult).
|
|
433
|
+
*/
|
|
434
|
+
function compactChainOutputs(outputs: ChainOutputMap | undefined): ChainOutputMap | undefined {
|
|
435
|
+
if (!outputs) return undefined;
|
|
436
|
+
const compact: ChainOutputMap = {};
|
|
437
|
+
for (const [name, entry] of Object.entries(outputs)) {
|
|
438
|
+
compact[name] = {
|
|
439
|
+
agent: entry.agent,
|
|
440
|
+
stepIndex: entry.stepIndex,
|
|
441
|
+
text: "",
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return compact;
|
|
445
|
+
}
|
|
446
|
+
|
|
420
447
|
export function compactForegroundDetails(details: Details): Details {
|
|
421
448
|
return {
|
|
422
449
|
...details,
|
|
450
|
+
outputs: compactChainOutputs(details.outputs),
|
|
423
451
|
results: details.results.map(compactForegroundResult),
|
|
424
452
|
progress: details.progress
|
|
425
453
|
? details.progress.map(compactCompletedProgress)
|
|
@@ -375,7 +375,11 @@ export function toSubagentDelegationExecutionParams(request: SubagentDelegationR
|
|
|
375
375
|
toolBudget: request.toolBudget,
|
|
376
376
|
skill: request.skill,
|
|
377
377
|
output: request.output,
|
|
378
|
-
outputMode:
|
|
378
|
+
// v1 has no default for outputMode: preserve the legacy full-text contract by
|
|
379
|
+
// resolving an omitted outputMode to explicit inline (response.output stays
|
|
380
|
+
// populated). The model-facing tool's mode-dependent reference-first default
|
|
381
|
+
// (omitted outputMode -> file-only) is intentionally NOT applied here.
|
|
382
|
+
outputMode: request.outputMode ?? "inline",
|
|
379
383
|
outputSchema: request.outputSchema,
|
|
380
384
|
agentContract: request.agentContract,
|
|
381
385
|
acceptance: request.acceptance,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Rendering functions for subagent results
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import * as fs from "node:fs";
|
|
5
6
|
import * as path from "node:path";
|
|
6
7
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
7
8
|
import { getMarkdownTheme, keyText, type ExtensionContext } from "@selesai/code";
|
|
@@ -27,6 +28,27 @@ import { contextModeBadge, contextModePrefix } from "../runs/shared/context-mode
|
|
|
27
28
|
|
|
28
29
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* UI-side output projection: completed terminal results strip `finalOutput`/
|
|
33
|
+
* `truncation` when an authoritative saved output path exists (see
|
|
34
|
+
* compactForegroundResult). The UI recovers the full text from that path so
|
|
35
|
+
* widgets keep showing output without reintroducing it into model-facing
|
|
36
|
+
* details.
|
|
37
|
+
*/
|
|
38
|
+
function resultOutputForUi(r: Details["results"][number]): string {
|
|
39
|
+
const output = r.truncation?.text || getSingleResultOutput(r);
|
|
40
|
+
if (output) return output;
|
|
41
|
+
if (r.savedOutputPath) {
|
|
42
|
+
try {
|
|
43
|
+
const content = fs.readFileSync(r.savedOutputPath, "utf-8").trim();
|
|
44
|
+
return content || output;
|
|
45
|
+
} catch {
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return output;
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
function liveDetailKeyText(): string {
|
|
31
53
|
return keyText("app.tools.expand");
|
|
32
54
|
}
|
|
@@ -1308,7 +1330,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
1308
1330
|
}
|
|
1309
1331
|
|
|
1310
1332
|
function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, frame?: number): Component {
|
|
1311
|
-
const output =
|
|
1333
|
+
const output = resultOutputForUi(r);
|
|
1312
1334
|
const progress = r.progress || r.progressSummary;
|
|
1313
1335
|
const isRunning = r.progress?.status === "running";
|
|
1314
1336
|
const contextBadge = contextModeBadge(theme, r.context ?? d.context);
|
|
@@ -1418,7 +1440,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
|
|
|
1418
1440
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
|
|
1419
1441
|
continue;
|
|
1420
1442
|
}
|
|
1421
|
-
const output =
|
|
1443
|
+
const output = resultOutputForUi(r);
|
|
1422
1444
|
const progressFromArray = d.progress?.find((p) => p.index === i) || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
|
|
1423
1445
|
const rProg = r.progress || progressFromArray || r.progressSummary;
|
|
1424
1446
|
const rRunning = rProg && "status" in rProg && rProg.status === "running";
|
|
@@ -1500,7 +1522,7 @@ export function renderSubagentResult(
|
|
|
1500
1522
|
? theme.fg("success", "ok")
|
|
1501
1523
|
: theme.fg("error", "failed");
|
|
1502
1524
|
const contextBadge = contextModeBadge(theme, r.context ?? d.context);
|
|
1503
|
-
const output =
|
|
1525
|
+
const output = resultOutputForUi(r);
|
|
1504
1526
|
|
|
1505
1527
|
const progressInfo = isRunning && r.progress
|
|
1506
1528
|
? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
|
|
@@ -1600,7 +1622,7 @@ export function renderSubagentResult(
|
|
|
1600
1622
|
const hasEmptyWithoutTarget = d.results.some((r) =>
|
|
1601
1623
|
r.exitCode === 0
|
|
1602
1624
|
&& r.progress?.status !== "running"
|
|
1603
|
-
&& hasEmptyTextOutputWithoutOutputTarget(r.task,
|
|
1625
|
+
&& hasEmptyTextOutputWithoutOutputTarget(r.task, resultOutputForUi(r)),
|
|
1604
1626
|
);
|
|
1605
1627
|
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"]);
|
|
1606
1628
|
const hasWorkflowStop = d.results.some((r) => r.stopped && r.progress?.status !== "running") || workflowGraphHasStatus(d, ["stopped"]);
|
|
@@ -1658,7 +1680,7 @@ export function renderSubagentResult(
|
|
|
1658
1680
|
const isComplete = result && result.exitCode === 0 && result.progress?.status !== "running";
|
|
1659
1681
|
const isEmptyWithoutTarget = Boolean(result)
|
|
1660
1682
|
&& Boolean(isComplete)
|
|
1661
|
-
&& hasEmptyTextOutputWithoutOutputTarget(result.task,
|
|
1683
|
+
&& hasEmptyTextOutputWithoutOutputTarget(result.task, resultOutputForUi(result));
|
|
1662
1684
|
const isCurrent = i === (d.currentStepIndex ?? d.results.length);
|
|
1663
1685
|
const stepIcon = isFailed
|
|
1664
1686
|
? theme.fg("error", "failed")
|
|
@@ -1729,7 +1751,7 @@ export function renderSubagentResult(
|
|
|
1729
1751
|
const rRunning = rProg?.status === "running";
|
|
1730
1752
|
const stepNumber = typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
|
|
1731
1753
|
|
|
1732
|
-
const resultOutput =
|
|
1754
|
+
const resultOutput = resultOutputForUi(r);
|
|
1733
1755
|
const statusIcon = rRunning
|
|
1734
1756
|
? theme.fg("warning", "running")
|
|
1735
1757
|
: r.exitCode !== 0
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { afterEach, describe, it } from "node:test";
|
|
15
15
|
import assert from "node:assert/strict";
|
|
16
|
+
import * as fs from "node:fs";
|
|
16
17
|
import * as os from "node:os";
|
|
17
18
|
import * as path from "node:path";
|
|
18
19
|
import { tryImport } from "../support/helpers.ts";
|
|
@@ -23,6 +24,17 @@ const piAi = await tryImport<unknown>("@earendil-works/pi-ai");
|
|
|
23
24
|
const available = Boolean(piCodingAgent && piAi);
|
|
24
25
|
|
|
25
26
|
const CHILD_MARKER = "CHILD_REAL_SESSION_OK";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reference-first tool results: completion content carries "Output saved to:
|
|
30
|
+
* <path> (…)". Read the durable file to inspect the full child output.
|
|
31
|
+
*/
|
|
32
|
+
function readSavedOutput(text: string): string {
|
|
33
|
+
const rest = text.split("Output saved to: ")[1];
|
|
34
|
+
assert.ok(rest, `expected a saved-output reference in: ${text.slice(0, 160)}`);
|
|
35
|
+
const outputPath = rest.split(" (")[0]!;
|
|
36
|
+
return fs.readFileSync(outputPath, "utf-8");
|
|
37
|
+
}
|
|
26
38
|
// Env vars the runner must clear so a parent that was itself spawned as a
|
|
27
39
|
// subagent child can still launch fresh children. The values are deliberately
|
|
28
40
|
// bogus sentinels (nonexistent paths) so a leaked value would break spawning.
|
|
@@ -122,10 +134,14 @@ Use the available tools.`;
|
|
|
122
134
|
const chainDetails = JSON.stringify((toolMessages[1] as { details?: unknown } | undefined)?.details);
|
|
123
135
|
const structuredDetails = JSON.stringify((toolMessages[2] as { details?: unknown } | undefined)?.details);
|
|
124
136
|
assert.equal(results.length, 4);
|
|
125
|
-
|
|
126
|
-
assert.match(
|
|
127
|
-
assert.match(
|
|
128
|
-
|
|
137
|
+
const directOutput = readSavedOutput(results[0] ?? "");
|
|
138
|
+
assert.match(directOutput, /ACTIVE_TOOLS:[^\n]*fixture_search/);
|
|
139
|
+
assert.match(directOutput, /ACTIVE_TOOLS:[^\n]*read/);
|
|
140
|
+
const chainFirstChild = (toolMessages[1] as { details?: { results?: Array<{ savedOutputPath?: string }> } } | undefined)?.details?.results?.[0];
|
|
141
|
+
assert.ok(chainFirstChild?.savedOutputPath, "chain details should carry the saved output path");
|
|
142
|
+
const chainOutput = fs.readFileSync(chainFirstChild.savedOutputPath, "utf-8");
|
|
143
|
+
assert.match(chainOutput, /ACTIVE_TOOLS:[^\n]*fixture_search/);
|
|
144
|
+
assert.match(chainOutput, /ACTIVE_TOOLS:[^\n]*read/);
|
|
129
145
|
assert.match(structuredDetails, /STRUCTURED_OUTPUT_OK/);
|
|
130
146
|
assert.match(results[3] ?? "", /requested unavailable child tools: missing_search/);
|
|
131
147
|
assert.match(results[3] ?? "", /subagentOnlyExtensions/);
|
|
@@ -172,7 +188,8 @@ Report active tools.`;
|
|
|
172
188
|
|
|
173
189
|
const results = subagentToolResults(run.parentSession);
|
|
174
190
|
assert.equal(results.length, 1);
|
|
175
|
-
|
|
191
|
+
const asyncOutput = readSavedOutput(results[0] ?? "");
|
|
192
|
+
assert.match(asyncOutput, /ACTIVE_TOOLS:[^\n]*fixture_async_search/);
|
|
176
193
|
assert.doesNotMatch(results[0] ?? "", /requested unavailable child tools/);
|
|
177
194
|
});
|
|
178
195
|
|
|
@@ -206,7 +223,7 @@ Report active tools.`;
|
|
|
206
223
|
|
|
207
224
|
const toolResults = subagentToolResults(run.parentSession);
|
|
208
225
|
assert.equal(toolResults.length, 1);
|
|
209
|
-
assert.match(toolResults[0]
|
|
226
|
+
assert.match(readSavedOutput(toolResults[0]!), new RegExp(CHILD_MARKER));
|
|
210
227
|
assert.match(run.responseText, new RegExp(CHILD_MARKER));
|
|
211
228
|
assert.doesNotMatch(run.responseText, /CHILD_MISSING/);
|
|
212
229
|
assert.ok(run.modelCalls >= 2, `expected parent tool-call and final turns, got ${run.modelCalls}`);
|
|
@@ -219,4 +236,92 @@ Report active tools.`;
|
|
|
219
236
|
}
|
|
220
237
|
}
|
|
221
238
|
});
|
|
239
|
+
|
|
240
|
+
function latestSubagentToolResultText(messages: Array<{ role?: string; toolName?: string; content?: unknown }>): string | undefined {
|
|
241
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
242
|
+
const message = messages[i]!;
|
|
243
|
+
if (message.role === "toolResult" && message.toolName === "subagent") {
|
|
244
|
+
return Array.isArray(message.content)
|
|
245
|
+
? message.content
|
|
246
|
+
.map((part) => part && typeof part === "object" && (part as { type?: unknown }).type === "text"
|
|
247
|
+
? String((part as { text?: unknown }).text ?? "")
|
|
248
|
+
: "")
|
|
249
|
+
.join("")
|
|
250
|
+
: "";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
it("lists then delegates to a non-bundled discovered writer in a broad-mutation request", async () => {
|
|
257
|
+
const { runRealSubagentSession, subagentCall, subagentToolResults } = await import("../support/real-session-runner.ts");
|
|
258
|
+
const writerAgent = `---
|
|
259
|
+
name: fixture-writer
|
|
260
|
+
description: Scoped mutation-capable fixture writer
|
|
261
|
+
aliases: fw
|
|
262
|
+
tools: read, grep, find, ls, bash, edit, write
|
|
263
|
+
acceptanceRole: writer
|
|
264
|
+
defaultContext: fork
|
|
265
|
+
completionGuard: false
|
|
266
|
+
---
|
|
267
|
+
Implement the scoped fixture change and return the marker.`;
|
|
268
|
+
|
|
269
|
+
run = await runRealSubagentSession({
|
|
270
|
+
prompt: "Implement the fixture change across the codebase.",
|
|
271
|
+
childText: CHILD_MARKER,
|
|
272
|
+
projectFiles: {
|
|
273
|
+
".selesai/agents/fixture-writer.md": writerAgent,
|
|
274
|
+
},
|
|
275
|
+
respond(context) {
|
|
276
|
+
const messages = context.messages as Array<{ role?: string; toolName?: string; content?: unknown; details?: unknown }>;
|
|
277
|
+
const subagentResults = messages.filter((message) => message.role === "toolResult" && message.toolName === "subagent");
|
|
278
|
+
if (subagentResults.length === 0) {
|
|
279
|
+
return subagentCall({ action: "list", agentScope: "project" }, "call-list-writer");
|
|
280
|
+
}
|
|
281
|
+
if (subagentResults.length === 1) {
|
|
282
|
+
const listText = latestSubagentToolResultText(messages) ?? "";
|
|
283
|
+
assert.match(
|
|
284
|
+
listText,
|
|
285
|
+
/- fixture-writer \(project, context: fork, role: writer, aliases: fw, tools: read, grep, find, ls, bash, edit, write\)/,
|
|
286
|
+
"catalog must expose the custom writer with its runtime metadata",
|
|
287
|
+
);
|
|
288
|
+
const listedDetails = JSON.stringify(subagentResults.at(-1)?.details ?? {});
|
|
289
|
+
assert.match(listedDetails, /"catalog"/);
|
|
290
|
+
assert.match(listedDetails, /"fixture-writer"/);
|
|
291
|
+
assert.match(listedDetails, /"acceptanceRole":"writer"/);
|
|
292
|
+
return subagentCall(
|
|
293
|
+
{ agent: "fixture-writer", task: "Implement the change and return the marker.", context: "fresh", agentScope: "project" },
|
|
294
|
+
"call-fixture-writer",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return "Broad mutation work complete.";
|
|
298
|
+
},
|
|
299
|
+
timeoutMs: 60_000,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const results = subagentToolResults(run.parentSession);
|
|
303
|
+
assert.equal(results.length, 2);
|
|
304
|
+
assert.match(results[0] ?? "", /fixture-writer \(project, context: fork, role: writer/);
|
|
305
|
+
assert.match(readSavedOutput(results[1] ?? ""), new RegExp(CHILD_MARKER));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("keeps tiny targeted reads local without a subagent call", async () => {
|
|
309
|
+
const { runRealSubagentSession, subagentToolResults } = await import("../support/real-session-runner.ts");
|
|
310
|
+
run = await runRealSubagentSession({
|
|
311
|
+
prompt: "What does the README say about subagents?",
|
|
312
|
+
childText: CHILD_MARKER,
|
|
313
|
+
projectFiles: {
|
|
314
|
+
"README.md": "Subagents are delegated workers.",
|
|
315
|
+
},
|
|
316
|
+
respond() {
|
|
317
|
+
return "The README says: Subagents are delegated workers.";
|
|
318
|
+
},
|
|
319
|
+
timeoutMs: 60_000,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const results = subagentToolResults(run.parentSession);
|
|
323
|
+
assert.equal(results.length, 0);
|
|
324
|
+
assert.match(run.responseText, /Subagents are delegated workers/);
|
|
325
|
+
assert.ok(run.modelCalls >= 1, `expected at least one parent turn, got ${run.modelCalls}`);
|
|
326
|
+
});
|
|
222
327
|
});
|