@selesai/code 0.13.31 → 0.13.33
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 +21 -0
- package/dist/core/remote-catalog-provider.js +5 -1
- package/dist/core/remote-catalog-provider.test.d.ts +1 -0
- package/dist/core/remote-catalog-provider.test.js +29 -0
- package/dist/extensions/capability-gateway/index.ts +9 -4
- package/dist/extensions/capability-gateway/integration.test.ts +69 -3
- package/dist/extensions/pi-subagents/agents/worker.md +3 -2
- package/dist/extensions/pi-subagents/docs/agents.md +2 -0
- package/dist/extensions/pi-subagents/docs/extension-api.md +3 -1
- package/dist/extensions/pi-subagents/docs/observability.md +2 -0
- package/dist/extensions/pi-subagents/docs/tool-reference.md +2 -2
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +10 -1
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +3 -21
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +10 -3
- package/dist/extensions/pi-subagents/src/runs/background/workflow-terminal-proof.ts +67 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +67 -5
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +6 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/git-environment.ts +29 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +69 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +20 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +4 -238
- package/dist/extensions/pi-subagents/src/slash/subagent-cost.ts +280 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +67 -0
- package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +6 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.part-2.test.ts +25 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +5 -5
- package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/child-tool-plan-permission-system.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +11 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +25 -0
- package/dist/extensions/pi-subagents/test/unit/git-environment.test.ts +38 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +105 -1
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/structured-output-rejection.test.ts +67 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-terminal-proof.test.ts +98 -0
- package/dist/extensions/pi-web-agent/src/extension.ts +2 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +8 -8
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +1 -1
- package/package.json +3 -3
|
@@ -71,7 +71,7 @@ import { deriveChildSessionName } from "../../shared/child-session-name.ts";
|
|
|
71
71
|
import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
72
72
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
73
73
|
import { assertThinkingWithinCeiling, intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
|
|
74
|
-
import { MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_CALL_ERROR } from "../shared/structured-output.ts";
|
|
74
|
+
import { formatStructuredOutputRejectionError, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_CALL_ERROR } from "../shared/structured-output.ts";
|
|
75
75
|
import { formatMidToolExitError, isOrdinaryToolForMidToolExit } from "../shared/process-signal.ts";
|
|
76
76
|
import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
|
|
77
77
|
import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
|
|
@@ -1431,10 +1431,14 @@ async function runSingleAttempt(
|
|
|
1431
1431
|
result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
|
|
1432
1432
|
result.structuredOutputPath = options.structuredOutput.outputPath;
|
|
1433
1433
|
const structured = capture.structuredOutput();
|
|
1434
|
-
if (!structuredOutputToolInvoked
|
|
1434
|
+
if (!structuredOutputToolInvoked) {
|
|
1435
1435
|
result.exitCode = 1;
|
|
1436
1436
|
result.error = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
|
|
1437
1437
|
result.structuredOutputFailed = true;
|
|
1438
|
+
} else if (!structured.called) {
|
|
1439
|
+
result.exitCode = 1;
|
|
1440
|
+
result.error = formatStructuredOutputRejectionError(result.messages ?? []);
|
|
1441
|
+
result.structuredOutputFailed = true;
|
|
1438
1442
|
} else {
|
|
1439
1443
|
result.structuredOutput = structured.value;
|
|
1440
1444
|
const acceptanceMode = options.structuredOutput.acceptanceReportPath
|
|
@@ -30,6 +30,17 @@ import {
|
|
|
30
30
|
} from "./capability-ceiling.ts";
|
|
31
31
|
|
|
32
32
|
const MAX_LAUNCH_RESOLVED_EXTENSION_IDS = 32;
|
|
33
|
+
export const CAPABILITY_GATEWAY_EXTENSION_PATH = path.join(
|
|
34
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
35
|
+
"..",
|
|
36
|
+
"..",
|
|
37
|
+
"..",
|
|
38
|
+
"..",
|
|
39
|
+
"capability-gateway",
|
|
40
|
+
"index.ts",
|
|
41
|
+
);
|
|
42
|
+
const CAPABILITY_GATEWAY_TOOL_NAMES = ["capability_catalog", "capability_discover", "capability_skill_show"] as const;
|
|
43
|
+
const CAPABILITY_GATEWAY_ENABLED_ENV = "SELESAI_CAPABILITY_GATEWAY";
|
|
33
44
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.join(
|
|
34
45
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
35
46
|
"subagent-prompt-runtime.ts",
|
|
@@ -172,6 +183,8 @@ export interface PiLaunchToolPlan {
|
|
|
172
183
|
extensionArgs: string[];
|
|
173
184
|
disableAmbientExtensions: boolean;
|
|
174
185
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
186
|
+
/** Whether the capability gateway is loaded for this child's permitted extension/tool policy. */
|
|
187
|
+
capabilityGatewayEnabled: boolean;
|
|
175
188
|
/** Non-fatal launch warnings; they do not change behavior. */
|
|
176
189
|
warnings: string[];
|
|
177
190
|
}
|
|
@@ -195,6 +208,14 @@ function hasPermissionRules(rules: PermissionRules | undefined): boolean {
|
|
|
195
208
|
return rules !== undefined && Object.keys(rules).length > 0;
|
|
196
209
|
}
|
|
197
210
|
|
|
211
|
+
function isCapabilityGatewayExtensionPath(value: string, cwd?: string): boolean {
|
|
212
|
+
const resolved = path.normalize(path.resolve(cwd ?? process.cwd(), value));
|
|
213
|
+
return (
|
|
214
|
+
resolved === path.normalize(CAPABILITY_GATEWAY_EXTENSION_PATH) ||
|
|
215
|
+
resolved === path.normalize(path.dirname(CAPABILITY_GATEWAY_EXTENSION_PATH))
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
198
219
|
/**
|
|
199
220
|
* Children are pi sessions inside the parent or the runner process; a spawned
|
|
200
221
|
* `pi` received extra MCP server definitions as a CLI argument, but a session
|
|
@@ -330,6 +351,9 @@ export function resolvePiLaunchToolPlan(
|
|
|
330
351
|
!excludedToolSet.has("subagent") &&
|
|
331
352
|
(!allowedToolSet || allowedToolSet.has("subagent"))
|
|
332
353
|
);
|
|
354
|
+
if (effectiveDeclaredBuiltinTools.includes("subagent_supervisor") && !fanoutAuthorized) {
|
|
355
|
+
throw new Error("Tool 'subagent_supervisor' requires fanout authorization: include 'subagent' in the effective tools allowlist or enable allowNestedSubagents.");
|
|
356
|
+
}
|
|
333
357
|
const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
|
|
334
358
|
? []
|
|
335
359
|
: (input.tools ?? []).filter(
|
|
@@ -363,13 +387,41 @@ export function resolvePiLaunchToolPlan(
|
|
|
363
387
|
(input.mcpDirectTools?.length ?? 0) > 0 ||
|
|
364
388
|
allowedToolSet !== undefined;
|
|
365
389
|
const internalTools = (input.structuredOutput ? ["structured_output"] : []).filter((tool) => !excludedToolSet.has(tool));
|
|
366
|
-
const
|
|
390
|
+
const baseEffectiveToolAllowlist = [
|
|
367
391
|
...new Set([
|
|
368
392
|
...effectiveDeclaredBuiltinTools,
|
|
369
393
|
...effectiveMcpTools,
|
|
370
394
|
...internalTools,
|
|
371
395
|
]),
|
|
372
396
|
];
|
|
397
|
+
const explicitlyNoTools =
|
|
398
|
+
explicitToolAllowlist &&
|
|
399
|
+
effectiveDeclaredBuiltinTools.length === 0 &&
|
|
400
|
+
effectiveMcpTools.length === 0;
|
|
401
|
+
const gatewayExplicitlyConfigured = [
|
|
402
|
+
...toolExtensionPaths,
|
|
403
|
+
...(input.extensions ?? []),
|
|
404
|
+
...(input.subagentOnlyExtensions ?? []),
|
|
405
|
+
].some((extensionPath) => isCapabilityGatewayExtensionPath(extensionPath, input.cwd));
|
|
406
|
+
const gatewayExtensionPolicyAllows =
|
|
407
|
+
capabilityCeiling?.denyExtensions !== true &&
|
|
408
|
+
fs.existsSync(CAPABILITY_GATEWAY_EXTENSION_PATH) &&
|
|
409
|
+
(input.extensions === undefined || gatewayExplicitlyConfigured);
|
|
410
|
+
const permittedGatewayTools = CAPABILITY_GATEWAY_TOOL_NAMES.filter(
|
|
411
|
+
(name) => (!allowedToolSet || allowedToolSet.has(name)) && !excludedToolSet.has(name),
|
|
412
|
+
);
|
|
413
|
+
const capabilityGatewayEnabled =
|
|
414
|
+
gatewayExtensionPolicyAllows &&
|
|
415
|
+
!explicitlyNoTools &&
|
|
416
|
+
permittedGatewayTools.length > 0 &&
|
|
417
|
+
process.env[CAPABILITY_GATEWAY_ENABLED_ENV] !== "0";
|
|
418
|
+
const gatewayTools = capabilityGatewayEnabled ? permittedGatewayTools : [];
|
|
419
|
+
const effectiveToolAllowlist = [
|
|
420
|
+
...new Set([
|
|
421
|
+
...baseEffectiveToolAllowlist,
|
|
422
|
+
...(explicitToolAllowlist ? gatewayTools : []),
|
|
423
|
+
]),
|
|
424
|
+
];
|
|
373
425
|
// Supervisor-coordination names stay in the --tools allowlist but are never
|
|
374
426
|
// strict requirements: children register contact_supervisor at runtime through
|
|
375
427
|
// the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
|
|
@@ -383,6 +435,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
383
435
|
...(input.tools !== undefined ? effectiveDeclaredBuiltinTools : []),
|
|
384
436
|
...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
|
|
385
437
|
...internalTools,
|
|
438
|
+
...gatewayTools,
|
|
386
439
|
].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
|
|
387
440
|
]
|
|
388
441
|
: [];
|
|
@@ -395,6 +448,9 @@ export function resolvePiLaunchToolPlan(
|
|
|
395
448
|
const fastModeExtensions = resolveFastModeExtension({ fast: input.fast, model: input.model, modelCandidates: input.modelCandidates, agentName: input.agentName });
|
|
396
449
|
const runtimeExtensions = [
|
|
397
450
|
PROMPT_RUNTIME_EXTENSION_PATH,
|
|
451
|
+
...(capabilityGatewayEnabled && !gatewayExplicitlyConfigured
|
|
452
|
+
? [CAPABILITY_GATEWAY_EXTENSION_PATH]
|
|
453
|
+
: []),
|
|
398
454
|
...fastModeExtensions,
|
|
399
455
|
...(fanoutAuthorized ? [FANOUT_CHILD_EXTENSION_PATH] : []),
|
|
400
456
|
...(permSystemExt ? [permSystemExt] : []),
|
|
@@ -412,20 +468,25 @@ export function resolvePiLaunchToolPlan(
|
|
|
412
468
|
+ "List the extensions this child actually needs instead of an empty array.",
|
|
413
469
|
);
|
|
414
470
|
}
|
|
471
|
+
const gatewayFilteredExtensions = (extensions: string[]) =>
|
|
472
|
+
extensions.filter(
|
|
473
|
+
(extensionPath) =>
|
|
474
|
+
!isCapabilityGatewayExtensionPath(extensionPath, input.cwd) || capabilityGatewayEnabled,
|
|
475
|
+
);
|
|
415
476
|
const configuredExtensions = capabilityCeiling?.denyExtensions
|
|
416
477
|
? []
|
|
417
|
-
: [
|
|
478
|
+
: gatewayFilteredExtensions([
|
|
418
479
|
...toolExtensionPaths,
|
|
419
480
|
...(input.extensions ?? []),
|
|
420
481
|
...(input.subagentOnlyExtensions ?? []),
|
|
421
|
-
];
|
|
482
|
+
]);
|
|
422
483
|
const extensionArgs = disableAmbientExtensions
|
|
423
484
|
? [...new Set([...runtimeExtensions, ...configuredExtensions])]
|
|
424
485
|
: [
|
|
425
486
|
...new Set([
|
|
426
487
|
...runtimeExtensions,
|
|
427
|
-
...toolExtensionPaths,
|
|
428
|
-
...(input.subagentOnlyExtensions ?? []),
|
|
488
|
+
...gatewayFilteredExtensions(toolExtensionPaths),
|
|
489
|
+
...gatewayFilteredExtensions(input.subagentOnlyExtensions ?? []),
|
|
429
490
|
]),
|
|
430
491
|
];
|
|
431
492
|
const requestedToolNames =
|
|
@@ -494,6 +555,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
494
555
|
configuredExtensions,
|
|
495
556
|
extensionArgs,
|
|
496
557
|
disableAmbientExtensions,
|
|
558
|
+
capabilityGatewayEnabled,
|
|
497
559
|
warnings,
|
|
498
560
|
...(capabilityAudit ? { capabilityAudit } : {}),
|
|
499
561
|
};
|
|
@@ -17,6 +17,12 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
|
|
|
17
17
|
"intercom",
|
|
18
18
|
"contact_supervisor",
|
|
19
19
|
"structured_output",
|
|
20
|
+
// Gateway controls only search, activate, or load capabilities; activation stays
|
|
21
|
+
// filtered by the child's own tool policy, so they are not mutation-capable.
|
|
22
|
+
// Kept literal: child-tool-plan transitively imports this module (via single-output).
|
|
23
|
+
"capability_catalog",
|
|
24
|
+
"capability_discover",
|
|
25
|
+
"capability_skill_show",
|
|
20
26
|
]);
|
|
21
27
|
|
|
22
28
|
// Cursor native edit/write often land as thinking traces (inactive_trace /
|
|
@@ -6,6 +6,7 @@ import { finished } from "node:stream/promises";
|
|
|
6
6
|
import type { ExternalProcessStatus } from "../../shared/types.ts";
|
|
7
7
|
import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "../background/owned-process-tree.ts";
|
|
8
8
|
import { omitExtensionBindingsEnv } from "./extension-bindings.ts";
|
|
9
|
+
import { omitGitRoutingEnv } from "./git-environment.ts";
|
|
9
10
|
import {
|
|
10
11
|
invalidateExternalCliPreflight,
|
|
11
12
|
preflightExternalCli,
|
|
@@ -86,7 +87,7 @@ function narrowLimit(value: number | undefined, ceiling: number, label: string):
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
function externalEnvironment(allowlist: readonly string[] | undefined, values: Readonly<Record<string, string>> | undefined): NodeJS.ProcessEnv {
|
|
89
|
-
if (!allowlist) return omitExtensionBindingsEnv(process.env);
|
|
90
|
+
if (!allowlist) return omitGitRoutingEnv(omitExtensionBindingsEnv(process.env));
|
|
90
91
|
const allowed = new Set(allowlist);
|
|
91
92
|
const env: NodeJS.ProcessEnv = {};
|
|
92
93
|
for (const key of allowed) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// ponytail: this static list can lag Git additions; refresh from `git rev-parse --local-env-vars` and indexed config keys when Git changes.
|
|
2
|
+
const GIT_ROUTING_VARIABLES = new Set([
|
|
3
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
4
|
+
"GIT_COMMON_DIR",
|
|
5
|
+
"GIT_CONFIG",
|
|
6
|
+
"GIT_CONFIG_COUNT",
|
|
7
|
+
"GIT_CONFIG_PARAMETERS",
|
|
8
|
+
"GIT_DIR",
|
|
9
|
+
"GIT_GRAFT_FILE",
|
|
10
|
+
"GIT_IMPLICIT_WORK_TREE",
|
|
11
|
+
"GIT_INDEX_FILE",
|
|
12
|
+
"GIT_NAMESPACE",
|
|
13
|
+
"GIT_NO_REPLACE_OBJECTS",
|
|
14
|
+
"GIT_OBJECT_DIRECTORY",
|
|
15
|
+
"GIT_PREFIX",
|
|
16
|
+
"GIT_REPLACE_REF_BASE",
|
|
17
|
+
"GIT_SHALLOW_FILE",
|
|
18
|
+
"GIT_WORK_TREE",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function isGitRoutingVariable(name: string): boolean {
|
|
22
|
+
const upper = name.toUpperCase();
|
|
23
|
+
return GIT_ROUTING_VARIABLES.has(upper) || /^GIT_CONFIG_(?:KEY|VALUE)_\d+$/.test(upper);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Remove inherited values that can route Git commands away from the child cwd. */
|
|
27
|
+
export function omitGitRoutingEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
28
|
+
return Object.fromEntries(Object.entries(env).filter(([name]) => !isGitRoutingVariable(name)));
|
|
29
|
+
}
|
|
@@ -3,12 +3,81 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
6
7
|
import { SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
|
|
7
8
|
import type { JsonSchemaObject } from "../../shared/types.ts";
|
|
8
9
|
import type { ResolvedAcceptanceReportMode } from "./acceptance.ts";
|
|
9
10
|
|
|
10
11
|
export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
|
|
11
12
|
export const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
|
|
13
|
+
export const STRUCTURED_OUTPUT_REJECTION_ERROR = "structured_output was invoked but no valid output was captured.";
|
|
14
|
+
export const INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR = "Structured output invocation was rejected: invalid outputSchema.";
|
|
15
|
+
export const STRUCTURED_OUTPUT_VALIDATOR_UNAVAILABLE_ERROR = "Structured output invocation was rejected: validator unavailable.";
|
|
16
|
+
export const MAX_STRUCTURED_OUTPUT_REJECTION_ERROR_BYTES = 4096;
|
|
17
|
+
|
|
18
|
+
function utf8Prefix(value: string, maxBytes: number): string {
|
|
19
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
|
|
20
|
+
const suffix = "...";
|
|
21
|
+
const contentLimit = maxBytes - Buffer.byteLength(suffix, "utf8");
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
let result = "";
|
|
24
|
+
for (const character of value) {
|
|
25
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
26
|
+
if (bytes + characterBytes > contentLimit) break;
|
|
27
|
+
result += character;
|
|
28
|
+
bytes += characterBytes;
|
|
29
|
+
}
|
|
30
|
+
return result + suffix;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function messageText(content: unknown): string {
|
|
34
|
+
if (typeof content === "string") return content;
|
|
35
|
+
if (!Array.isArray(content)) return "";
|
|
36
|
+
return content
|
|
37
|
+
.filter((part): part is { type: "text"; text: string } => Boolean(part) && typeof part === "object" && (part as { type?: unknown }).type === "text" && typeof (part as { text?: unknown }).text === "string")
|
|
38
|
+
.map((part) => part.text)
|
|
39
|
+
.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sanitizeStructuredOutputRejection(text: string): string {
|
|
43
|
+
const withoutControls = text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
|
|
44
|
+
if (/invalid outputSchema(?:\s*:|$)/i.test(withoutControls)) return INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR;
|
|
45
|
+
if (/failed to validate structured output:|cannot load typebox\/compile/i.test(withoutControls)) return STRUCTURED_OUTPUT_VALIDATOR_UNAVAILABLE_ERROR;
|
|
46
|
+
const validationMarker = "Structured output validation failed:";
|
|
47
|
+
const markerIndex = withoutControls.indexOf(validationMarker);
|
|
48
|
+
const diagnostic = markerIndex >= 0 ? withoutControls.slice(markerIndex) : withoutControls;
|
|
49
|
+
const lines = diagnostic.split(/\r?\n/).filter((line) => {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
return trimmed.length > 0
|
|
52
|
+
&& !/^at\s/.test(trimmed)
|
|
53
|
+
&& !/^(?:arguments?|parameters?|submitted value|input|outputSchema|schema|stack)\s*:/i.test(trimmed);
|
|
54
|
+
});
|
|
55
|
+
const sanitized = lines.join("\n")
|
|
56
|
+
.replace(/\b(received|actual|got)(?:\s+value)?\s*[:=]\s*(?:\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|\{[^\n]*\}|\[[^\n]*\]|\S+)/gi, "$1: [redacted]")
|
|
57
|
+
.trim();
|
|
58
|
+
return sanitized || STRUCTURED_OUTPUT_REJECTION_ERROR;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Returns bounded evidence from the latest failed structured_output result in a terminal transcript. */
|
|
62
|
+
export function formatStructuredOutputRejectionError(messages: readonly Message[]): string {
|
|
63
|
+
const toolCallIds = new Set<string>();
|
|
64
|
+
for (const message of messages) {
|
|
65
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
66
|
+
for (const part of message.content) {
|
|
67
|
+
const call = part as { type?: unknown; id?: unknown; name?: unknown };
|
|
68
|
+
if (call.type === "toolCall" && call.name === "structured_output" && typeof call.id === "string") toolCallIds.add(call.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
72
|
+
const message = messages[index] as Message & { toolCallId?: unknown; toolName?: unknown; isError?: unknown };
|
|
73
|
+
if (message.role !== "toolResult" || message.isError !== true) continue;
|
|
74
|
+
const matchingName = message.toolName === "structured_output";
|
|
75
|
+
const matchingId = typeof message.toolCallId === "string" && toolCallIds.has(message.toolCallId);
|
|
76
|
+
if (!matchingName && !matchingId) continue;
|
|
77
|
+
return utf8Prefix(sanitizeStructuredOutputRejection(messageText(message.content)), MAX_STRUCTURED_OUTPUT_REJECTION_ERROR_BYTES);
|
|
78
|
+
}
|
|
79
|
+
return STRUCTURED_OUTPUT_REJECTION_ERROR;
|
|
80
|
+
}
|
|
12
81
|
|
|
13
82
|
export interface StructuredOutputRuntime {
|
|
14
83
|
schema: JsonSchemaObject;
|
|
@@ -704,6 +704,25 @@ export type ProcessTerminal =
|
|
|
704
704
|
diagnostic?: string;
|
|
705
705
|
});
|
|
706
706
|
|
|
707
|
+
export type WorkflowTerminalProof =
|
|
708
|
+
| {
|
|
709
|
+
version: 1;
|
|
710
|
+
kind: "workflow";
|
|
711
|
+
runId: string;
|
|
712
|
+
state: "observed";
|
|
713
|
+
dispatchClosed: true;
|
|
714
|
+
observedAt: number;
|
|
715
|
+
children: ProcessTerminal[];
|
|
716
|
+
}
|
|
717
|
+
| {
|
|
718
|
+
version: 1;
|
|
719
|
+
kind: "workflow";
|
|
720
|
+
runId: string;
|
|
721
|
+
state: "pending" | "unknown";
|
|
722
|
+
dispatchClosed: boolean;
|
|
723
|
+
reason: string;
|
|
724
|
+
};
|
|
725
|
+
|
|
707
726
|
/** Identifies the durable schedule that launched a run, so its completion is attributable. */
|
|
708
727
|
export interface ScheduleOrigin {
|
|
709
728
|
id: string;
|
|
@@ -1398,6 +1417,7 @@ export interface Details {
|
|
|
1398
1417
|
context?: "fresh" | "fork" | "mixed";
|
|
1399
1418
|
results: SingleResult[];
|
|
1400
1419
|
workflowChildren?: WorkflowChildSummary;
|
|
1420
|
+
workflowTerminalProof?: WorkflowTerminalProof;
|
|
1401
1421
|
/**
|
|
1402
1422
|
* Terminal completion payloads for runs this bg_wait call observed
|
|
1403
1423
|
* finishing. Async completions travel as result files that are consumed and
|
|
@@ -17,17 +17,17 @@ import {
|
|
|
17
17
|
} from "../profiles/profiles.ts";
|
|
18
18
|
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
19
19
|
import { findModelInfo, toModelInfo } from "../shared/model-info.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { shortenPath } from "../shared/formatters.ts";
|
|
21
21
|
import { listAsyncRuns, formatAsyncRunProgressLabel, type AsyncRunSummary } from "../runs/background/async-status.ts";
|
|
22
22
|
import { encodeInspectReply, handleInspectRpcArgs, INSPECT_WIDGET_KEY } from "../runs/background/inspect-rpc.ts";
|
|
23
23
|
import { listScheduledRunSummaries } from "../runs/background/scheduled-runs.ts";
|
|
24
24
|
import { resolveAsyncStatusChild } from "../runs/shared/child-identity.ts";
|
|
25
25
|
import { readStatus } from "../shared/utils.ts";
|
|
26
|
-
|
|
27
|
-
import { readWorkflowReceipt } from "../workflows/workflow-receipt.ts";
|
|
26
|
+
|
|
28
27
|
import { FLEET_OPEN_SHORTCUT } from "../shared/shortcuts.ts";
|
|
29
28
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
30
29
|
import { registerPromptWorkflowCommands } from "./prompt-workflows.ts";
|
|
30
|
+
import { collectSubagentCost, formatSubagentCostReport } from "./subagent-cost.ts";
|
|
31
31
|
import { openSubagentsAdmin } from "./subagents-admin.ts";
|
|
32
32
|
import { SUBAGENT_GUIDE_TOPICS } from "../extension/subagent-guide.ts";
|
|
33
33
|
import { openSubagentFleet } from "../tui/fleet.ts";
|
|
@@ -36,7 +36,6 @@ import {
|
|
|
36
36
|
buildSlashInitialResult,
|
|
37
37
|
failSlashResult,
|
|
38
38
|
finalizeSlashResult,
|
|
39
|
-
resolveSlashMessageDetails,
|
|
40
39
|
} from "./slash-live-state.ts";
|
|
41
40
|
import {
|
|
42
41
|
SLASH_RESULT_TYPE,
|
|
@@ -47,11 +46,9 @@ import {
|
|
|
47
46
|
SLASH_SUBAGENT_STARTED_EVENT,
|
|
48
47
|
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
49
48
|
DIRS,
|
|
50
|
-
type Details,
|
|
51
49
|
type FleetKeybindingsConfig,
|
|
52
50
|
type SingleResult,
|
|
53
51
|
type SubagentState,
|
|
54
|
-
type Usage,
|
|
55
52
|
} from "../shared/types.ts";
|
|
56
53
|
|
|
57
54
|
interface InlineConfig {
|
|
@@ -328,237 +325,6 @@ class SubagentsStopSelector implements Component {
|
|
|
328
325
|
}
|
|
329
326
|
}
|
|
330
327
|
|
|
331
|
-
function emptyUsage(): Usage {
|
|
332
|
-
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function addUsage(target: Usage, source: Usage): void {
|
|
336
|
-
target.input += source.input;
|
|
337
|
-
target.output += source.output;
|
|
338
|
-
target.cacheRead += source.cacheRead;
|
|
339
|
-
target.cacheWrite += source.cacheWrite;
|
|
340
|
-
target.cost += source.cost;
|
|
341
|
-
target.turns += source.turns;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function usageHasValue(usage: Usage): boolean {
|
|
345
|
-
return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function nonNegativeNumber(value: unknown): number | undefined {
|
|
349
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function usageFromValue(value: unknown, turnsOverride?: number): Usage | undefined {
|
|
353
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
354
|
-
const record = value as Record<string, unknown>;
|
|
355
|
-
const costValue = typeof record.cost === "object" && record.cost !== null && !Array.isArray(record.cost)
|
|
356
|
-
? (record.cost as Record<string, unknown>).total
|
|
357
|
-
: record.cost;
|
|
358
|
-
const turnsValue = turnsOverride ?? record.turns;
|
|
359
|
-
const usage = {
|
|
360
|
-
input: nonNegativeNumber(record.input) ?? 0,
|
|
361
|
-
output: nonNegativeNumber(record.output) ?? 0,
|
|
362
|
-
cacheRead: nonNegativeNumber(record.cacheRead) ?? 0,
|
|
363
|
-
cacheWrite: nonNegativeNumber(record.cacheWrite) ?? 0,
|
|
364
|
-
cost: nonNegativeNumber(costValue) ?? 0,
|
|
365
|
-
turns: nonNegativeNumber(turnsValue) ?? 0,
|
|
366
|
-
};
|
|
367
|
-
return Number.isSafeInteger(usage.turns) ? usage : undefined;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function assistantUsageFromMessage(message: unknown): Usage | undefined {
|
|
371
|
-
if (!message || typeof message !== "object") return undefined;
|
|
372
|
-
const msg = message as { role?: unknown; usage?: unknown };
|
|
373
|
-
return msg.role === "assistant" ? usageFromValue(msg.usage, 1) : undefined;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function compactionUsageFromEntry(entry: unknown): Usage | undefined {
|
|
377
|
-
if (!entry || typeof entry !== "object") return undefined;
|
|
378
|
-
const record = entry as { type?: unknown; usage?: unknown };
|
|
379
|
-
return record.type === "compaction" ? usageFromValue(record.usage, 0) : undefined;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const MAX_USAGE_METADATA_BYTES = 2 * 1024 * 1024;
|
|
383
|
-
|
|
384
|
-
function readUsageMetadata(metadataPath: string): Record<string, unknown> | undefined {
|
|
385
|
-
let fd: number | undefined;
|
|
386
|
-
try {
|
|
387
|
-
fd = fs.openSync(metadataPath, "r");
|
|
388
|
-
const stat = fs.fstatSync(fd);
|
|
389
|
-
if (!stat.isFile() || stat.size > MAX_USAGE_METADATA_BYTES) return undefined;
|
|
390
|
-
const buffer = Buffer.allocUnsafe(stat.size);
|
|
391
|
-
let offset = 0;
|
|
392
|
-
while (offset < buffer.length) {
|
|
393
|
-
const bytesRead = fs.readSync(fd, buffer, offset, buffer.length - offset, null);
|
|
394
|
-
if (bytesRead <= 0) return undefined;
|
|
395
|
-
offset += bytesRead;
|
|
396
|
-
}
|
|
397
|
-
const value: unknown = JSON.parse(buffer.toString("utf-8"));
|
|
398
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
399
|
-
} finally {
|
|
400
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function isSubagentDetails(value: unknown): value is Details {
|
|
405
|
-
if (!value || typeof value !== "object") return false;
|
|
406
|
-
const details = value as { mode?: unknown; results?: unknown };
|
|
407
|
-
return typeof details.mode === "string" && Array.isArray(details.results);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function detailsFromSessionEntry(entry: unknown): Details | undefined {
|
|
411
|
-
if (!entry || typeof entry !== "object") return undefined;
|
|
412
|
-
const record = entry as { type?: unknown; customType?: unknown; details?: unknown; message?: unknown };
|
|
413
|
-
if (record.type === "custom_message" && record.customType === SLASH_RESULT_TYPE) {
|
|
414
|
-
const details = resolveSlashMessageDetails(record.details)?.result.details;
|
|
415
|
-
return isSubagentDetails(details) ? details : undefined;
|
|
416
|
-
}
|
|
417
|
-
if (record.type !== "message" || !record.message || typeof record.message !== "object") return undefined;
|
|
418
|
-
const message = record.message as { role?: unknown; toolName?: unknown; details?: unknown };
|
|
419
|
-
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait")) return undefined;
|
|
420
|
-
return isSubagentDetails(message.details) ? message.details : undefined;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function metadataUsage(
|
|
424
|
-
artifactsDirs: string[],
|
|
425
|
-
input: { runId: string; agent: string },
|
|
426
|
-
): Usage | undefined {
|
|
427
|
-
if (!/^[A-Za-z0-9._-]+$/.test(input.runId)) return undefined;
|
|
428
|
-
for (const artifactsDir of artifactsDirs) {
|
|
429
|
-
for (const index of [0, undefined] as const) {
|
|
430
|
-
const metadataPath = getArtifactPaths(artifactsDir, input.runId, input.agent, index).metadataPath;
|
|
431
|
-
try {
|
|
432
|
-
const metadata = readUsageMetadata(metadataPath);
|
|
433
|
-
if (!metadata) continue;
|
|
434
|
-
if (metadata.runId !== input.runId || metadata.agent !== input.agent) continue;
|
|
435
|
-
const usage = usageFromValue(metadata.usage);
|
|
436
|
-
if (usage && usageHasValue(usage)) return usage;
|
|
437
|
-
} catch (error) {
|
|
438
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to read subagent usage metadata '${metadataPath}':`, error);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function formatCostUsage(label: string, usage: Usage): string {
|
|
446
|
-
const extras = [
|
|
447
|
-
usage.cacheRead ? `cache read ${formatTokens(usage.cacheRead)}` : "",
|
|
448
|
-
usage.cacheWrite ? `cache write ${formatTokens(usage.cacheWrite)}` : "",
|
|
449
|
-
usage.turns ? `${usage.turns} turn${usage.turns === 1 ? "" : "s"}` : "",
|
|
450
|
-
].filter(Boolean);
|
|
451
|
-
return `${label}: ↑${formatTokens(usage.input)} ↓${formatTokens(usage.output)} $${usage.cost.toFixed(4)}${extras.length ? ` (${extras.join(", ")})` : ""}`;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function buildSubagentCostReport(ctx: ExtensionContext, state: SubagentState): string {
|
|
455
|
-
const parent = emptyUsage();
|
|
456
|
-
const childTotal = emptyUsage();
|
|
457
|
-
const total = emptyUsage();
|
|
458
|
-
const children: Array<{ label: string; usage: Usage; sessionFile?: string }> = [];
|
|
459
|
-
const seenChildren = new Set<string>();
|
|
460
|
-
const workflowRunIds = new Set<string>();
|
|
461
|
-
let unresolvedAsyncChildren = 0;
|
|
462
|
-
|
|
463
|
-
const addChild = (input: { agent?: string; runId?: string; usage?: Usage; sessionFile?: string }): boolean => {
|
|
464
|
-
if (!input.usage || !usageHasValue(input.usage)) return false;
|
|
465
|
-
const identity = input.runId ? `run:${input.runId}` : input.sessionFile ? `session:${input.sessionFile}` : undefined;
|
|
466
|
-
if (identity && seenChildren.has(identity)) return true;
|
|
467
|
-
if (identity) seenChildren.add(identity);
|
|
468
|
-
const usage = { ...input.usage };
|
|
469
|
-
children.push({
|
|
470
|
-
label: `Child ${children.length + 1} (${input.agent ?? "unknown"})`,
|
|
471
|
-
usage,
|
|
472
|
-
...(input.sessionFile ? { sessionFile: input.sessionFile } : {}),
|
|
473
|
-
});
|
|
474
|
-
addUsage(childTotal, usage);
|
|
475
|
-
return true;
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
479
|
-
const message = entry.type === "message" ? (entry as { message?: unknown }).message : undefined;
|
|
480
|
-
const parentUsage = assistantUsageFromMessage(message) ?? compactionUsageFromEntry(entry);
|
|
481
|
-
if (parentUsage) addUsage(parent, parentUsage);
|
|
482
|
-
const details = detailsFromSessionEntry(entry);
|
|
483
|
-
if (!details) continue;
|
|
484
|
-
if (details.mode === "workflow" && details.runId) workflowRunIds.add(details.runId);
|
|
485
|
-
for (const result of details.results) {
|
|
486
|
-
const resultRunId = (result as SingleResult & { runId?: unknown }).runId;
|
|
487
|
-
addChild({ agent: result.agent, runId: typeof resultRunId === "string" ? resultRunId : undefined, usage: usageFromValue(result.usage), sessionFile: result.sessionFile });
|
|
488
|
-
}
|
|
489
|
-
for (const completion of details.completions ?? []) {
|
|
490
|
-
if (completion.mode === "workflow") workflowRunIds.add(completion.runId);
|
|
491
|
-
for (const result of completion.results ?? []) {
|
|
492
|
-
addChild({ agent: result.agent, runId: result.runId, usage: usageFromValue(result.usage), sessionFile: result.sessionFile });
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
let sessionFile: string | null = null;
|
|
498
|
-
try {
|
|
499
|
-
sessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
500
|
-
} catch { /* an unpersisted session can still report direct child usage */ }
|
|
501
|
-
const artifactsDirs = new Set<string>();
|
|
502
|
-
const addArtifactsDir = (cwd: string | undefined): void => {
|
|
503
|
-
try {
|
|
504
|
-
artifactsDirs.add(getArtifactsDir(sessionFile, cwd, state.artifactDirPreference));
|
|
505
|
-
} catch { /* invalid or unavailable project roots are ignored */ }
|
|
506
|
-
};
|
|
507
|
-
addArtifactsDir(ctx.cwd);
|
|
508
|
-
addArtifactsDir(state.baseCwd);
|
|
509
|
-
|
|
510
|
-
for (const workflowRunId of workflowRunIds) {
|
|
511
|
-
try {
|
|
512
|
-
const status = readStatus(path.join(DIRS.async, workflowRunId));
|
|
513
|
-
addArtifactsDir(status?.cwd);
|
|
514
|
-
const receipt = readWorkflowReceipt(DIRS.async, workflowRunId);
|
|
515
|
-
const summaryChildren = new Map((receipt.workflowChildren?.children ?? []).map((child) => [child.childId, child]));
|
|
516
|
-
const refsByRunId = new Map<string, { runId: string; agent: string }>();
|
|
517
|
-
const addRef = (runId: string | undefined, agent: string | undefined): void => {
|
|
518
|
-
if (!runId || !agent || refsByRunId.has(runId)) return;
|
|
519
|
-
refsByRunId.set(runId, { runId, agent });
|
|
520
|
-
};
|
|
521
|
-
for (const [key, entry] of Object.entries(receipt.entries)) {
|
|
522
|
-
const summaryChild = summaryChildren.get(key);
|
|
523
|
-
const runIds = entry.continuation?.runIds?.length
|
|
524
|
-
? entry.continuation.runIds
|
|
525
|
-
: entry.latestRunId ? [entry.latestRunId] : summaryChild?.runId ? [summaryChild.runId] : [];
|
|
526
|
-
for (const runId of runIds) addRef(runId, entry.agent ?? summaryChild?.agent);
|
|
527
|
-
}
|
|
528
|
-
for (const child of receipt.workflowChildren?.children ?? []) {
|
|
529
|
-
if (!Object.hasOwn(receipt.entries, child.childId)) addRef(child.runId, child.agent);
|
|
530
|
-
}
|
|
531
|
-
const refs = [...refsByRunId.values()];
|
|
532
|
-
for (const ref of refs) {
|
|
533
|
-
if (seenChildren.has(`run:${ref.runId}`)) continue;
|
|
534
|
-
const usage = metadataUsage([...artifactsDirs], ref);
|
|
535
|
-
if (!usage || !addChild({ ...ref, usage })) unresolvedAsyncChildren += 1;
|
|
536
|
-
}
|
|
537
|
-
} catch (error) {
|
|
538
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to resolve async subagent usage for '${workflowRunId}':`, error);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
addUsage(total, parent);
|
|
543
|
-
addUsage(total, childTotal);
|
|
544
|
-
const lines = [
|
|
545
|
-
"Subagent cost",
|
|
546
|
-
"",
|
|
547
|
-
formatCostUsage("Parent", parent),
|
|
548
|
-
];
|
|
549
|
-
if (children.length === 0) {
|
|
550
|
-
lines.push("No subagent child usage found in this session.");
|
|
551
|
-
} else {
|
|
552
|
-
for (const child of children) {
|
|
553
|
-
lines.push(formatCostUsage(child.label, child.usage));
|
|
554
|
-
if (child.sessionFile) lines.push(` Session: ${child.sessionFile}`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (unresolvedAsyncChildren > 0) lines.push(`Async child usage unavailable: ${unresolvedAsyncChildren}.`);
|
|
558
|
-
lines.push("────────────────────────────", formatCostUsage("Children", childTotal), formatCostUsage("Total", total));
|
|
559
|
-
return lines.join("\n");
|
|
560
|
-
}
|
|
561
|
-
|
|
562
328
|
function parseSingleRequiredArg(args: string, usage: string): { ok: true; value: string } | { ok: false; message: string } {
|
|
563
329
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
564
330
|
if (parts.length !== 1) return { ok: false, message: usage };
|
|
@@ -923,7 +689,7 @@ export function registerSlashCommands(
|
|
|
923
689
|
pi.registerCommand("subagent-cost", {
|
|
924
690
|
description: "Show parent and subagent child usage cost for this session",
|
|
925
691
|
handler: async (_args, ctx) => {
|
|
926
|
-
sendSlashText(pi,
|
|
692
|
+
sendSlashText(pi, formatSubagentCostReport(collectSubagentCost(ctx, state)));
|
|
927
693
|
},
|
|
928
694
|
});
|
|
929
695
|
|