@oh-my-pi/pi-coding-agent 15.0.2 → 15.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -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 +7 -17
- 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/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 +9 -190
- package/src/config/models-config-schema.ts +166 -0
- package/src/config/settings-schema.ts +18 -0
- 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/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/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 +30 -0
- 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/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 +7 -7
- 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/components/session-observer-overlay.ts +12 -1
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/controllers/command-controller.ts +27 -2
- package/src/modes/controllers/event-controller.ts +3 -4
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/rpc/host-tools.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/theme.ts +111 -117
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/context-usage.ts +2 -2
- 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/executor.ts +208 -86
- 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.ts +9 -23
- 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 +6 -8
- package/src/tools/find.ts +10 -13
- package/src/tools/gh.ts +71 -128
- 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 +32 -82
- 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 +14 -21
- package/src/tools/read.ts +7 -8
- 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 +7 -7
- package/src/tools/todo-write.ts +26 -34
- package/src/tools/vim.ts +10 -26
- package/src/tools/write.ts +5 -5
- package/src/tools/yield.ts +100 -54
- package/src/web/search/index.ts +9 -24
- 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";
|
|
@@ -51,7 +51,6 @@ import {
|
|
|
51
51
|
} from "./types";
|
|
52
52
|
|
|
53
53
|
const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
54
|
-
const ajv = new Ajv({ allErrors: true, strict: false, logger: false });
|
|
55
54
|
|
|
56
55
|
/** Agent event types to forward for progress tracking. */
|
|
57
56
|
const agentEventTypes = new Set<AgentEvent["type"]>([
|
|
@@ -182,6 +181,15 @@ export interface ExecutorOptions {
|
|
|
182
181
|
*/
|
|
183
182
|
parentArtifactManager?: ArtifactManager;
|
|
184
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;
|
|
185
193
|
}
|
|
186
194
|
|
|
187
195
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -196,16 +204,12 @@ function parseStringifiedJson(value: unknown): unknown {
|
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
206
|
|
|
199
|
-
function buildOutputValidator(schema: unknown): { validate?:
|
|
207
|
+
function buildOutputValidator(schema: unknown): { validate?: (value: unknown) => boolean; error?: string } {
|
|
200
208
|
const { normalized, error } = normalizeSchema(schema);
|
|
201
209
|
if (error) return { error };
|
|
202
210
|
if (normalized === undefined) return {};
|
|
203
211
|
const jsonSchema = jtdToJsonSchema(normalized);
|
|
204
|
-
|
|
205
|
-
return { validate: ajv.compile(jsonSchema as any) };
|
|
206
|
-
} catch (err) {
|
|
207
|
-
return { error: err instanceof Error ? err.message : String(err) };
|
|
208
|
-
}
|
|
212
|
+
return { validate: value => isJsonSchemaValueValid(jsonSchema, value) };
|
|
209
213
|
}
|
|
210
214
|
|
|
211
215
|
function tryParseJsonOutput(text: string): unknown | undefined {
|
|
@@ -408,14 +412,14 @@ function getUsageTokens(usage: unknown): number {
|
|
|
408
412
|
/**
|
|
409
413
|
* Create proxy tools that reuse the parent's MCP connections.
|
|
410
414
|
*/
|
|
411
|
-
function createMCPProxyTools(mcpManager: MCPManager): CustomTool
|
|
415
|
+
function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
|
|
412
416
|
return mcpManager.getTools().map(tool => {
|
|
413
417
|
const mcpTool = tool as { mcpToolName?: string; mcpServerName?: string };
|
|
414
418
|
return {
|
|
415
419
|
name: tool.name,
|
|
416
420
|
label: tool.label ?? tool.name,
|
|
417
421
|
description: tool.description ?? "",
|
|
418
|
-
parameters: tool.parameters
|
|
422
|
+
parameters: tool.parameters,
|
|
419
423
|
execute: async (_toolCallId, params, _onUpdate, _ctx, signal) => {
|
|
420
424
|
if (signal?.aborted) {
|
|
421
425
|
throw new ToolAbortError();
|
|
@@ -543,6 +547,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
543
547
|
const settings = options.settings ?? Settings.isolated();
|
|
544
548
|
const subagentSettings = createSubagentSettings(settings);
|
|
545
549
|
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
550
|
+
const maxRuntimeMs = Math.max(0, Math.trunc(Number(settings.get("task.maxRuntimeMs") ?? 0) || 0));
|
|
546
551
|
const parentDepth = options.taskDepth ?? 0;
|
|
547
552
|
const childDepth = parentDepth + 1;
|
|
548
553
|
const atMaxDepth = maxRecursionDepth >= 0 && childDepth >= maxRecursionDepth;
|
|
@@ -590,9 +595,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
590
595
|
let recentOutputTail = "";
|
|
591
596
|
let stderr = "";
|
|
592
597
|
let resolved = false;
|
|
593
|
-
type AbortReason = "signal" | "terminate";
|
|
598
|
+
type AbortReason = "signal" | "terminate" | "timeout";
|
|
594
599
|
let abortSent = false;
|
|
595
600
|
let abortReason: AbortReason | undefined;
|
|
601
|
+
let runtimeLimitExceeded = false;
|
|
596
602
|
const listenerController = new AbortController();
|
|
597
603
|
const listenerSignal = listenerController.signal;
|
|
598
604
|
const abortController = new AbortController();
|
|
@@ -613,8 +619,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
613
619
|
let hasUsage = false;
|
|
614
620
|
|
|
615
621
|
const requestAbort = (reason: AbortReason) => {
|
|
622
|
+
if (reason === "timeout") {
|
|
623
|
+
runtimeLimitExceeded = true;
|
|
624
|
+
}
|
|
616
625
|
if (abortSent) {
|
|
617
|
-
if (reason === "signal" && abortReason !== "signal") {
|
|
626
|
+
if (reason === "signal" && abortReason !== "signal" && abortReason !== "timeout") {
|
|
618
627
|
abortReason = "signal";
|
|
619
628
|
}
|
|
620
629
|
return;
|
|
@@ -636,6 +645,24 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
636
645
|
signal.addEventListener("abort", onAbort, { once: true, signal: listenerSignal });
|
|
637
646
|
}
|
|
638
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
|
+
|
|
639
666
|
const resolveSignalAbortReason = (): string => {
|
|
640
667
|
const reason = signal?.reason;
|
|
641
668
|
if (reason instanceof Error) {
|
|
@@ -647,6 +674,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
647
674
|
}
|
|
648
675
|
return "Cancelled by caller";
|
|
649
676
|
};
|
|
677
|
+
const resolveAbortReasonText = (): string => {
|
|
678
|
+
if (runtimeLimitExceeded) {
|
|
679
|
+
return `Subagent runtime limit exceeded (task.maxRuntimeMs=${maxRuntimeMs})`;
|
|
680
|
+
}
|
|
681
|
+
return resolveSignalAbortReason();
|
|
682
|
+
};
|
|
650
683
|
const PROGRESS_COALESCE_MS = 150;
|
|
651
684
|
let lastProgressEmitMs = 0;
|
|
652
685
|
let progressTimeoutId: NodeJS.Timeout | null = null;
|
|
@@ -907,6 +940,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
907
940
|
}
|
|
908
941
|
// Accumulate tokens for progress display
|
|
909
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
|
+
}
|
|
910
951
|
}
|
|
911
952
|
break;
|
|
912
953
|
}
|
|
@@ -947,21 +988,39 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
947
988
|
let abortReasonText: string | undefined;
|
|
948
989
|
const checkAbort = () => {
|
|
949
990
|
if (abortSignal.aborted) {
|
|
950
|
-
aborted = abortReason === "signal" || abortReason === undefined;
|
|
991
|
+
aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
|
|
951
992
|
if (aborted) {
|
|
952
|
-
abortReasonText ??=
|
|
993
|
+
abortReasonText ??= resolveAbortReasonText();
|
|
953
994
|
}
|
|
954
995
|
exitCode = 1;
|
|
955
996
|
throw new ToolAbortError();
|
|
956
997
|
}
|
|
957
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
|
+
};
|
|
958
1016
|
|
|
959
1017
|
try {
|
|
960
1018
|
checkAbort();
|
|
961
1019
|
// Pin authStorage to modelRegistry.authStorage — mirrors the createAgentSession invariant.
|
|
962
1020
|
const registryFromParent = options.modelRegistry !== undefined;
|
|
963
1021
|
const modelRegistry =
|
|
964
|
-
options.modelRegistry ??
|
|
1022
|
+
options.modelRegistry ??
|
|
1023
|
+
new ModelRegistry(options.authStorage ?? (await awaitAbortable(discoverAuthStorage())));
|
|
965
1024
|
const authStorage = modelRegistry.authStorage;
|
|
966
1025
|
if (options.authStorage && options.authStorage !== authStorage) {
|
|
967
1026
|
throw new Error(
|
|
@@ -970,7 +1029,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
970
1029
|
}
|
|
971
1030
|
checkAbort();
|
|
972
1031
|
if (!registryFromParent) {
|
|
973
|
-
await modelRegistry.refresh();
|
|
1032
|
+
await awaitAbortable(modelRegistry.refresh());
|
|
974
1033
|
} else {
|
|
975
1034
|
logger.debug("runSubagent: reusing parent modelRegistry; skipping refresh");
|
|
976
1035
|
}
|
|
@@ -981,11 +1040,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
981
1040
|
thinkingLevel: resolvedThinkingLevel,
|
|
982
1041
|
explicitThinkingLevel,
|
|
983
1042
|
authFallbackUsed,
|
|
984
|
-
} = await
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1043
|
+
} = await awaitAbortable(
|
|
1044
|
+
resolveModelOverrideWithAuthFallback(
|
|
1045
|
+
modelPatterns,
|
|
1046
|
+
options.parentActiveModelPattern,
|
|
1047
|
+
modelRegistry,
|
|
1048
|
+
settings,
|
|
1049
|
+
),
|
|
989
1050
|
);
|
|
990
1051
|
if (authFallbackUsed && model) {
|
|
991
1052
|
logger.warn("Subagent model has no working credentials; falling back to parent session model", {
|
|
@@ -995,12 +1056,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
995
1056
|
resolvedModel: model.id,
|
|
996
1057
|
});
|
|
997
1058
|
}
|
|
1059
|
+
if (model?.contextWindow && model.contextWindow > 0) {
|
|
1060
|
+
progress.contextWindow = model.contextWindow;
|
|
1061
|
+
}
|
|
998
1062
|
const effectiveThinkingLevel = explicitThinkingLevel
|
|
999
1063
|
? resolvedThinkingLevel
|
|
1000
1064
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
1001
1065
|
|
|
1002
1066
|
const sessionManager = sessionFile
|
|
1003
|
-
? await SessionManager.open(sessionFile)
|
|
1067
|
+
? await awaitAbortable(SessionManager.open(sessionFile))
|
|
1004
1068
|
: SessionManager.inMemory(worktree ?? cwd);
|
|
1005
1069
|
if (options.parentArtifactManager) {
|
|
1006
1070
|
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
@@ -1009,51 +1073,84 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1009
1073
|
const mcpProxyTools = options.mcpManager ? createMCPProxyTools(options.mcpManager) : [];
|
|
1010
1074
|
const enableMCP = !options.mcpManager;
|
|
1011
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
|
+
|
|
1012
1106
|
const { normalized: normalizedOutputSchema } = normalizeSchema(outputSchema);
|
|
1013
1107
|
|
|
1014
|
-
const { session } = await
|
|
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
|
-
|
|
1056
|
-
|
|
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
|
+
);
|
|
1057
1154
|
|
|
1058
1155
|
activeSession = session;
|
|
1059
1156
|
|
|
@@ -1074,7 +1171,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1074
1171
|
const parentOwnedToolNames = new Set(["todo_write"]);
|
|
1075
1172
|
const filteredSubagentTools = subagentToolNames.filter(name => !parentOwnedToolNames.has(name));
|
|
1076
1173
|
if (filteredSubagentTools.length !== subagentToolNames.length) {
|
|
1077
|
-
await session.setActiveToolsByName(filteredSubagentTools);
|
|
1174
|
+
await awaitAbortable(session.setActiveToolsByName(filteredSubagentTools));
|
|
1078
1175
|
}
|
|
1079
1176
|
|
|
1080
1177
|
session.sessionManager.appendSessionInit({
|
|
@@ -1091,6 +1188,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1091
1188
|
},
|
|
1092
1189
|
{ once: true, signal: sessionAbortController.signal },
|
|
1093
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
|
+
}
|
|
1094
1197
|
|
|
1095
1198
|
const extensionRunner = session.extensionRunner;
|
|
1096
1199
|
if (extensionRunner) {
|
|
@@ -1143,7 +1246,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1143
1246
|
extensionRunner.onError(err => {
|
|
1144
1247
|
logger.error("Extension error", { path: err.extensionPath, error: err.error });
|
|
1145
1248
|
});
|
|
1146
|
-
await extensionRunner.emit({ type: "session_start" });
|
|
1249
|
+
await awaitAbortable(extensionRunner.emit({ type: "session_start" }));
|
|
1147
1250
|
}
|
|
1148
1251
|
|
|
1149
1252
|
const MAX_YIELD_RETRIES = 3;
|
|
@@ -1160,8 +1263,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1160
1263
|
}
|
|
1161
1264
|
});
|
|
1162
1265
|
|
|
1163
|
-
|
|
1164
|
-
await session.
|
|
1266
|
+
checkAbort();
|
|
1267
|
+
await awaitAbortable(session.prompt(task, { attribution: "agent" }));
|
|
1268
|
+
await awaitAbortable(session.waitForIdle());
|
|
1165
1269
|
|
|
1166
1270
|
const reminderToolChoice = buildNamedToolChoice("yield", session.model);
|
|
1167
1271
|
|
|
@@ -1175,11 +1279,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1175
1279
|
});
|
|
1176
1280
|
|
|
1177
1281
|
const isFinalRetry = retryCount >= MAX_YIELD_RETRIES;
|
|
1178
|
-
await
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1282
|
+
await awaitAbortable(
|
|
1283
|
+
session.prompt(reminder, {
|
|
1284
|
+
attribution: "agent",
|
|
1285
|
+
...(isFinalRetry && reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
|
|
1286
|
+
}),
|
|
1287
|
+
);
|
|
1288
|
+
await awaitAbortable(session.waitForIdle());
|
|
1183
1289
|
} catch (err) {
|
|
1184
1290
|
logger.error("Subagent prompt failed", {
|
|
1185
1291
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -1187,7 +1293,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1187
1293
|
}
|
|
1188
1294
|
}
|
|
1189
1295
|
|
|
1190
|
-
await session.waitForIdle();
|
|
1296
|
+
await awaitAbortable(session.waitForIdle());
|
|
1191
1297
|
if (!yieldCalled && !abortSignal.aborted) {
|
|
1192
1298
|
exitCode = 0;
|
|
1193
1299
|
}
|
|
@@ -1195,9 +1301,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1195
1301
|
const lastAssistant = session.getLastAssistantMessage();
|
|
1196
1302
|
if (lastAssistant) {
|
|
1197
1303
|
if (lastAssistant.stopReason === "aborted") {
|
|
1198
|
-
aborted = abortReason === "signal" || abortReason === undefined;
|
|
1304
|
+
aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
|
|
1199
1305
|
if (aborted) {
|
|
1200
|
-
abortReasonText ??=
|
|
1306
|
+
abortReasonText ??= resolveAbortReasonText();
|
|
1201
1307
|
}
|
|
1202
1308
|
exitCode = 1;
|
|
1203
1309
|
} else if (lastAssistant.stopReason === "error") {
|
|
@@ -1212,9 +1318,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1212
1318
|
}
|
|
1213
1319
|
} finally {
|
|
1214
1320
|
if (abortSignal.aborted) {
|
|
1215
|
-
aborted = abortReason === "signal" || abortReason === undefined;
|
|
1321
|
+
aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
|
|
1216
1322
|
if (aborted) {
|
|
1217
|
-
abortReasonText ??=
|
|
1323
|
+
abortReasonText ??= resolveAbortReasonText();
|
|
1218
1324
|
}
|
|
1219
1325
|
if (exitCode === 0) exitCode = 1;
|
|
1220
1326
|
}
|
|
@@ -1250,6 +1356,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1250
1356
|
const done = await runSubagent();
|
|
1251
1357
|
resolved = true;
|
|
1252
1358
|
listenerController.abort();
|
|
1359
|
+
if (runtimeTimeoutId !== undefined) {
|
|
1360
|
+
clearTimeout(runtimeTimeoutId);
|
|
1361
|
+
runtimeTimeoutId = undefined;
|
|
1362
|
+
}
|
|
1253
1363
|
|
|
1254
1364
|
if (progressTimeoutId) {
|
|
1255
1365
|
clearTimeout(progressTimeoutId);
|
|
@@ -1303,12 +1413,22 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1303
1413
|
}
|
|
1304
1414
|
}
|
|
1305
1415
|
|
|
1306
|
-
// Update final progress
|
|
1307
|
-
|
|
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));
|
|
1308
1426
|
const finalAbortReason = wasAborted
|
|
1309
|
-
?
|
|
1310
|
-
?
|
|
1311
|
-
:
|
|
1427
|
+
? runtimeLimitExceeded
|
|
1428
|
+
? resolveAbortReasonText()
|
|
1429
|
+
: abortedViaYield
|
|
1430
|
+
? yieldAbortReason
|
|
1431
|
+
: (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : resolveAbortReasonText()))
|
|
1312
1432
|
: undefined;
|
|
1313
1433
|
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1314
1434
|
scheduleProgress(true);
|
|
@@ -1341,6 +1461,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1341
1461
|
truncated: Boolean(truncated),
|
|
1342
1462
|
durationMs: Date.now() - startTime,
|
|
1343
1463
|
tokens: progress.tokens,
|
|
1464
|
+
contextTokens: progress.contextTokens,
|
|
1465
|
+
contextWindow: progress.contextWindow,
|
|
1344
1466
|
modelOverride,
|
|
1345
1467
|
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
1346
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) {
|