@selesai/code 0.13.32 → 0.13.34
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 +28 -0
- package/dist/extensions/capability-gateway/catalog.ts +4 -1
- package/dist/extensions/capability-gateway/index.test.ts +93 -4
- package/dist/extensions/capability-gateway/index.ts +294 -59
- package/dist/extensions/capability-gateway/integration.test.ts +71 -0
- package/dist/extensions/capability-gateway/routing.test.ts +154 -1
- package/dist/extensions/capability-gateway/routing.ts +227 -45
- package/dist/extensions/jev/decisions.test.ts +37 -0
- package/dist/extensions/jev/decisions.ts +148 -43
- package/dist/extensions/jev-ask-tool.test.ts +436 -0
- package/dist/extensions/jev-ask-tool.ts +587 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-hermes-memory/README.md +11 -36
- package/dist/extensions/pi-hermes-memory/src/config.ts +36 -8
- package/dist/extensions/pi-hermes-memory/src/constants.ts +5 -5
- package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +60 -46
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +26 -2
- package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +7 -1
- package/dist/extensions/pi-intercom/index.ts +5 -1
- 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/rtk.test.ts +21 -13
- package/dist/extensions/tps.test.ts +32 -1
- package/dist/extensions/tps.ts +3 -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/docs/settings.md +46 -7
- package/package.json +3 -3
|
@@ -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
|
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ExtensionContext } from "@selesai/code";
|
|
4
|
+
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
5
|
+
import { formatTokens } from "../shared/formatters.ts";
|
|
6
|
+
import { DIRS, SLASH_RESULT_TYPE, type Details, type SingleResult, type SubagentState, type Usage } from "../shared/types.ts";
|
|
7
|
+
import { readStatus } from "../shared/utils.ts";
|
|
8
|
+
import { readWorkflowReceipt } from "../workflows/workflow-receipt.ts";
|
|
9
|
+
import { resolveSlashMessageDetails } from "./slash-live-state.ts";
|
|
10
|
+
|
|
11
|
+
/** Structured parent-plus-child accounting for one session; the source of `/subagent-cost` and the RPC `cost` method. */
|
|
12
|
+
export interface SubagentCostReport {
|
|
13
|
+
version: 1;
|
|
14
|
+
parent: Usage;
|
|
15
|
+
children: SubagentCostChild[];
|
|
16
|
+
childTotal: Usage;
|
|
17
|
+
total: Usage;
|
|
18
|
+
/** Async children whose usage metadata could not be resolved; the child total is a lower bound when this is non-zero. */
|
|
19
|
+
unresolvedAsyncChildren: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SubagentCostChild {
|
|
23
|
+
label: string;
|
|
24
|
+
agent?: string;
|
|
25
|
+
runId?: string;
|
|
26
|
+
usage: Usage;
|
|
27
|
+
sessionFile?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const SUBAGENT_COST_REPORT_VERSION = 1 as const;
|
|
31
|
+
|
|
32
|
+
function emptyUsage(): Usage {
|
|
33
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addUsage(target: Usage, source: Usage): void {
|
|
37
|
+
target.input += source.input;
|
|
38
|
+
target.output += source.output;
|
|
39
|
+
target.cacheRead += source.cacheRead;
|
|
40
|
+
target.cacheWrite += source.cacheWrite;
|
|
41
|
+
target.cost += source.cost;
|
|
42
|
+
target.turns += source.turns;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function usageHasValue(usage: Usage): boolean {
|
|
46
|
+
return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nonNegativeNumber(value: unknown): number | undefined {
|
|
50
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function usageFromValue(value: unknown, turnsOverride?: number): Usage | undefined {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
55
|
+
const record = value as Record<string, unknown>;
|
|
56
|
+
const costValue = typeof record.cost === "object" && record.cost !== null && !Array.isArray(record.cost)
|
|
57
|
+
? (record.cost as Record<string, unknown>).total
|
|
58
|
+
: record.cost;
|
|
59
|
+
const turnsValue = turnsOverride ?? record.turns;
|
|
60
|
+
const usage = {
|
|
61
|
+
input: nonNegativeNumber(record.input) ?? 0,
|
|
62
|
+
output: nonNegativeNumber(record.output) ?? 0,
|
|
63
|
+
cacheRead: nonNegativeNumber(record.cacheRead) ?? 0,
|
|
64
|
+
cacheWrite: nonNegativeNumber(record.cacheWrite) ?? 0,
|
|
65
|
+
cost: nonNegativeNumber(costValue) ?? 0,
|
|
66
|
+
turns: nonNegativeNumber(turnsValue) ?? 0,
|
|
67
|
+
};
|
|
68
|
+
return Number.isSafeInteger(usage.turns) ? usage : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assistantUsageFromMessage(message: unknown): Usage | undefined {
|
|
72
|
+
if (!message || typeof message !== "object") return undefined;
|
|
73
|
+
const msg = message as { role?: unknown; usage?: unknown };
|
|
74
|
+
return msg.role === "assistant" ? usageFromValue(msg.usage, 1) : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compactionUsageFromEntry(entry: unknown): Usage | undefined {
|
|
78
|
+
if (!entry || typeof entry !== "object") return undefined;
|
|
79
|
+
const record = entry as { type?: unknown; usage?: unknown };
|
|
80
|
+
return record.type === "compaction" ? usageFromValue(record.usage, 0) : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const MAX_USAGE_METADATA_BYTES = 2 * 1024 * 1024;
|
|
84
|
+
|
|
85
|
+
function readUsageMetadata(metadataPath: string): Record<string, unknown> | undefined {
|
|
86
|
+
let fd: number | undefined;
|
|
87
|
+
try {
|
|
88
|
+
fd = fs.openSync(metadataPath, "r");
|
|
89
|
+
const stat = fs.fstatSync(fd);
|
|
90
|
+
if (!stat.isFile() || stat.size > MAX_USAGE_METADATA_BYTES) return undefined;
|
|
91
|
+
const buffer = Buffer.allocUnsafe(stat.size);
|
|
92
|
+
let offset = 0;
|
|
93
|
+
while (offset < buffer.length) {
|
|
94
|
+
const bytesRead = fs.readSync(fd, buffer, offset, buffer.length - offset, null);
|
|
95
|
+
if (bytesRead <= 0) return undefined;
|
|
96
|
+
offset += bytesRead;
|
|
97
|
+
}
|
|
98
|
+
const value: unknown = JSON.parse(buffer.toString("utf-8"));
|
|
99
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
100
|
+
} finally {
|
|
101
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isSubagentDetails(value: unknown): value is Details {
|
|
106
|
+
if (!value || typeof value !== "object") return false;
|
|
107
|
+
const details = value as { mode?: unknown; results?: unknown };
|
|
108
|
+
return typeof details.mode === "string" && Array.isArray(details.results);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function detailsFromSessionEntry(entry: unknown): Details | undefined {
|
|
112
|
+
if (!entry || typeof entry !== "object") return undefined;
|
|
113
|
+
const record = entry as { type?: unknown; customType?: unknown; details?: unknown; message?: unknown };
|
|
114
|
+
if (record.type === "custom_message" && record.customType === SLASH_RESULT_TYPE) {
|
|
115
|
+
const details = resolveSlashMessageDetails(record.details)?.result.details;
|
|
116
|
+
return isSubagentDetails(details) ? details : undefined;
|
|
117
|
+
}
|
|
118
|
+
if (record.type !== "message" || !record.message || typeof record.message !== "object") return undefined;
|
|
119
|
+
const message = record.message as { role?: unknown; toolName?: unknown; details?: unknown };
|
|
120
|
+
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait")) return undefined;
|
|
121
|
+
return isSubagentDetails(message.details) ? message.details : undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function metadataUsage(
|
|
125
|
+
artifactsDirs: string[],
|
|
126
|
+
input: { runId: string; agent: string },
|
|
127
|
+
): Usage | undefined {
|
|
128
|
+
if (!/^[A-Za-z0-9._-]+$/.test(input.runId)) return undefined;
|
|
129
|
+
for (const artifactsDir of artifactsDirs) {
|
|
130
|
+
for (const index of [0, undefined] as const) {
|
|
131
|
+
const metadataPath = getArtifactPaths(artifactsDir, input.runId, input.agent, index).metadataPath;
|
|
132
|
+
try {
|
|
133
|
+
const metadata = readUsageMetadata(metadataPath);
|
|
134
|
+
if (!metadata) continue;
|
|
135
|
+
if (metadata.runId !== input.runId || metadata.agent !== input.agent) continue;
|
|
136
|
+
const usage = usageFromValue(metadata.usage);
|
|
137
|
+
if (usage && usageHasValue(usage)) return usage;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to read subagent usage metadata '${metadataPath}':`, error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Collect parent and child usage for the current session branch. Foreground
|
|
148
|
+
* children come from persisted `subagent`/`bg_wait` tool-result details; async
|
|
149
|
+
* workflow children are resolved through receipts and artifact metadata.
|
|
150
|
+
*/
|
|
151
|
+
export function collectSubagentCost(
|
|
152
|
+
ctx: ExtensionContext,
|
|
153
|
+
state: Pick<SubagentState, "baseCwd" | "artifactDirPreference">,
|
|
154
|
+
): SubagentCostReport {
|
|
155
|
+
const parent = emptyUsage();
|
|
156
|
+
const childTotal = emptyUsage();
|
|
157
|
+
const total = emptyUsage();
|
|
158
|
+
const children: SubagentCostChild[] = [];
|
|
159
|
+
const seenChildren = new Set<string>();
|
|
160
|
+
const workflowRunIds = new Set<string>();
|
|
161
|
+
let unresolvedAsyncChildren = 0;
|
|
162
|
+
|
|
163
|
+
const addChild = (input: { agent?: string; runId?: string; usage?: Usage; sessionFile?: string }): boolean => {
|
|
164
|
+
if (!input.usage || !usageHasValue(input.usage)) return false;
|
|
165
|
+
const identity = input.runId ? `run:${input.runId}` : input.sessionFile ? `session:${input.sessionFile}` : undefined;
|
|
166
|
+
if (identity && seenChildren.has(identity)) return true;
|
|
167
|
+
if (identity) seenChildren.add(identity);
|
|
168
|
+
const usage = { ...input.usage };
|
|
169
|
+
children.push({
|
|
170
|
+
label: `Child ${children.length + 1} (${input.agent ?? "unknown"})`,
|
|
171
|
+
...(input.agent ? { agent: input.agent } : {}),
|
|
172
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
173
|
+
usage,
|
|
174
|
+
...(input.sessionFile ? { sessionFile: input.sessionFile } : {}),
|
|
175
|
+
});
|
|
176
|
+
addUsage(childTotal, usage);
|
|
177
|
+
return true;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
181
|
+
const message = entry.type === "message" ? (entry as { message?: unknown }).message : undefined;
|
|
182
|
+
const parentUsage = assistantUsageFromMessage(message) ?? compactionUsageFromEntry(entry);
|
|
183
|
+
if (parentUsage) addUsage(parent, parentUsage);
|
|
184
|
+
const details = detailsFromSessionEntry(entry);
|
|
185
|
+
if (!details) continue;
|
|
186
|
+
if (details.mode === "workflow" && details.runId) workflowRunIds.add(details.runId);
|
|
187
|
+
for (const result of details.results) {
|
|
188
|
+
const resultRunId = (result as SingleResult & { runId?: unknown }).runId;
|
|
189
|
+
addChild({ agent: result.agent, runId: typeof resultRunId === "string" ? resultRunId : undefined, usage: usageFromValue(result.usage), sessionFile: result.sessionFile });
|
|
190
|
+
}
|
|
191
|
+
for (const completion of details.completions ?? []) {
|
|
192
|
+
if (completion.mode === "workflow") workflowRunIds.add(completion.runId);
|
|
193
|
+
for (const result of completion.results ?? []) {
|
|
194
|
+
addChild({ agent: result.agent, runId: result.runId, usage: usageFromValue(result.usage), sessionFile: result.sessionFile });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let sessionFile: string | null = null;
|
|
200
|
+
try {
|
|
201
|
+
sessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
202
|
+
} catch { /* an unpersisted session can still report direct child usage */ }
|
|
203
|
+
const artifactsDirs = new Set<string>();
|
|
204
|
+
const addArtifactsDir = (cwd: string | undefined): void => {
|
|
205
|
+
try {
|
|
206
|
+
artifactsDirs.add(getArtifactsDir(sessionFile, cwd, state.artifactDirPreference));
|
|
207
|
+
} catch { /* invalid or unavailable project roots are ignored */ }
|
|
208
|
+
};
|
|
209
|
+
addArtifactsDir(ctx.cwd);
|
|
210
|
+
addArtifactsDir(state.baseCwd);
|
|
211
|
+
|
|
212
|
+
for (const workflowRunId of workflowRunIds) {
|
|
213
|
+
try {
|
|
214
|
+
const status = readStatus(path.join(DIRS.async, workflowRunId));
|
|
215
|
+
addArtifactsDir(status?.cwd);
|
|
216
|
+
const receipt = readWorkflowReceipt(DIRS.async, workflowRunId);
|
|
217
|
+
const summaryChildren = new Map((receipt.workflowChildren?.children ?? []).map((child) => [child.childId, child]));
|
|
218
|
+
const refsByRunId = new Map<string, { runId: string; agent?: string }>();
|
|
219
|
+
const addRef = (runId: string | undefined, agent: string | undefined): void => {
|
|
220
|
+
if (!runId) return;
|
|
221
|
+
const existing = refsByRunId.get(runId);
|
|
222
|
+
if (!existing || (!existing.agent && agent)) refsByRunId.set(runId, { runId, ...(agent ? { agent } : {}) });
|
|
223
|
+
};
|
|
224
|
+
for (const [key, entry] of Object.entries(receipt.entries)) {
|
|
225
|
+
const summaryChild = summaryChildren.get(key);
|
|
226
|
+
const runIds = entry.continuation?.runIds?.length
|
|
227
|
+
? entry.continuation.runIds
|
|
228
|
+
: entry.latestRunId ? [entry.latestRunId] : summaryChild?.runId ? [summaryChild.runId] : [];
|
|
229
|
+
for (const runId of runIds) addRef(runId, entry.agent ?? summaryChild?.agent);
|
|
230
|
+
}
|
|
231
|
+
for (const child of receipt.workflowChildren?.children ?? []) {
|
|
232
|
+
if (!Object.hasOwn(receipt.entries, child.childId)) addRef(child.runId, child.agent);
|
|
233
|
+
}
|
|
234
|
+
for (const ref of refsByRunId.values()) {
|
|
235
|
+
if (seenChildren.has(`run:${ref.runId}`)) continue;
|
|
236
|
+
if (!ref.agent) {
|
|
237
|
+
unresolvedAsyncChildren += 1;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const usage = metadataUsage([...artifactsDirs], { runId: ref.runId, agent: ref.agent });
|
|
241
|
+
if (!usage || !addChild({ ...ref, usage })) unresolvedAsyncChildren += 1;
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") console.error(`Failed to resolve async subagent usage for '${workflowRunId}':`, error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
addUsage(total, parent);
|
|
249
|
+
addUsage(total, childTotal);
|
|
250
|
+
return { version: SUBAGENT_COST_REPORT_VERSION, parent, children, childTotal, total, unresolvedAsyncChildren };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function formatCostUsage(label: string, usage: Usage): string {
|
|
254
|
+
const extras = [
|
|
255
|
+
usage.cacheRead ? `cache read ${formatTokens(usage.cacheRead)}` : "",
|
|
256
|
+
usage.cacheWrite ? `cache write ${formatTokens(usage.cacheWrite)}` : "",
|
|
257
|
+
usage.turns ? `${usage.turns} turn${usage.turns === 1 ? "" : "s"}` : "",
|
|
258
|
+
].filter(Boolean);
|
|
259
|
+
return `${label}: ↑${formatTokens(usage.input)} ↓${formatTokens(usage.output)} $${usage.cost.toFixed(4)}${extras.length ? ` (${extras.join(", ")})` : ""}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The `/subagent-cost` text rendering of a collected report. */
|
|
263
|
+
export function formatSubagentCostReport(report: SubagentCostReport): string {
|
|
264
|
+
const lines = [
|
|
265
|
+
"Subagent cost",
|
|
266
|
+
"",
|
|
267
|
+
formatCostUsage("Parent", report.parent),
|
|
268
|
+
];
|
|
269
|
+
if (report.children.length === 0) {
|
|
270
|
+
lines.push("No subagent child usage found in this session.");
|
|
271
|
+
} else {
|
|
272
|
+
for (const child of report.children) {
|
|
273
|
+
lines.push(formatCostUsage(child.label, child.usage));
|
|
274
|
+
if (child.sessionFile) lines.push(` Session: ${child.sessionFile}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (report.unresolvedAsyncChildren > 0) lines.push(`Async child usage unavailable: ${report.unresolvedAsyncChildren}.`);
|
|
278
|
+
lines.push("────────────────────────────", formatCostUsage("Children", report.childTotal), formatCostUsage("Total", report.total));
|
|
279
|
+
return lines.join("\n");
|
|
280
|
+
}
|
|
@@ -17,6 +17,7 @@ import { createEventBus, createTempDir, events, makeAgent, makeMinimalCtx, remov
|
|
|
17
17
|
import { discoverAgents } from "../../src/agents/agents.ts";
|
|
18
18
|
import { ACTIVE_ASYNC_CAPACITY_DIR, acquireActiveAsyncCapacity, activeAsyncCapacitySessionKey } from "../../src/runs/background/active-async-capacity.ts";
|
|
19
19
|
import { deriveForkPromptCacheKey } from "../../src/runs/shared/child-tool-plan.ts";
|
|
20
|
+
import { INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR, validateStructuredOutputValue } from "../../src/runs/shared/structured-output.ts";
|
|
20
21
|
import type { AsyncExecutionResult, AsyncResultPayload, AsyncStatusPayload } from "../support/async-execution-fixture.ts";
|
|
21
22
|
import {
|
|
22
23
|
installAsyncExecutionHooks, waitForMockPiRuntime, available, isAsyncAvailable,
|
|
@@ -1102,6 +1103,72 @@ export default function() {
|
|
|
1102
1103
|
assert.deepEqual(payload.results[0]?.structuredOutput, { ok: true, note: "async" });
|
|
1103
1104
|
});
|
|
1104
1105
|
|
|
1106
|
+
it("background settlement preserves rejected structured_output evidence", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1107
|
+
const diagnostic = "Structured output validation failed: ok: is required";
|
|
1108
|
+
mockPi.onCall({
|
|
1109
|
+
jsonl: [
|
|
1110
|
+
{ type: "message_end", message: { role: "assistant", content: [{ type: "toolCall", id: "structured-rejected", name: "structured_output", arguments: { value: {} } }], model: "mock/test-model", stopReason: "toolUse", usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } } } },
|
|
1111
|
+
{ type: "tool_execution_start", toolCallId: "structured-rejected", toolName: "structured_output", args: { value: {} } },
|
|
1112
|
+
{ type: "tool_result_end", message: { role: "toolResult", toolCallId: "structured-rejected", toolName: "structured_output", isError: true, content: [{ type: "text", text: diagnostic }] } },
|
|
1113
|
+
{ type: "tool_execution_end", toolCallId: "structured-rejected", toolName: "structured_output" },
|
|
1114
|
+
],
|
|
1115
|
+
});
|
|
1116
|
+
const id = `async-single-schema-rejected-${Date.now().toString(36)}`;
|
|
1117
|
+
|
|
1118
|
+
executeAsyncSingle(id, {
|
|
1119
|
+
agent: "worker",
|
|
1120
|
+
task: "Return structured data",
|
|
1121
|
+
agentConfig: makeAgent("worker"),
|
|
1122
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
1123
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
1124
|
+
shareEnabled: false,
|
|
1125
|
+
sessionRoot: path.join(tempDir, "sessions"),
|
|
1126
|
+
maxSubagentDepth: 2,
|
|
1127
|
+
acceptance: false,
|
|
1128
|
+
structuredOutputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } },
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id, 10_000), "utf-8")) as AsyncResultPayload;
|
|
1132
|
+
const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
|
|
1133
|
+
assert.equal(payload.success, false);
|
|
1134
|
+
assert.match(payload.results[0]?.error ?? "", /Structured output validation failed: ok: is required/);
|
|
1135
|
+
assert.doesNotMatch(payload.results[0]?.error ?? "", /Missing structured_output call/);
|
|
1136
|
+
assert.equal(payload.results[0]?.structuredOutputFailed, true);
|
|
1137
|
+
assert.match(status.steps?.[0]?.error ?? "", /Structured output validation failed: ok: is required/);
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it("does not persist malformed outputSchema compiler text in background evidence", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1141
|
+
const sentinel = "BACKGROUND_PRIVATE_SCHEMA_SENTINEL";
|
|
1142
|
+
const structuredOutputSchema = { type: "string", pattern: `${sentinel}_[invalid` };
|
|
1143
|
+
const validation = await validateStructuredOutputValue(structuredOutputSchema, "value");
|
|
1144
|
+
assert.equal(validation.status, "invalid");
|
|
1145
|
+
if (validation.status !== "invalid") return;
|
|
1146
|
+
assert.match(validation.message, new RegExp(sentinel));
|
|
1147
|
+
mockPi.onCall({
|
|
1148
|
+
jsonl: [
|
|
1149
|
+
{ type: "tool_execution_start", toolCallId: "structured-malformed-schema", toolName: "structured_output", args: { value: "value" } },
|
|
1150
|
+
{ type: "tool_result_end", message: { role: "toolResult", toolCallId: "structured-malformed-schema", toolName: "structured_output", isError: true, content: [{ type: "text", text: `Structured output validation failed: ${validation.message}` }] } },
|
|
1151
|
+
{ type: "tool_execution_end", toolCallId: "structured-malformed-schema", toolName: "structured_output" },
|
|
1152
|
+
],
|
|
1153
|
+
});
|
|
1154
|
+
const id = `async-single-malformed-schema-${Date.now().toString(36)}`;
|
|
1155
|
+
executeAsyncSingle(id, {
|
|
1156
|
+
agent: "worker", task: "Return structured data", agentConfig: makeAgent("worker"),
|
|
1157
|
+
ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
|
|
1158
|
+
artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
|
|
1159
|
+
shareEnabled: false, sessionRoot: path.join(tempDir, "sessions"), maxSubagentDepth: 2,
|
|
1160
|
+
acceptance: false, structuredOutputSchema,
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
const resultPath = await waitForAsyncResultFile(id, 10_000);
|
|
1164
|
+
const payloadText = fs.readFileSync(resultPath, "utf-8");
|
|
1165
|
+
const payload = JSON.parse(payloadText) as AsyncResultPayload;
|
|
1166
|
+
const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
|
|
1167
|
+
assert.match(payload.results[0]?.error ?? "", new RegExp(`^${escapeRegExp(INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR)}`));
|
|
1168
|
+
assert.equal(payloadText.includes(sentinel), false);
|
|
1169
|
+
assert.equal((status.steps?.[0]?.error ?? "").includes(sentinel), false);
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1105
1172
|
it("background outputSchema runs fail closed when required acceptanceReport is missing", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
|
|
1106
1173
|
mockPi.onCall({ output: "structured", structuredOutput: { ok: true } });
|
|
1107
1174
|
const id = `async-schema-missing-acceptance-${Date.now().toString(36)}`;
|