@oh-my-pi/pi-coding-agent 15.0.1 → 15.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -1
- package/examples/custom-tools/README.md +11 -7
- package/examples/custom-tools/hello/index.ts +2 -2
- package/examples/extensions/README.md +19 -8
- package/examples/extensions/api-demo.ts +15 -19
- package/examples/extensions/hello.ts +5 -6
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/extensions/reload-runtime.ts +4 -3
- package/examples/extensions/with-deps/index.ts +4 -3
- package/examples/sdk/06-extensions.ts +4 -2
- package/package.json +8 -18
- package/src/autoresearch/tools/init-experiment.ts +38 -41
- package/src/autoresearch/tools/log-experiment.ts +32 -41
- package/src/autoresearch/tools/run-experiment.ts +3 -3
- package/src/autoresearch/tools/update-notes.ts +11 -11
- package/src/commands/commit.ts +10 -0
- package/src/commit/agentic/tools/analyze-file.ts +4 -4
- package/src/commit/agentic/tools/git-file-diff.ts +4 -4
- package/src/commit/agentic/tools/git-hunk.ts +5 -5
- package/src/commit/agentic/tools/git-overview.ts +4 -4
- package/src/commit/agentic/tools/propose-changelog.ts +13 -13
- package/src/commit/agentic/tools/propose-commit.ts +6 -6
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/schemas.ts +28 -28
- package/src/commit/agentic/tools/split-commit.ts +22 -21
- package/src/commit/analysis/summary.ts +4 -4
- package/src/commit/changelog/generate.ts +7 -11
- package/src/commit/shared-llm.ts +22 -34
- package/src/config/config-file.ts +35 -13
- package/src/config/model-registry.ts +40 -191
- package/src/config/models-config-schema.ts +166 -0
- package/src/config/settings-schema.ts +29 -0
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/edit/index.ts +2 -2
- package/src/edit/modes/apply-patch.ts +7 -6
- package/src/edit/modes/patch.ts +18 -25
- package/src/edit/modes/replace.ts +18 -20
- package/src/eval/js/shared/rewrite-imports.ts +131 -10
- package/src/eval/py/executor.ts +233 -623
- package/src/eval/py/kernel.ts +27 -2
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -0
- package/src/exa/factory.ts +5 -4
- package/src/exa/mcp-client.ts +1 -1
- package/src/exa/researcher.ts +9 -20
- package/src/exa/search.ts +26 -52
- package/src/exa/types.ts +1 -1
- package/src/exa/websets.ts +54 -53
- package/src/exec/bash-executor.ts +2 -1
- package/src/extensibility/custom-commands/loader.ts +5 -3
- package/src/extensibility/custom-commands/types.ts +4 -2
- package/src/extensibility/custom-tools/loader.ts +5 -3
- package/src/extensibility/custom-tools/types.ts +7 -6
- package/src/extensibility/custom-tools/wrapper.ts +1 -1
- package/src/extensibility/extensions/get-commands-handler.ts +77 -0
- package/src/extensibility/extensions/loader.ts +7 -3
- package/src/extensibility/extensions/types.ts +9 -5
- package/src/extensibility/extensions/wrapper.ts +1 -2
- package/src/extensibility/hooks/loader.ts +3 -1
- package/src/extensibility/hooks/tool-wrapper.ts +1 -1
- package/src/extensibility/hooks/types.ts +4 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +78 -31
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/typebox.ts +391 -0
- package/src/goals/tools/goal-tool.ts +6 -12
- package/src/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- package/src/hashline/types.ts +4 -4
- package/src/hindsight/state.ts +2 -2
- package/src/index.ts +0 -2
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/lsp/types.ts +30 -38
- package/src/mcp/manager.ts +1 -1
- package/src/mcp/tool-bridge.ts +1 -1
- package/src/modes/acp/acp-agent.ts +248 -50
- package/src/modes/components/session-observer-overlay.ts +12 -1
- package/src/modes/components/status-line/segments.ts +39 -4
- package/src/modes/controllers/command-controller.ts +27 -2
- package/src/modes/controllers/event-controller.ts +3 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/rpc/host-tools.ts +1 -1
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-mode.ts +27 -1
- package/src/modes/rpc/rpc-types.ts +58 -1
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/theme/defaults/dark-poimandres.json +1 -0
- package/src/modes/theme/defaults/light-poimandres.json +1 -0
- package/src/modes/theme/theme.ts +117 -117
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/hashline.md +22 -26
- package/src/prompts/tools/read.md +55 -37
- package/src/sdk.ts +31 -8
- package/src/session/agent-session.ts +74 -104
- package/src/session/messages.ts +16 -51
- package/src/session/session-manager.ts +22 -2
- package/src/session/streaming-output.ts +16 -6
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +210 -87
- package/src/task/index.ts +15 -11
- package/src/task/render.ts +32 -5
- package/src/task/types.ts +54 -39
- package/src/tools/ask.ts +12 -12
- package/src/tools/ast-edit.ts +11 -15
- package/src/tools/ast-grep.ts +9 -10
- package/src/tools/bash-command-fixup.ts +47 -0
- package/src/tools/bash.ts +48 -38
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser.ts +39 -53
- package/src/tools/calculator.ts +12 -11
- package/src/tools/checkpoint.ts +7 -7
- package/src/tools/debug.ts +40 -43
- package/src/tools/eval.ts +16 -10
- package/src/tools/find.ts +10 -13
- package/src/tools/gh.ts +108 -132
- package/src/tools/hindsight-recall.ts +4 -6
- package/src/tools/hindsight-reflect.ts +5 -5
- package/src/tools/hindsight-retain.ts +15 -17
- package/src/tools/image-gen.ts +31 -81
- package/src/tools/index.ts +4 -1
- package/src/tools/inspect-image.ts +8 -9
- package/src/tools/irc.ts +15 -27
- package/src/tools/job.ts +30 -28
- package/src/tools/output-meta.ts +26 -0
- package/src/tools/read.ts +39 -12
- package/src/tools/recipe/index.ts +7 -9
- package/src/tools/render-mermaid.ts +12 -12
- package/src/tools/report-tool-issue.ts +4 -4
- package/src/tools/resolve.ts +11 -11
- package/src/tools/review.ts +14 -26
- package/src/tools/search-tool-bm25.ts +7 -9
- package/src/tools/search.ts +19 -22
- package/src/tools/ssh.ts +10 -9
- package/src/tools/todo-write.ts +26 -34
- package/src/tools/vim.ts +10 -26
- package/src/tools/write.ts +25 -5
- package/src/tools/yield.ts +100 -54
- package/src/web/search/index.ts +9 -24
- package/src/web/search/providers/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +5 -0
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- package/src/prompts/compaction/branch-summary-context.md +0 -5
- package/src/prompts/compaction/branch-summary-preamble.md +0 -2
- package/src/prompts/compaction/branch-summary.md +0 -30
- package/src/prompts/compaction/compaction-short-summary.md +0 -9
- package/src/prompts/compaction/compaction-summary-context.md +0 -5
- package/src/prompts/compaction/compaction-summary.md +0 -38
- package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
- package/src/prompts/compaction/compaction-update-summary.md +0 -45
- package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
- package/src/prompts/system/file-operations.md +0 -10
- package/src/prompts/system/handoff-document.md +0 -49
- package/src/prompts/system/summarization-system.md +0 -3
- package/src/session/compaction/branch-summarization.ts +0 -324
- package/src/session/compaction/compaction.ts +0 -1420
- package/src/session/compaction/errors.ts +0 -31
- package/src/session/compaction/index.ts +0 -8
- package/src/session/compaction/pruning.ts +0 -91
- package/src/session/compaction/utils.ts +0 -184
package/src/task/executor.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
|
+
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
|
+
import { isJsonSchemaValueValid } from "@oh-my-pi/pi-ai/utils/schema";
|
|
9
11
|
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
|
-
import type { TSchema } from "@sinclair/typebox";
|
|
11
|
-
import Ajv, { type ValidateFunction } from "ajv";
|
|
12
12
|
import { ModelRegistry } from "../config/model-registry";
|
|
13
13
|
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
14
14
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
@@ -16,6 +16,7 @@ import { Settings } from "../config/settings";
|
|
|
16
16
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
17
17
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
18
18
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
19
|
+
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
19
20
|
import type { Skill } from "../extensibility/skills";
|
|
20
21
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
21
22
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
@@ -50,7 +51,6 @@ import {
|
|
|
50
51
|
} from "./types";
|
|
51
52
|
|
|
52
53
|
const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
53
|
-
const ajv = new Ajv({ allErrors: true, strict: false, logger: false });
|
|
54
54
|
|
|
55
55
|
/** Agent event types to forward for progress tracking. */
|
|
56
56
|
const agentEventTypes = new Set<AgentEvent["type"]>([
|
|
@@ -181,6 +181,15 @@ export interface ExecutorOptions {
|
|
|
181
181
|
*/
|
|
182
182
|
parentArtifactManager?: ArtifactManager;
|
|
183
183
|
parentHindsightSessionState?: HindsightSessionState;
|
|
184
|
+
/**
|
|
185
|
+
* Parent agent's OpenTelemetry configuration. When defined, the subagent's
|
|
186
|
+
* loop is started with the same tracer/hooks but its own agent identity
|
|
187
|
+
* stamped, so its `invoke_agent` / `chat` / `execute_tool` spans appear as
|
|
188
|
+
* a sub-tree under the parent's active `execute_tool task` span. A
|
|
189
|
+
* `handoff` span is emitted on dispatch to mark the parent → subagent
|
|
190
|
+
* transition explicitly.
|
|
191
|
+
*/
|
|
192
|
+
parentTelemetry?: AgentTelemetryConfig;
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -195,16 +204,12 @@ function parseStringifiedJson(value: unknown): unknown {
|
|
|
195
204
|
}
|
|
196
205
|
}
|
|
197
206
|
|
|
198
|
-
function buildOutputValidator(schema: unknown): { validate?:
|
|
207
|
+
function buildOutputValidator(schema: unknown): { validate?: (value: unknown) => boolean; error?: string } {
|
|
199
208
|
const { normalized, error } = normalizeSchema(schema);
|
|
200
209
|
if (error) return { error };
|
|
201
210
|
if (normalized === undefined) return {};
|
|
202
211
|
const jsonSchema = jtdToJsonSchema(normalized);
|
|
203
|
-
|
|
204
|
-
return { validate: ajv.compile(jsonSchema as any) };
|
|
205
|
-
} catch (err) {
|
|
206
|
-
return { error: err instanceof Error ? err.message : String(err) };
|
|
207
|
-
}
|
|
212
|
+
return { validate: value => isJsonSchemaValueValid(jsonSchema, value) };
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
function tryParseJsonOutput(text: string): unknown | undefined {
|
|
@@ -407,14 +412,14 @@ function getUsageTokens(usage: unknown): number {
|
|
|
407
412
|
/**
|
|
408
413
|
* Create proxy tools that reuse the parent's MCP connections.
|
|
409
414
|
*/
|
|
410
|
-
function createMCPProxyTools(mcpManager: MCPManager): CustomTool
|
|
415
|
+
function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
|
|
411
416
|
return mcpManager.getTools().map(tool => {
|
|
412
417
|
const mcpTool = tool as { mcpToolName?: string; mcpServerName?: string };
|
|
413
418
|
return {
|
|
414
419
|
name: tool.name,
|
|
415
420
|
label: tool.label ?? tool.name,
|
|
416
421
|
description: tool.description ?? "",
|
|
417
|
-
parameters: tool.parameters
|
|
422
|
+
parameters: tool.parameters,
|
|
418
423
|
execute: async (_toolCallId, params, _onUpdate, _ctx, signal) => {
|
|
419
424
|
if (signal?.aborted) {
|
|
420
425
|
throw new ToolAbortError();
|
|
@@ -542,6 +547,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
542
547
|
const settings = options.settings ?? Settings.isolated();
|
|
543
548
|
const subagentSettings = createSubagentSettings(settings);
|
|
544
549
|
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
550
|
+
const maxRuntimeMs = Math.max(0, Math.trunc(Number(settings.get("task.maxRuntimeMs") ?? 0) || 0));
|
|
545
551
|
const parentDepth = options.taskDepth ?? 0;
|
|
546
552
|
const childDepth = parentDepth + 1;
|
|
547
553
|
const atMaxDepth = maxRecursionDepth >= 0 && childDepth >= maxRecursionDepth;
|
|
@@ -589,9 +595,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
589
595
|
let recentOutputTail = "";
|
|
590
596
|
let stderr = "";
|
|
591
597
|
let resolved = false;
|
|
592
|
-
type AbortReason = "signal" | "terminate";
|
|
598
|
+
type AbortReason = "signal" | "terminate" | "timeout";
|
|
593
599
|
let abortSent = false;
|
|
594
600
|
let abortReason: AbortReason | undefined;
|
|
601
|
+
let runtimeLimitExceeded = false;
|
|
595
602
|
const listenerController = new AbortController();
|
|
596
603
|
const listenerSignal = listenerController.signal;
|
|
597
604
|
const abortController = new AbortController();
|
|
@@ -612,8 +619,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
612
619
|
let hasUsage = false;
|
|
613
620
|
|
|
614
621
|
const requestAbort = (reason: AbortReason) => {
|
|
622
|
+
if (reason === "timeout") {
|
|
623
|
+
runtimeLimitExceeded = true;
|
|
624
|
+
}
|
|
615
625
|
if (abortSent) {
|
|
616
|
-
if (reason === "signal" && abortReason !== "signal") {
|
|
626
|
+
if (reason === "signal" && abortReason !== "signal" && abortReason !== "timeout") {
|
|
617
627
|
abortReason = "signal";
|
|
618
628
|
}
|
|
619
629
|
return;
|
|
@@ -635,6 +645,24 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
635
645
|
signal.addEventListener("abort", onAbort, { once: true, signal: listenerSignal });
|
|
636
646
|
}
|
|
637
647
|
|
|
648
|
+
// Wall-clock hard limit. Defense-in-depth for the case where a provider stream
|
|
649
|
+
// hang escapes the inference-layer watchdog (see openai-completions
|
|
650
|
+
// `isOpenAICompletionsProgressChunk`). Disabled by default; set
|
|
651
|
+
// `task.maxRuntimeMs > 0` to cap each subagent's lifetime.
|
|
652
|
+
let runtimeTimeoutId: NodeJS.Timeout | undefined;
|
|
653
|
+
if (maxRuntimeMs > 0) {
|
|
654
|
+
runtimeTimeoutId = setTimeout(() => {
|
|
655
|
+
if (!resolved) {
|
|
656
|
+
logger.warn("Subagent runtime limit exceeded; aborting", {
|
|
657
|
+
id,
|
|
658
|
+
agent: agent.name,
|
|
659
|
+
maxRuntimeMs,
|
|
660
|
+
});
|
|
661
|
+
requestAbort("timeout");
|
|
662
|
+
}
|
|
663
|
+
}, maxRuntimeMs);
|
|
664
|
+
}
|
|
665
|
+
|
|
638
666
|
const resolveSignalAbortReason = (): string => {
|
|
639
667
|
const reason = signal?.reason;
|
|
640
668
|
if (reason instanceof Error) {
|
|
@@ -646,6 +674,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
646
674
|
}
|
|
647
675
|
return "Cancelled by caller";
|
|
648
676
|
};
|
|
677
|
+
const resolveAbortReasonText = (): string => {
|
|
678
|
+
if (runtimeLimitExceeded) {
|
|
679
|
+
return `Subagent runtime limit exceeded (task.maxRuntimeMs=${maxRuntimeMs})`;
|
|
680
|
+
}
|
|
681
|
+
return resolveSignalAbortReason();
|
|
682
|
+
};
|
|
649
683
|
const PROGRESS_COALESCE_MS = 150;
|
|
650
684
|
let lastProgressEmitMs = 0;
|
|
651
685
|
let progressTimeoutId: NodeJS.Timeout | null = null;
|
|
@@ -906,6 +940,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
906
940
|
}
|
|
907
941
|
// Accumulate tokens for progress display
|
|
908
942
|
progress.tokens += getUsageTokens(messageUsage);
|
|
943
|
+
// Track latest per-turn context size so the UI can show
|
|
944
|
+
// "current context", not just cumulative billing volume.
|
|
945
|
+
if (role === "assistant") {
|
|
946
|
+
const perTurnTotal = getNumberField(messageUsage as Record<string, unknown>, "totalTokens");
|
|
947
|
+
if (perTurnTotal !== undefined && perTurnTotal > 0) {
|
|
948
|
+
progress.contextTokens = perTurnTotal;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
909
951
|
}
|
|
910
952
|
break;
|
|
911
953
|
}
|
|
@@ -946,21 +988,39 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
946
988
|
let abortReasonText: string | undefined;
|
|
947
989
|
const checkAbort = () => {
|
|
948
990
|
if (abortSignal.aborted) {
|
|
949
|
-
aborted = abortReason === "signal" || abortReason === undefined;
|
|
991
|
+
aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
|
|
950
992
|
if (aborted) {
|
|
951
|
-
abortReasonText ??=
|
|
993
|
+
abortReasonText ??= resolveAbortReasonText();
|
|
952
994
|
}
|
|
953
995
|
exitCode = 1;
|
|
954
996
|
throw new ToolAbortError();
|
|
955
997
|
}
|
|
956
998
|
};
|
|
999
|
+
const awaitAbortable = async <T>(promise: Promise<T>): Promise<T> => {
|
|
1000
|
+
checkAbort();
|
|
1001
|
+
const { promise: abortPromise, reject } = Promise.withResolvers<never>();
|
|
1002
|
+
const onAbort = () => {
|
|
1003
|
+
try {
|
|
1004
|
+
checkAbort();
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
reject(err);
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1010
|
+
try {
|
|
1011
|
+
return await Promise.race([promise, abortPromise]);
|
|
1012
|
+
} finally {
|
|
1013
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
957
1016
|
|
|
958
1017
|
try {
|
|
959
1018
|
checkAbort();
|
|
960
1019
|
// Pin authStorage to modelRegistry.authStorage — mirrors the createAgentSession invariant.
|
|
961
1020
|
const registryFromParent = options.modelRegistry !== undefined;
|
|
962
1021
|
const modelRegistry =
|
|
963
|
-
options.modelRegistry ??
|
|
1022
|
+
options.modelRegistry ??
|
|
1023
|
+
new ModelRegistry(options.authStorage ?? (await awaitAbortable(discoverAuthStorage())));
|
|
964
1024
|
const authStorage = modelRegistry.authStorage;
|
|
965
1025
|
if (options.authStorage && options.authStorage !== authStorage) {
|
|
966
1026
|
throw new Error(
|
|
@@ -969,7 +1029,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
969
1029
|
}
|
|
970
1030
|
checkAbort();
|
|
971
1031
|
if (!registryFromParent) {
|
|
972
|
-
await modelRegistry.refresh();
|
|
1032
|
+
await awaitAbortable(modelRegistry.refresh());
|
|
973
1033
|
} else {
|
|
974
1034
|
logger.debug("runSubagent: reusing parent modelRegistry; skipping refresh");
|
|
975
1035
|
}
|
|
@@ -980,11 +1040,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
980
1040
|
thinkingLevel: resolvedThinkingLevel,
|
|
981
1041
|
explicitThinkingLevel,
|
|
982
1042
|
authFallbackUsed,
|
|
983
|
-
} = await
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1043
|
+
} = await awaitAbortable(
|
|
1044
|
+
resolveModelOverrideWithAuthFallback(
|
|
1045
|
+
modelPatterns,
|
|
1046
|
+
options.parentActiveModelPattern,
|
|
1047
|
+
modelRegistry,
|
|
1048
|
+
settings,
|
|
1049
|
+
),
|
|
988
1050
|
);
|
|
989
1051
|
if (authFallbackUsed && model) {
|
|
990
1052
|
logger.warn("Subagent model has no working credentials; falling back to parent session model", {
|
|
@@ -994,12 +1056,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
994
1056
|
resolvedModel: model.id,
|
|
995
1057
|
});
|
|
996
1058
|
}
|
|
1059
|
+
if (model?.contextWindow && model.contextWindow > 0) {
|
|
1060
|
+
progress.contextWindow = model.contextWindow;
|
|
1061
|
+
}
|
|
997
1062
|
const effectiveThinkingLevel = explicitThinkingLevel
|
|
998
1063
|
? resolvedThinkingLevel
|
|
999
1064
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
1000
1065
|
|
|
1001
1066
|
const sessionManager = sessionFile
|
|
1002
|
-
? await SessionManager.open(sessionFile)
|
|
1067
|
+
? await awaitAbortable(SessionManager.open(sessionFile))
|
|
1003
1068
|
: SessionManager.inMemory(worktree ?? cwd);
|
|
1004
1069
|
if (options.parentArtifactManager) {
|
|
1005
1070
|
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
@@ -1008,51 +1073,84 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1008
1073
|
const mcpProxyTools = options.mcpManager ? createMCPProxyTools(options.mcpManager) : [];
|
|
1009
1074
|
const enableMCP = !options.mcpManager;
|
|
1010
1075
|
|
|
1076
|
+
// Derive subagent-scoped telemetry from the parent's config so the
|
|
1077
|
+
// child loop's spans nest under the parent's active execute_tool span
|
|
1078
|
+
// (OTEL context propagation handles parent linkage automatically),
|
|
1079
|
+
// carry the subagent's own agent identity, and use the subagent's
|
|
1080
|
+
// own session id for `gen_ai.conversation.id`.
|
|
1081
|
+
const subagentAgentIdentity: AgentIdentity | undefined = options.parentTelemetry
|
|
1082
|
+
? { id, name: agent.name, description: agent.description }
|
|
1083
|
+
: undefined;
|
|
1084
|
+
const subagentTelemetry: AgentTelemetryConfig | undefined =
|
|
1085
|
+
options.parentTelemetry && subagentAgentIdentity
|
|
1086
|
+
? {
|
|
1087
|
+
...options.parentTelemetry,
|
|
1088
|
+
agent: subagentAgentIdentity,
|
|
1089
|
+
// Clear parent's conversationId; the child loop falls back to
|
|
1090
|
+
// its own AgentLoopConfig.sessionId.
|
|
1091
|
+
conversationId: undefined,
|
|
1092
|
+
}
|
|
1093
|
+
: undefined;
|
|
1094
|
+
|
|
1095
|
+
if (options.parentTelemetry && subagentAgentIdentity) {
|
|
1096
|
+
const parentTelemetryHandle = resolveTelemetry(
|
|
1097
|
+
options.parentTelemetry,
|
|
1098
|
+
options.parentTelemetry.conversationId,
|
|
1099
|
+
);
|
|
1100
|
+
recordHandoff(parentTelemetryHandle, {
|
|
1101
|
+
fromAgent: options.parentTelemetry.agent,
|
|
1102
|
+
toAgent: subagentAgentIdentity,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1011
1106
|
const { normalized: normalizedOutputSchema } = normalizeSchema(outputSchema);
|
|
1012
1107
|
|
|
1013
|
-
const { session } = await
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1108
|
+
const { session } = await awaitAbortable(
|
|
1109
|
+
createAgentSession({
|
|
1110
|
+
cwd: worktree ?? cwd,
|
|
1111
|
+
authStorage,
|
|
1112
|
+
modelRegistry,
|
|
1113
|
+
settings: subagentSettings,
|
|
1114
|
+
model,
|
|
1115
|
+
thinkingLevel: effectiveThinkingLevel,
|
|
1116
|
+
toolNames,
|
|
1117
|
+
outputSchema,
|
|
1118
|
+
requireYieldTool: true,
|
|
1119
|
+
contextFiles: options.contextFiles,
|
|
1120
|
+
skills: options.skills,
|
|
1121
|
+
promptTemplates: options.promptTemplates,
|
|
1122
|
+
workspaceTree: options.workspaceTree,
|
|
1123
|
+
systemPrompt: defaultPrompt => {
|
|
1124
|
+
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
|
|
1125
|
+
agent: agent.systemPrompt,
|
|
1126
|
+
context: options.context?.trim() ?? "",
|
|
1127
|
+
worktree: worktree ?? "",
|
|
1128
|
+
outputSchema: normalizedOutputSchema,
|
|
1129
|
+
contextFile: contextFileForPrompt,
|
|
1130
|
+
ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
|
|
1131
|
+
ircSelfId: ircEnabled ? id : "",
|
|
1132
|
+
});
|
|
1133
|
+
return defaultPrompt.length === 0
|
|
1134
|
+
? [subagentPrompt]
|
|
1135
|
+
: [...defaultPrompt.slice(0, -1), subagentPrompt, defaultPrompt[defaultPrompt.length - 1]];
|
|
1136
|
+
},
|
|
1137
|
+
sessionManager,
|
|
1138
|
+
hasUI: false,
|
|
1139
|
+
spawns: spawnsEnv,
|
|
1140
|
+
taskDepth: childDepth,
|
|
1141
|
+
parentHindsightSessionState: options.parentHindsightSessionState,
|
|
1142
|
+
parentTaskPrefix: id,
|
|
1143
|
+
agentId: id,
|
|
1144
|
+
agentDisplayName: agent.name,
|
|
1145
|
+
enableLsp: lspEnabled,
|
|
1146
|
+
skipPythonPreflight,
|
|
1147
|
+
enableMCP,
|
|
1148
|
+
mcpManager: options.mcpManager,
|
|
1149
|
+
customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
|
|
1150
|
+
localProtocolOptions: options.localProtocolOptions,
|
|
1151
|
+
telemetry: subagentTelemetry,
|
|
1152
|
+
}),
|
|
1153
|
+
);
|
|
1056
1154
|
|
|
1057
1155
|
activeSession = session;
|
|
1058
1156
|
|
|
@@ -1073,7 +1171,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1073
1171
|
const parentOwnedToolNames = new Set(["todo_write"]);
|
|
1074
1172
|
const filteredSubagentTools = subagentToolNames.filter(name => !parentOwnedToolNames.has(name));
|
|
1075
1173
|
if (filteredSubagentTools.length !== subagentToolNames.length) {
|
|
1076
|
-
await session.setActiveToolsByName(filteredSubagentTools);
|
|
1174
|
+
await awaitAbortable(session.setActiveToolsByName(filteredSubagentTools));
|
|
1077
1175
|
}
|
|
1078
1176
|
|
|
1079
1177
|
session.sessionManager.appendSessionInit({
|
|
@@ -1090,6 +1188,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1090
1188
|
},
|
|
1091
1189
|
{ once: true, signal: sessionAbortController.signal },
|
|
1092
1190
|
);
|
|
1191
|
+
// Defensive: if the wall-clock timer (or external signal) fired during
|
|
1192
|
+
// the awaited setup above, the listener registration races the dispatch
|
|
1193
|
+
// and may not observe the already-fired abort event. Mirror it manually.
|
|
1194
|
+
if (abortSignal.aborted) {
|
|
1195
|
+
void session.abort();
|
|
1196
|
+
}
|
|
1093
1197
|
|
|
1094
1198
|
const extensionRunner = session.extensionRunner;
|
|
1095
1199
|
if (extensionRunner) {
|
|
@@ -1119,7 +1223,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1119
1223
|
getAllTools: () => session.getAllToolNames(),
|
|
1120
1224
|
setActiveTools: (toolNames: string[]) =>
|
|
1121
1225
|
session.setActiveToolsByName(toolNames.filter(name => !parentOwnedToolNames.has(name))),
|
|
1122
|
-
getCommands: () =>
|
|
1226
|
+
getCommands: () => getSessionSlashCommands(session),
|
|
1123
1227
|
setModel: model => runExtensionSetModel(session, model),
|
|
1124
1228
|
getThinkingLevel: () => session.thinkingLevel,
|
|
1125
1229
|
setThinkingLevel: level => session.setThinkingLevel(level),
|
|
@@ -1142,7 +1246,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1142
1246
|
extensionRunner.onError(err => {
|
|
1143
1247
|
logger.error("Extension error", { path: err.extensionPath, error: err.error });
|
|
1144
1248
|
});
|
|
1145
|
-
await extensionRunner.emit({ type: "session_start" });
|
|
1249
|
+
await awaitAbortable(extensionRunner.emit({ type: "session_start" }));
|
|
1146
1250
|
}
|
|
1147
1251
|
|
|
1148
1252
|
const MAX_YIELD_RETRIES = 3;
|
|
@@ -1159,8 +1263,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1159
1263
|
}
|
|
1160
1264
|
});
|
|
1161
1265
|
|
|
1162
|
-
|
|
1163
|
-
await session.
|
|
1266
|
+
checkAbort();
|
|
1267
|
+
await awaitAbortable(session.prompt(task, { attribution: "agent" }));
|
|
1268
|
+
await awaitAbortable(session.waitForIdle());
|
|
1164
1269
|
|
|
1165
1270
|
const reminderToolChoice = buildNamedToolChoice("yield", session.model);
|
|
1166
1271
|
|
|
@@ -1174,11 +1279,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1174
1279
|
});
|
|
1175
1280
|
|
|
1176
1281
|
const isFinalRetry = retryCount >= MAX_YIELD_RETRIES;
|
|
1177
|
-
await
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1282
|
+
await awaitAbortable(
|
|
1283
|
+
session.prompt(reminder, {
|
|
1284
|
+
attribution: "agent",
|
|
1285
|
+
...(isFinalRetry && reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
|
|
1286
|
+
}),
|
|
1287
|
+
);
|
|
1288
|
+
await awaitAbortable(session.waitForIdle());
|
|
1182
1289
|
} catch (err) {
|
|
1183
1290
|
logger.error("Subagent prompt failed", {
|
|
1184
1291
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -1186,7 +1293,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1186
1293
|
}
|
|
1187
1294
|
}
|
|
1188
1295
|
|
|
1189
|
-
await session.waitForIdle();
|
|
1296
|
+
await awaitAbortable(session.waitForIdle());
|
|
1190
1297
|
if (!yieldCalled && !abortSignal.aborted) {
|
|
1191
1298
|
exitCode = 0;
|
|
1192
1299
|
}
|
|
@@ -1194,9 +1301,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1194
1301
|
const lastAssistant = session.getLastAssistantMessage();
|
|
1195
1302
|
if (lastAssistant) {
|
|
1196
1303
|
if (lastAssistant.stopReason === "aborted") {
|
|
1197
|
-
aborted = abortReason === "signal" || abortReason === undefined;
|
|
1304
|
+
aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
|
|
1198
1305
|
if (aborted) {
|
|
1199
|
-
abortReasonText ??=
|
|
1306
|
+
abortReasonText ??= resolveAbortReasonText();
|
|
1200
1307
|
}
|
|
1201
1308
|
exitCode = 1;
|
|
1202
1309
|
} else if (lastAssistant.stopReason === "error") {
|
|
@@ -1211,9 +1318,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1211
1318
|
}
|
|
1212
1319
|
} finally {
|
|
1213
1320
|
if (abortSignal.aborted) {
|
|
1214
|
-
aborted = abortReason === "signal" || abortReason === undefined;
|
|
1321
|
+
aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
|
|
1215
1322
|
if (aborted) {
|
|
1216
|
-
abortReasonText ??=
|
|
1323
|
+
abortReasonText ??= resolveAbortReasonText();
|
|
1217
1324
|
}
|
|
1218
1325
|
if (exitCode === 0) exitCode = 1;
|
|
1219
1326
|
}
|
|
@@ -1249,6 +1356,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1249
1356
|
const done = await runSubagent();
|
|
1250
1357
|
resolved = true;
|
|
1251
1358
|
listenerController.abort();
|
|
1359
|
+
if (runtimeTimeoutId !== undefined) {
|
|
1360
|
+
clearTimeout(runtimeTimeoutId);
|
|
1361
|
+
runtimeTimeoutId = undefined;
|
|
1362
|
+
}
|
|
1252
1363
|
|
|
1253
1364
|
if (progressTimeoutId) {
|
|
1254
1365
|
clearTimeout(progressTimeoutId);
|
|
@@ -1302,12 +1413,22 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1302
1413
|
}
|
|
1303
1414
|
}
|
|
1304
1415
|
|
|
1305
|
-
// Update final progress
|
|
1306
|
-
|
|
1416
|
+
// Update final progress. A wall-clock timeout always wins: if the runtime
|
|
1417
|
+
// limit fired we report aborted/failed regardless of whether a yield landed
|
|
1418
|
+
// while we were tearing the session down. The yield data is still surfaced
|
|
1419
|
+
// to the caller via `progress.extractedToolData`, but the exit status must
|
|
1420
|
+
// reflect the timeout so on-call doesn't mistake a stuck run for success.
|
|
1421
|
+
if (runtimeLimitExceeded && exitCode === 0) {
|
|
1422
|
+
exitCode = 1;
|
|
1423
|
+
}
|
|
1424
|
+
const wasAborted =
|
|
1425
|
+
runtimeLimitExceeded || abortedViaYield || (!hasYield && (done.aborted || signal?.aborted || false));
|
|
1307
1426
|
const finalAbortReason = wasAborted
|
|
1308
|
-
?
|
|
1309
|
-
?
|
|
1310
|
-
:
|
|
1427
|
+
? runtimeLimitExceeded
|
|
1428
|
+
? resolveAbortReasonText()
|
|
1429
|
+
: abortedViaYield
|
|
1430
|
+
? yieldAbortReason
|
|
1431
|
+
: (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : resolveAbortReasonText()))
|
|
1311
1432
|
: undefined;
|
|
1312
1433
|
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1313
1434
|
scheduleProgress(true);
|
|
@@ -1340,6 +1461,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1340
1461
|
truncated: Boolean(truncated),
|
|
1341
1462
|
durationMs: Date.now() - startTime,
|
|
1342
1463
|
tokens: progress.tokens,
|
|
1464
|
+
contextTokens: progress.contextTokens,
|
|
1465
|
+
contextWindow: progress.contextWindow,
|
|
1343
1466
|
modelOverride,
|
|
1344
1467
|
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
1345
1468
|
aborted: wasAborted,
|
package/src/task/index.ts
CHANGED
|
@@ -18,7 +18,6 @@ import path from "node:path";
|
|
|
18
18
|
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
20
20
|
import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
21
|
-
import type { TSchema } from "@sinclair/typebox";
|
|
22
21
|
import type { ToolSession } from "..";
|
|
23
22
|
import { AsyncJobManager } from "../async";
|
|
24
23
|
import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
@@ -29,6 +28,15 @@ import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.m
|
|
|
29
28
|
import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
|
|
30
29
|
import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
|
|
31
30
|
import { formatBytes, formatDuration } from "../tools/render-utils";
|
|
31
|
+
import {
|
|
32
|
+
type AgentDefinition,
|
|
33
|
+
type AgentProgress,
|
|
34
|
+
getTaskSchema,
|
|
35
|
+
type SingleResult,
|
|
36
|
+
type TaskParams,
|
|
37
|
+
type TaskToolDetails,
|
|
38
|
+
type TaskToolSchemaInstance,
|
|
39
|
+
} from "./types";
|
|
32
40
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
33
41
|
import "../tools/review";
|
|
34
42
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
@@ -40,14 +48,6 @@ import { AgentOutputManager } from "./output-manager";
|
|
|
40
48
|
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
41
49
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
42
50
|
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
43
|
-
import {
|
|
44
|
-
type AgentDefinition,
|
|
45
|
-
type AgentProgress,
|
|
46
|
-
getTaskSchema,
|
|
47
|
-
type SingleResult,
|
|
48
|
-
type TaskParams,
|
|
49
|
-
type TaskToolDetails,
|
|
50
|
-
} from "./types";
|
|
51
51
|
import {
|
|
52
52
|
applyNestedPatches,
|
|
53
53
|
captureBaseline,
|
|
@@ -198,7 +198,7 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
|
|
|
198
198
|
* Requires async initialization to discover available agents.
|
|
199
199
|
* Use `TaskTool.create(session)` to instantiate.
|
|
200
200
|
*/
|
|
201
|
-
export class TaskTool implements AgentTool<
|
|
201
|
+
export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetails, Theme> {
|
|
202
202
|
readonly name = "task";
|
|
203
203
|
readonly label = "Task";
|
|
204
204
|
readonly summary = "Spawn a subagent to complete a parallel task";
|
|
@@ -208,7 +208,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
208
208
|
readonly #discoveredAgents: AgentDefinition[];
|
|
209
209
|
readonly #blockedAgent: string | undefined;
|
|
210
210
|
|
|
211
|
-
get parameters():
|
|
211
|
+
get parameters(): TaskToolSchemaInstance {
|
|
212
212
|
const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
|
|
213
213
|
return getTaskSchema({ isolationEnabled, simpleMode: this.#getTaskSimpleMode() });
|
|
214
214
|
}
|
|
@@ -391,6 +391,8 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
391
391
|
: "failed";
|
|
392
392
|
progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
|
|
393
393
|
progress.tokens = singleResult?.tokens ?? 0;
|
|
394
|
+
progress.contextTokens = singleResult?.contextTokens;
|
|
395
|
+
progress.contextWindow = singleResult?.contextWindow;
|
|
394
396
|
progress.cost = singleResult?.usage?.cost.total ?? 0;
|
|
395
397
|
progress.extractedToolData = singleResult?.extractedToolData;
|
|
396
398
|
}
|
|
@@ -881,6 +883,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
881
883
|
localProtocolOptions,
|
|
882
884
|
parentArtifactManager,
|
|
883
885
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
886
|
+
parentTelemetry: this.session.getTelemetry?.(),
|
|
884
887
|
});
|
|
885
888
|
}
|
|
886
889
|
|
|
@@ -934,6 +937,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
934
937
|
localProtocolOptions,
|
|
935
938
|
parentArtifactManager,
|
|
936
939
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
940
|
+
parentTelemetry: this.session.getTelemetry?.(),
|
|
937
941
|
});
|
|
938
942
|
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
939
943
|
try {
|
package/src/task/render.ts
CHANGED
|
@@ -50,17 +50,35 @@ function getStatusIcon(status: AgentProgress["status"], theme: Theme, spinnerFra
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/** Append tool-count,
|
|
53
|
+
/** Append tool-count, context, cumulative-tokens, and cost stats to a status line string. */
|
|
54
54
|
function appendAgentStats(
|
|
55
55
|
line: string,
|
|
56
|
-
opts: {
|
|
56
|
+
opts: {
|
|
57
|
+
toolCount?: number;
|
|
58
|
+
tokens: number;
|
|
59
|
+
contextTokens?: number;
|
|
60
|
+
contextWindow?: number;
|
|
61
|
+
cost: number;
|
|
62
|
+
},
|
|
57
63
|
theme: Theme,
|
|
58
64
|
): string {
|
|
59
65
|
if (opts.toolCount) {
|
|
60
66
|
line += `${theme.sep.dot}${theme.fg("dim", `${opts.toolCount} tools`)}`;
|
|
61
67
|
}
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
// Current per-turn context — what the user reads as "how full is the context".
|
|
69
|
+
// Cumulative tokens (billing volume) renders separately with a Σ sigil to avoid
|
|
70
|
+
// being mistaken for current window pressure.
|
|
71
|
+
if (opts.contextTokens && opts.contextTokens > 0) {
|
|
72
|
+
const ctx =
|
|
73
|
+
opts.contextWindow && opts.contextWindow > 0
|
|
74
|
+
? `${formatNumber(opts.contextTokens)}/${formatNumber(opts.contextWindow)} ctx`
|
|
75
|
+
: `${formatNumber(opts.contextTokens)} ctx`;
|
|
76
|
+
line += `${theme.sep.dot}${theme.fg("dim", ctx)}`;
|
|
77
|
+
if (opts.tokens > 0) {
|
|
78
|
+
line += `${theme.sep.dot}${theme.fg("dim", `Σ${formatNumber(opts.tokens)}`)}`;
|
|
79
|
+
}
|
|
80
|
+
} else if (opts.tokens > 0) {
|
|
81
|
+
line += `${theme.sep.dot}${theme.fg("dim", `Σ${formatNumber(opts.tokens)}`)}`;
|
|
64
82
|
}
|
|
65
83
|
if (opts.cost > 0) {
|
|
66
84
|
line += `${theme.sep.dot}${theme.fg("statusLineCost", `$${opts.cost.toFixed(2)}`)}`;
|
|
@@ -776,7 +794,16 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
776
794
|
iconColor,
|
|
777
795
|
theme,
|
|
778
796
|
)}`;
|
|
779
|
-
statusLine = appendAgentStats(
|
|
797
|
+
statusLine = appendAgentStats(
|
|
798
|
+
statusLine,
|
|
799
|
+
{
|
|
800
|
+
tokens: result.tokens,
|
|
801
|
+
contextTokens: result.contextTokens,
|
|
802
|
+
contextWindow: result.contextWindow,
|
|
803
|
+
cost: result.usage?.cost.total ?? 0,
|
|
804
|
+
},
|
|
805
|
+
theme,
|
|
806
|
+
);
|
|
780
807
|
statusLine += `${theme.sep.dot}${theme.fg("dim", formatDuration(result.durationMs))}`;
|
|
781
808
|
|
|
782
809
|
if (result.truncated) {
|