@oh-my-pi/pi-coding-agent 17.0.2 → 17.0.3
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 +70 -20
- package/dist/cli.js +4087 -4108
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +29 -0
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +14 -3
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-manager.d.ts +5 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/package.json +12 -12
- package/src/cli/file-processor.ts +1 -2
- package/src/config/settings-schema.ts +4 -3
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
- package/src/lsp/client.ts +57 -0
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +152 -1
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/interactive-mode.ts +30 -8
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +129 -87
- package/src/session/agent-session.ts +75 -21
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-manager.ts +9 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +99 -21
- package/src/task/index.ts +258 -429
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +58 -0
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/fetch.ts +28 -105
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/index.ts +47 -14
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +14 -26
- package/src/tools/todo.ts +126 -13
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/zip.ts +16 -1
|
@@ -2095,6 +2095,9 @@ export class AgentSession {
|
|
|
2095
2095
|
#xdevRegistry: XdevRegistry | undefined;
|
|
2096
2096
|
/** Names of discoverable tools currently mounted under `xd://` (dynamic mounts only, not built-in devices). */
|
|
2097
2097
|
#mountedXdevToolNames = new Set<string>();
|
|
2098
|
+
/** Coalesced xd:// mount delta not yet announced to the model; delivered as a
|
|
2099
|
+
* hidden notice alongside the next prompt (see {@link #notifyXdevMountDelta}). */
|
|
2100
|
+
#pendingXdevMountDelta: { added: Set<string>; removed: Set<string> } | undefined;
|
|
2098
2101
|
|
|
2099
2102
|
// TTSR manager for time-traveling stream rules
|
|
2100
2103
|
#ttsrManager: TtsrManager | undefined = undefined;
|
|
@@ -2242,6 +2245,17 @@ export class AgentSession {
|
|
|
2242
2245
|
* queue was consumed normally or a new turn already started. */
|
|
2243
2246
|
#drainStrandedQueuedMessages(): void {
|
|
2244
2247
|
if (this.#abortInProgress) return;
|
|
2248
|
+
// Session transitions (newSession/`/new`, compact, model-switch, session-switch,
|
|
2249
|
+
// dispose) call #disconnectFromAgent() BEFORE `await abort()`, so abort's own
|
|
2250
|
+
// finally lands here with no listener attached. Auto-resuming now would snapshot
|
|
2251
|
+
// the still-old context (the transition hasn't reached agent.reset() yet), start a
|
|
2252
|
+
// stale provider turn that races the reset, and — once reconnected — append its
|
|
2253
|
+
// output to the fresh session (issue #5800). A disconnected session never owns the
|
|
2254
|
+
// queue: the transition does. newSession/switchSession drop the queue (reset /
|
|
2255
|
+
// clearAllQueues), so nothing survives; compaction preserves it and re-drains itself
|
|
2256
|
+
// after #reconnectToAgent (see compact()'s finally); an explicit prompt flushes it
|
|
2257
|
+
// in every case.
|
|
2258
|
+
if (this.#unsubscribeAgent === undefined) return;
|
|
2245
2259
|
// A concern steered into a resumed streaming run after a user interrupt can
|
|
2246
2260
|
// strand at the turn tail (steered past the loop's final boundary poll). While
|
|
2247
2261
|
// that interrupt's suppression is still in effect, reclaim such advisor steers
|
|
@@ -7307,35 +7321,62 @@ export class AgentSession {
|
|
|
7307
7321
|
}
|
|
7308
7322
|
|
|
7309
7323
|
/**
|
|
7310
|
-
*
|
|
7311
|
-
*
|
|
7312
|
-
*
|
|
7313
|
-
*
|
|
7314
|
-
*
|
|
7315
|
-
*
|
|
7324
|
+
* Record a mid-session `xd://` mount delta for the model without rewriting
|
|
7325
|
+
* the system prompt: the prompt (and its provider cache prefix) stays
|
|
7326
|
+
* byte-stable across MCP connects and disconnects. The delta is NOT steered
|
|
7327
|
+
* immediately — a steered notice landing at a run's stop boundary (or while
|
|
7328
|
+
* the session is idle) forces an unsolicited extra assistant turn — it is
|
|
7329
|
+
* coalesced into {@link #pendingXdevMountDelta} and rides along with the
|
|
7330
|
+
* next prompt (docs + schema stay one `read xd://<tool>` away). The full
|
|
7331
|
+
* docs join the system prompt opportunistically on the next unrelated
|
|
7332
|
+
* rebuild.
|
|
7316
7333
|
*/
|
|
7317
7334
|
#notifyXdevMountDelta(previousMounted: ReadonlySet<string>): void {
|
|
7318
7335
|
const registry = this.#xdevRegistry;
|
|
7319
7336
|
if (!registry) return;
|
|
7320
7337
|
const current = this.#mountedXdevToolNames;
|
|
7321
7338
|
const addedNames = [...current].filter(name => !previousMounted.has(name));
|
|
7322
|
-
const
|
|
7323
|
-
if (addedNames.length === 0 &&
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7339
|
+
const removedNames = [...previousMounted].filter(name => !current.has(name));
|
|
7340
|
+
if (addedNames.length === 0 && removedNames.length === 0) return;
|
|
7341
|
+
// Coalesce against the unannounced delta: an unmount cancels a pending
|
|
7342
|
+
// mount the model never learned about, and a remount cancels a pending
|
|
7343
|
+
// unmount.
|
|
7344
|
+
const pending = this.#pendingXdevMountDelta ?? { added: new Set<string>(), removed: new Set<string>() };
|
|
7345
|
+
for (const name of addedNames) {
|
|
7346
|
+
if (!pending.removed.delete(name)) pending.added.add(name);
|
|
7347
|
+
}
|
|
7348
|
+
for (const name of removedNames) {
|
|
7349
|
+
if (!pending.added.delete(name)) pending.removed.add(name);
|
|
7350
|
+
}
|
|
7351
|
+
this.#pendingXdevMountDelta = pending.added.size > 0 || pending.removed.size > 0 ? pending : undefined;
|
|
7352
|
+
if (this.settings.get("startup.quiet")) return;
|
|
7353
|
+
const parts: string[] = [];
|
|
7354
|
+
if (addedNames.length > 0) parts.push(`mounted ${addedNames.join(", ")}`);
|
|
7355
|
+
if (removedNames.length > 0) parts.push(`unmounted ${removedNames.join(", ")}`);
|
|
7356
|
+
this.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
/**
|
|
7360
|
+
* Render and consume the pending xd:// mount delta as a hidden notice, or
|
|
7361
|
+
* `undefined` when nothing unannounced is queued. Called from the prompt
|
|
7362
|
+
* paths so the notice rides along with user input instead of forcing its
|
|
7363
|
+
* own model turn.
|
|
7364
|
+
*/
|
|
7365
|
+
#takePendingXdevMountNotice(): CustomMessage | undefined {
|
|
7366
|
+
const pending = this.#pendingXdevMountDelta;
|
|
7367
|
+
if (!pending) return undefined;
|
|
7368
|
+
this.#pendingXdevMountDelta = undefined;
|
|
7369
|
+
const summaries = new Map(this.#xdevRegistry?.entries().map(entry => [entry.name, entry.summary]) ?? []);
|
|
7370
|
+
const added = [...pending.added].map(name => ({ name, summary: summaries.get(name) ?? "" }));
|
|
7371
|
+
const removed = [...pending.removed].map(name => ({ name }));
|
|
7372
|
+
return {
|
|
7327
7373
|
role: "custom",
|
|
7328
7374
|
customType: XDEV_MOUNT_NOTICE_MESSAGE_TYPE,
|
|
7329
7375
|
content: prompt.render(xdevMountNoticePrompt, { added, removed }),
|
|
7330
7376
|
attribution: "agent",
|
|
7331
7377
|
display: false,
|
|
7332
7378
|
timestamp: Date.now(),
|
|
7333
|
-
}
|
|
7334
|
-
if (this.settings.get("startup.quiet")) return;
|
|
7335
|
-
const parts: string[] = [];
|
|
7336
|
-
if (added.length > 0) parts.push(`mounted ${added.map(entry => entry.name).join(", ")}`);
|
|
7337
|
-
if (removed.length > 0) parts.push(`unmounted ${removed.map(entry => entry.name).join(", ")}`);
|
|
7338
|
-
this.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
7379
|
+
};
|
|
7339
7380
|
}
|
|
7340
7381
|
|
|
7341
7382
|
/**
|
|
@@ -8693,14 +8734,19 @@ export class AgentSession {
|
|
|
8693
8734
|
messages.push(...options.prependMessages);
|
|
8694
8735
|
}
|
|
8695
8736
|
|
|
8696
|
-
messages.push(message);
|
|
8697
|
-
|
|
8698
8737
|
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
8699
8738
|
// return before mutating shared state (nextTurn messages, system prompt).
|
|
8700
8739
|
if (this.#promptGeneration !== generation) {
|
|
8701
8740
|
return;
|
|
8702
8741
|
}
|
|
8703
8742
|
|
|
8743
|
+
// A pending xd:// delta accompanies the next user-authored prompt,
|
|
8744
|
+
// never an agent-initiated continuation.
|
|
8745
|
+
const xdevMountNotice = isUserQueuedMessage(message) ? this.#takePendingXdevMountNotice() : undefined;
|
|
8746
|
+
if (xdevMountNotice) {
|
|
8747
|
+
messages.push(xdevMountNotice);
|
|
8748
|
+
}
|
|
8749
|
+
messages.push(message);
|
|
8704
8750
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
8705
8751
|
for (const msg of this.#pendingNextTurnMessages) {
|
|
8706
8752
|
messages.push(msg);
|
|
@@ -11014,6 +11060,13 @@ export class AgentSession {
|
|
|
11014
11060
|
this.#compactionAbortController = undefined;
|
|
11015
11061
|
}
|
|
11016
11062
|
this.#reconnectToAgent();
|
|
11063
|
+
// Compaction disconnected before `await abort()`, so abort's finally drain
|
|
11064
|
+
// (and any steer/follow-up that arrived mid-compaction — async IRC, an
|
|
11065
|
+
// `xd://` mount notice, an SDK/RPC steer) was suppressed while disconnected
|
|
11066
|
+
// (issue #5800). Unlike `/new`/switchSession, compaction preserves the agent
|
|
11067
|
+
// queues, so nothing else resumes them: re-drain now that the listener is back
|
|
11068
|
+
// and `isCompacting` is false, or the queued turn hangs until the next prompt.
|
|
11069
|
+
this.#drainStrandedQueuedMessages();
|
|
11017
11070
|
}
|
|
11018
11071
|
}
|
|
11019
11072
|
|
|
@@ -11912,11 +11965,12 @@ export class AgentSession {
|
|
|
11912
11965
|
#isEmptyAssistantStop(assistantMessage: AssistantMessage): boolean {
|
|
11913
11966
|
switch (assistantMessage.stopReason) {
|
|
11914
11967
|
case "stop":
|
|
11915
|
-
//
|
|
11916
|
-
//
|
|
11968
|
+
// Unsigned thinking alone is not actionable, but a signature is
|
|
11969
|
+
// provider-authenticated content and makes the stop terminal.
|
|
11917
11970
|
for (const content of assistantMessage.content) {
|
|
11918
11971
|
if (content.type === "toolCall") return false;
|
|
11919
11972
|
if (content.type === "text" && hasNonWhitespace(content.text)) return false;
|
|
11973
|
+
if (content.type === "thinking" && hasNonWhitespace(content.thinkingSignature ?? "")) return false;
|
|
11920
11974
|
}
|
|
11921
11975
|
return true;
|
|
11922
11976
|
case "toolUse":
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { ImageContent, MessageAttribution, ServiceTierByFamily, TextContent } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import type { StructuredSubagentSchemaMode } from "../task/types";
|
|
3
4
|
|
|
4
5
|
export const CURRENT_SESSION_VERSION = 3;
|
|
5
6
|
|
|
@@ -164,8 +165,12 @@ export interface SessionInitEntry extends SessionEntryBase {
|
|
|
164
165
|
task: string;
|
|
165
166
|
/** Tools available to the agent */
|
|
166
167
|
tools: string[];
|
|
167
|
-
/** Output schema if structured output was requested */
|
|
168
|
+
/** Output schema if structured output was requested. */
|
|
168
169
|
outputSchema?: unknown;
|
|
170
|
+
/** Enforcement policy recorded with the output schema for faithful revival. */
|
|
171
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
172
|
+
/** Whether revival must retain only the explicitly persisted tool names. */
|
|
173
|
+
restrictToolNames?: boolean;
|
|
169
174
|
/** Spawn allowlist the subagent ran with ("" = none, "*" = any, else CSV); absent on pre-spawns files. */
|
|
170
175
|
spawns?: string;
|
|
171
176
|
/** The agent's `readSummarize` setting (`false` = read summarization disabled); absent uses the session default. */
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
stringifyJson,
|
|
19
19
|
toError,
|
|
20
20
|
} from "@oh-my-pi/pi-utils";
|
|
21
|
+
import type { StructuredSubagentSchemaMode } from "../task/types";
|
|
21
22
|
import { ArtifactManager } from "./artifacts";
|
|
22
23
|
import { type BlobPutOptions, type BlobPutResult, BlobStore } from "./blob-store";
|
|
23
24
|
import {
|
|
@@ -1558,6 +1559,8 @@ export class SessionManager {
|
|
|
1558
1559
|
task: string;
|
|
1559
1560
|
tools: string[];
|
|
1560
1561
|
outputSchema?: unknown;
|
|
1562
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
1563
|
+
restrictToolNames?: boolean;
|
|
1561
1564
|
spawns?: string;
|
|
1562
1565
|
readSummarize?: boolean;
|
|
1563
1566
|
}): string {
|
|
@@ -1996,6 +1999,8 @@ export class SessionManager {
|
|
|
1996
1999
|
task: string;
|
|
1997
2000
|
tools: string[];
|
|
1998
2001
|
outputSchema?: unknown;
|
|
2002
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2003
|
+
restrictToolNames?: boolean;
|
|
1999
2004
|
spawns?: string;
|
|
2000
2005
|
readSummarize?: boolean;
|
|
2001
2006
|
} | null;
|
|
@@ -2014,6 +2019,8 @@ export class SessionManager {
|
|
|
2014
2019
|
task: string;
|
|
2015
2020
|
tools: string[];
|
|
2016
2021
|
outputSchema?: unknown;
|
|
2022
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2023
|
+
restrictToolNames?: boolean;
|
|
2017
2024
|
spawns?: string;
|
|
2018
2025
|
readSummarize?: boolean;
|
|
2019
2026
|
} | null = null;
|
|
@@ -2025,6 +2032,8 @@ export class SessionManager {
|
|
|
2025
2032
|
task: entry.task,
|
|
2026
2033
|
tools: entry.tools,
|
|
2027
2034
|
outputSchema: entry.outputSchema,
|
|
2035
|
+
outputSchemaMode: entry.outputSchemaMode,
|
|
2036
|
+
restrictToolNames: entry.restrictToolNames,
|
|
2028
2037
|
readSummarize: entry.readSummarize,
|
|
2029
2038
|
spawns: entry.spawns,
|
|
2030
2039
|
};
|
|
@@ -22,6 +22,7 @@ export const ARTIFACT_DEFAULT_MAX_BYTES = 0;
|
|
|
22
22
|
export const ARTIFACT_DEFAULT_HEAD_BYTES = 3 * 1024 * 1024; // 3 MiB
|
|
23
23
|
|
|
24
24
|
const NL = "\n";
|
|
25
|
+
const CR = "\r";
|
|
25
26
|
const ELLIPSIS = "…";
|
|
26
27
|
|
|
27
28
|
// =============================================================================
|
|
@@ -737,6 +738,7 @@ export class OutputSink {
|
|
|
737
738
|
#truncated = false;
|
|
738
739
|
#lastChunkTime = 0;
|
|
739
740
|
#pendingChunk = "";
|
|
741
|
+
#pendingCarriageReturn = false;
|
|
740
742
|
#pendingChunkTimer: Timer | undefined;
|
|
741
743
|
|
|
742
744
|
// Per-line column cap streaming state (persists across `push` calls so a
|
|
@@ -802,12 +804,45 @@ export class OutputSink {
|
|
|
802
804
|
this.#artifactTailBudget = Math.max(0, this.#artifactMaxBytes - this.#artifactHeadBudget);
|
|
803
805
|
}
|
|
804
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Converts carriage-return progress updates into line boundaries while
|
|
809
|
+
* collapsing CRLF to one newline. A trailing CR is held until the next
|
|
810
|
+
* chunk so split CRLF sequences do not create blank lines.
|
|
811
|
+
*/
|
|
812
|
+
#normalizeCarriageReturns(text: string): string {
|
|
813
|
+
if (text.length === 0 || (!this.#pendingCarriageReturn && !text.includes(CR))) return text;
|
|
814
|
+
|
|
815
|
+
let cursor = 0;
|
|
816
|
+
let normalized = "";
|
|
817
|
+
if (this.#pendingCarriageReturn) {
|
|
818
|
+
this.#pendingCarriageReturn = false;
|
|
819
|
+
normalized = NL;
|
|
820
|
+
if (text.startsWith(NL)) cursor = 1;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
while (cursor < text.length) {
|
|
824
|
+
const carriageReturn = text.indexOf(CR, cursor);
|
|
825
|
+
if (carriageReturn === -1) {
|
|
826
|
+
normalized += text.substring(cursor);
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
normalized += text.substring(cursor, carriageReturn);
|
|
830
|
+
if (carriageReturn === text.length - 1) {
|
|
831
|
+
this.#pendingCarriageReturn = true;
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
normalized += NL;
|
|
835
|
+
cursor = text.startsWith(NL, carriageReturn + 1) ? carriageReturn + 2 : carriageReturn + 1;
|
|
836
|
+
}
|
|
837
|
+
return normalized;
|
|
838
|
+
}
|
|
839
|
+
|
|
805
840
|
/**
|
|
806
841
|
* Push a chunk of output. The buffer management and onChunk callback run
|
|
807
842
|
* synchronously. File sink writes are deferred and serialized internally.
|
|
808
843
|
*/
|
|
809
844
|
push(chunk: string): void {
|
|
810
|
-
chunk = sanitizeWithOptionalSixelPassthrough(chunk, sanitizeText);
|
|
845
|
+
chunk = sanitizeWithOptionalSixelPassthrough(chunk, text => sanitizeText(this.#normalizeCarriageReturns(text)));
|
|
811
846
|
|
|
812
847
|
// Throttled onChunk: coalesce chunks arriving inside the throttle window.
|
|
813
848
|
// A timer flushes quiet tails at the throttle boundary; dump() catches a
|
|
@@ -1137,6 +1172,7 @@ export class OutputSink {
|
|
|
1137
1172
|
this.#columnDroppedBytes = 0;
|
|
1138
1173
|
this.#columnTruncatedLines = 0;
|
|
1139
1174
|
this.#pendingChunk = "";
|
|
1175
|
+
this.#pendingCarriageReturn = false;
|
|
1140
1176
|
}
|
|
1141
1177
|
|
|
1142
1178
|
#clearPendingChunkTimer(): void {
|
|
@@ -1208,6 +1244,10 @@ export class OutputSink {
|
|
|
1208
1244
|
}
|
|
1209
1245
|
|
|
1210
1246
|
async dump(notice?: string): Promise<OutputSummary> {
|
|
1247
|
+
if (this.#pendingCarriageReturn) {
|
|
1248
|
+
this.#pendingCarriageReturn = false;
|
|
1249
|
+
this.push(NL);
|
|
1250
|
+
}
|
|
1211
1251
|
const noticeLine = notice ? `[${notice}]\n` : "";
|
|
1212
1252
|
|
|
1213
1253
|
// Flush any chunk still held back by the throttle so the live preview
|
package/src/system-prompt.ts
CHANGED
|
@@ -729,13 +729,18 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
729
729
|
if (!toolPromptNames.has(mounted.name)) toolPromptNames.set(mounted.name, mounted.name);
|
|
730
730
|
}
|
|
731
731
|
const toolRefs = Object.fromEntries(toolPromptNames.entries());
|
|
732
|
-
const
|
|
732
|
+
const xdevToolNames = new Set(xdevTools.map(mounted => mounted.name));
|
|
733
|
+
// A direct custom tool can share a name with a retained built-in device.
|
|
734
|
+
// Presence in both toolNames and tools proves it still has a top-level definition.
|
|
735
|
+
const inventoryToolNames =
|
|
736
|
+
xdevToolNames.size === 0 ? toolNames : toolNames.filter(name => tools?.has(name) || !xdevToolNames.has(name));
|
|
737
|
+
const toolInfo = inventoryToolNames.map(name => ({
|
|
733
738
|
name: toolPromptNames.get(name) ?? name,
|
|
734
739
|
internalName: name,
|
|
735
740
|
label: tools?.get(name)?.label ?? "",
|
|
736
741
|
description: tools?.get(name)?.description ?? "",
|
|
737
742
|
}));
|
|
738
|
-
const inventoryTools =
|
|
743
|
+
const inventoryTools = inventoryToolNames.map(name => {
|
|
739
744
|
const meta = tools?.get(name);
|
|
740
745
|
return {
|
|
741
746
|
name: toolPromptNames.get(name) ?? name,
|
package/src/task/executor.ts
CHANGED
|
@@ -61,6 +61,9 @@ import {
|
|
|
61
61
|
MAX_OUTPUT_BYTES,
|
|
62
62
|
MAX_OUTPUT_LINES,
|
|
63
63
|
type SingleResult,
|
|
64
|
+
type StructuredSubagentOutput,
|
|
65
|
+
type StructuredSubagentSchemaMode,
|
|
66
|
+
type StructuredSubagentSchemaSource,
|
|
64
67
|
TASK_SUBAGENT_EVENT_CHANNEL,
|
|
65
68
|
TASK_SUBAGENT_LIFECYCLE_CHANNEL,
|
|
66
69
|
TASK_SUBAGENT_PROGRESS_CHANNEL,
|
|
@@ -292,7 +295,12 @@ export interface ExecutorOptions {
|
|
|
292
295
|
*/
|
|
293
296
|
parentActiveModelPattern?: string;
|
|
294
297
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
298
|
+
/** Schema used to validate the final structured completion. */
|
|
295
299
|
outputSchema?: unknown;
|
|
300
|
+
/** Enforcement policy for {@link outputSchema}; defaults to legacy permissive behavior. */
|
|
301
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
302
|
+
/** Origin of the selected schema, preserved in {@link SingleResult.structuredOutput}. */
|
|
303
|
+
outputSchemaSource?: StructuredSubagentSchemaSource;
|
|
296
304
|
/**
|
|
297
305
|
* Caller supplied a schema that supersedes the agent's native output prompt.
|
|
298
306
|
* Eval `agent(..., schema=...)` sets this so built-in agents ignore stale yield labels.
|
|
@@ -307,7 +315,20 @@ export interface ExecutorOptions {
|
|
|
307
315
|
* watchdog is already suspended for the call's duration.
|
|
308
316
|
*/
|
|
309
317
|
maxRuntimeMs?: number;
|
|
318
|
+
/** Include IRC only when the invocation policy permits collaboration. */
|
|
319
|
+
enableIrc?: boolean;
|
|
310
320
|
enableLsp?: boolean;
|
|
321
|
+
/**
|
|
322
|
+
* Enable MCP capabilities for this child. `false` suppresses both inherited
|
|
323
|
+
* MCP proxy tools and session MCP discovery; it never consults the
|
|
324
|
+
* process-global MCP manager. Defaults to `true`.
|
|
325
|
+
*/
|
|
326
|
+
enableMCP?: boolean;
|
|
327
|
+
/**
|
|
328
|
+
* Limit the child to its explicit host tool names and the required yield
|
|
329
|
+
* tool, suppressing discovered and always-included capabilities.
|
|
330
|
+
*/
|
|
331
|
+
restrictToolNames?: boolean;
|
|
311
332
|
signal?: AbortSignal;
|
|
312
333
|
onProgress?: (progress: AgentProgress) => void;
|
|
313
334
|
/**
|
|
@@ -450,6 +471,8 @@ interface FinalizeSubprocessOutputArgs {
|
|
|
450
471
|
signalAborted: boolean;
|
|
451
472
|
yieldItems?: YieldItem[];
|
|
452
473
|
outputSchema: unknown;
|
|
474
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
475
|
+
outputSchemaSource?: StructuredSubagentSchemaSource;
|
|
453
476
|
lastAssistantText?: string;
|
|
454
477
|
}
|
|
455
478
|
|
|
@@ -459,6 +482,7 @@ interface FinalizeSubprocessOutputResult {
|
|
|
459
482
|
stderr: string;
|
|
460
483
|
abortedViaYield: boolean;
|
|
461
484
|
hasYield: boolean;
|
|
485
|
+
structuredOutput?: StructuredSubagentOutput;
|
|
462
486
|
}
|
|
463
487
|
export const SUBAGENT_WARNING_SCHEMA_OVERRIDDEN =
|
|
464
488
|
"SYSTEM WARNING: Subagent exhausted schema-retry budget; result was accepted despite failing the output schema.";
|
|
@@ -494,6 +518,10 @@ function buildSchemaViolationOutcome(
|
|
|
494
518
|
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
|
|
495
519
|
let { rawOutput, exitCode, stderr } = args;
|
|
496
520
|
const { yieldItems, doneAborted, signalAborted, outputSchema, lastAssistantText } = args;
|
|
521
|
+
const mode = args.outputSchemaMode ?? "permissive";
|
|
522
|
+
const source = args.outputSchemaSource ?? (outputSchema === undefined ? "none" : "session");
|
|
523
|
+
const includeStructuredOutput = source !== "none";
|
|
524
|
+
let structuredOutput: StructuredSubagentOutput | undefined;
|
|
497
525
|
let abortedViaYield = false;
|
|
498
526
|
const hasYield = Array.isArray(yieldItems) && yieldItems.length > 0;
|
|
499
527
|
const hadFailureBeforeYield = exitCode !== 0 && stderr.trim().length > 0;
|
|
@@ -514,15 +542,35 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
514
542
|
if (!assembled || assembled.missingData) {
|
|
515
543
|
rawOutput = rawOutput ? `${SUBAGENT_WARNING_NULL_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_NULL_YIELD;
|
|
516
544
|
} else {
|
|
517
|
-
const { validator, error: schemaError } = buildOutputValidator(outputSchema);
|
|
545
|
+
const { validator, error: schemaError, normalized } = buildOutputValidator(outputSchema);
|
|
518
546
|
const completeData = assembled.rawText ? assembled.data : parseStringifiedJson(assembled.data ?? null);
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
547
|
+
const validation = validator?.validate(completeData);
|
|
548
|
+
const failure =
|
|
549
|
+
validation && !validation.success
|
|
550
|
+
? summarizeValidationFailure(validation, completeData, validator?.requiredFields ?? [])
|
|
551
|
+
: assembled.schemaOverridden
|
|
552
|
+
? { message: SUBAGENT_WARNING_SCHEMA_OVERRIDDEN, missingRequired: [] }
|
|
553
|
+
: schemaError
|
|
554
|
+
? { message: `invalid output schema: ${schemaError}`, missingRequired: [] }
|
|
555
|
+
: undefined;
|
|
556
|
+
if (includeStructuredOutput) {
|
|
557
|
+
structuredOutput =
|
|
558
|
+
schemaError || normalized === undefined
|
|
559
|
+
? {
|
|
560
|
+
source,
|
|
561
|
+
mode,
|
|
562
|
+
status: "unavailable",
|
|
563
|
+
data: completeData,
|
|
564
|
+
error: schemaError ? `invalid output schema: ${schemaError}` : undefined,
|
|
565
|
+
}
|
|
566
|
+
: failure
|
|
567
|
+
? { source, mode, status: "invalid", data: completeData, error: failure.message }
|
|
568
|
+
: { source, mode, status: "valid", data: completeData };
|
|
569
|
+
}
|
|
570
|
+
const mustReject =
|
|
571
|
+
failure !== undefined && (mode === "strict" || (!assembled.schemaOverridden && !schemaError));
|
|
572
|
+
if (mustReject && failure) {
|
|
573
|
+
const outcome = buildSchemaViolationOutcome(failure, completeData);
|
|
526
574
|
rawOutput = outcome.rawOutput;
|
|
527
575
|
stderr = outcome.stderr;
|
|
528
576
|
exitCode = outcome.exitCode;
|
|
@@ -540,9 +588,7 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
540
588
|
exitCode = 0;
|
|
541
589
|
stderr = assembled.schemaOverridden
|
|
542
590
|
? SUBAGENT_WARNING_SCHEMA_OVERRIDDEN
|
|
543
|
-
:
|
|
544
|
-
? `invalid output schema: ${schemaError}`
|
|
545
|
-
: "";
|
|
591
|
+
: (structuredOutput?.error ?? "");
|
|
546
592
|
} else if (!stderr) {
|
|
547
593
|
stderr = "Subagent failed after yielding a result.";
|
|
548
594
|
}
|
|
@@ -560,11 +606,22 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
560
606
|
const result = validator?.validate(completeData) ?? { success: true as const };
|
|
561
607
|
if (!result.success) {
|
|
562
608
|
const summary = summarizeValidationFailure(result, completeData, validator?.requiredFields ?? []);
|
|
609
|
+
if (includeStructuredOutput) {
|
|
610
|
+
structuredOutput = { source, mode, status: "invalid", data: completeData, error: summary.message };
|
|
611
|
+
}
|
|
563
612
|
const outcome = buildSchemaViolationOutcome(summary, completeData);
|
|
564
613
|
rawOutput = outcome.rawOutput;
|
|
565
614
|
stderr = outcome.stderr;
|
|
566
615
|
exitCode = outcome.exitCode;
|
|
567
616
|
} else {
|
|
617
|
+
if (includeStructuredOutput) {
|
|
618
|
+
structuredOutput = {
|
|
619
|
+
source,
|
|
620
|
+
mode,
|
|
621
|
+
status: "valid",
|
|
622
|
+
data: completeData,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
568
625
|
try {
|
|
569
626
|
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
570
627
|
} catch (err) {
|
|
@@ -587,7 +644,7 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
587
644
|
}
|
|
588
645
|
}
|
|
589
646
|
|
|
590
|
-
return { rawOutput, exitCode, stderr, abortedViaYield, hasYield };
|
|
647
|
+
return { rawOutput, exitCode, stderr, abortedViaYield, hasYield, structuredOutput };
|
|
591
648
|
}
|
|
592
649
|
|
|
593
650
|
/**
|
|
@@ -1710,6 +1767,8 @@ interface FinalizeRunArgs {
|
|
|
1710
1767
|
assignment?: string;
|
|
1711
1768
|
modelOverride?: string | string[];
|
|
1712
1769
|
outputSchema?: unknown;
|
|
1770
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
1771
|
+
outputSchemaSource?: StructuredSubagentSchemaSource;
|
|
1713
1772
|
signal?: AbortSignal;
|
|
1714
1773
|
artifactsDir?: string;
|
|
1715
1774
|
eventBus?: EventBus;
|
|
@@ -1747,6 +1806,8 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1747
1806
|
signalAborted: Boolean(signal?.aborted),
|
|
1748
1807
|
yieldItems,
|
|
1749
1808
|
outputSchema: args.outputSchema,
|
|
1809
|
+
outputSchemaMode: args.outputSchemaMode,
|
|
1810
|
+
outputSchemaSource: args.outputSchemaSource,
|
|
1750
1811
|
lastAssistantText: monitor.lastAssistantSalvageText(),
|
|
1751
1812
|
});
|
|
1752
1813
|
} finally {
|
|
@@ -1841,6 +1902,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1841
1902
|
output: truncatedOutput,
|
|
1842
1903
|
stderr,
|
|
1843
1904
|
truncated: Boolean(truncated),
|
|
1905
|
+
...(finalized.structuredOutput ? { structuredOutput: finalized.structuredOutput } : {}),
|
|
1844
1906
|
durationMs: Date.now() - args.startTime,
|
|
1845
1907
|
tokens: progress.tokens,
|
|
1846
1908
|
requests: progress.requests,
|
|
@@ -1935,6 +1997,10 @@ export interface FollowUpTurnOptions {
|
|
|
1935
1997
|
message: string;
|
|
1936
1998
|
index?: number;
|
|
1937
1999
|
description?: string;
|
|
2000
|
+
/** Structured-output state retained from the original invocation. */
|
|
2001
|
+
outputSchema?: unknown;
|
|
2002
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2003
|
+
outputSchemaSource?: StructuredSubagentSchemaSource;
|
|
1938
2004
|
signal?: AbortSignal;
|
|
1939
2005
|
onProgress?: (progress: AgentProgress) => void;
|
|
1940
2006
|
eventBus?: EventBus;
|
|
@@ -2019,6 +2085,9 @@ export async function runSubagentFollowUpTurn(options: FollowUpTurnOptions): Pro
|
|
|
2019
2085
|
id,
|
|
2020
2086
|
agent,
|
|
2021
2087
|
task: message,
|
|
2088
|
+
outputSchema: options.outputSchema,
|
|
2089
|
+
outputSchemaMode: options.outputSchemaMode,
|
|
2090
|
+
outputSchemaSource: options.outputSchemaSource,
|
|
2022
2091
|
signal,
|
|
2023
2092
|
artifactsDir: options.artifactsDir,
|
|
2024
2093
|
eventBus: options.eventBus,
|
|
@@ -2107,6 +2176,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2107
2176
|
const parentDepth = options.taskDepth ?? 0;
|
|
2108
2177
|
const childDepth = parentDepth + 1;
|
|
2109
2178
|
const atMaxDepth = maxRecursionDepth >= 0 && childDepth >= maxRecursionDepth;
|
|
2179
|
+
const ircEnabled = options.enableIrc !== false && isIrcEnabled(subagentSettings, childDepth);
|
|
2110
2180
|
|
|
2111
2181
|
// Add tools if specified
|
|
2112
2182
|
let toolNames: string[] | undefined;
|
|
@@ -2121,9 +2191,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2121
2191
|
if (atMaxDepth && toolNames?.includes("task")) {
|
|
2122
2192
|
toolNames = toolNames.filter(name => name !== "task");
|
|
2123
2193
|
}
|
|
2124
|
-
//
|
|
2125
|
-
//
|
|
2126
|
-
if (toolNames && !toolNames.includes("hub")) {
|
|
2194
|
+
// Ordinary agents retain the host's always-on collaboration capability.
|
|
2195
|
+
// Restricted sessions must not widen their explicit host tool list with hub.
|
|
2196
|
+
if (toolNames && !options.restrictToolNames && !toolNames.includes("hub")) {
|
|
2127
2197
|
toolNames = [...toolNames, "hub"];
|
|
2128
2198
|
}
|
|
2129
2199
|
if (toolNames?.includes("exec")) {
|
|
@@ -2145,7 +2215,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2145
2215
|
: agent.spawns.join(",");
|
|
2146
2216
|
|
|
2147
2217
|
const lspEnabled = enableLsp ?? true;
|
|
2148
|
-
const ircEnabled = isIrcEnabled(subagentSettings, childDepth);
|
|
2149
2218
|
const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
|
|
2150
2219
|
|
|
2151
2220
|
const monitor = createSubagentRunMonitor({
|
|
@@ -2354,8 +2423,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2354
2423
|
}
|
|
2355
2424
|
sessionOpenedAt = performance.now();
|
|
2356
2425
|
|
|
2357
|
-
const
|
|
2358
|
-
const enableMCP = !options.
|
|
2426
|
+
const restrictToolNames = options.restrictToolNames === true;
|
|
2427
|
+
const enableMCP = !restrictToolNames && (options.enableMCP ?? true);
|
|
2428
|
+
const mcpManager = enableMCP ? options.mcpManager : undefined;
|
|
2429
|
+
const mcpProxyTools = mcpManager ? createMCPProxyTools(mcpManager) : [];
|
|
2359
2430
|
|
|
2360
2431
|
// Derive subagent-scoped telemetry from the parent's config so the
|
|
2361
2432
|
// child loop's spans nest under the parent's active execute_tool span
|
|
@@ -2411,14 +2482,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2411
2482
|
thinkingLevel: effectiveThinkingLevel,
|
|
2412
2483
|
toolNames,
|
|
2413
2484
|
outputSchema,
|
|
2485
|
+
outputSchemaMode: options.outputSchemaMode,
|
|
2486
|
+
restrictToolNames: options.restrictToolNames,
|
|
2414
2487
|
requireYieldTool: true,
|
|
2415
2488
|
contextFiles: options.contextFiles,
|
|
2416
2489
|
skills: options.skills,
|
|
2417
2490
|
promptTemplates: options.promptTemplates,
|
|
2418
2491
|
workspaceTree: options.workspaceTree,
|
|
2419
2492
|
rules: options.rules,
|
|
2420
|
-
preloadedExtensionPaths: options.preloadedExtensionPaths,
|
|
2421
|
-
preloadedCustomToolPaths: options.preloadedCustomToolPaths,
|
|
2493
|
+
preloadedExtensionPaths: restrictToolNames ? [] : options.preloadedExtensionPaths,
|
|
2494
|
+
preloadedCustomToolPaths: restrictToolNames ? [] : options.preloadedCustomToolPaths,
|
|
2422
2495
|
systemPrompt: defaultPrompt => {
|
|
2423
2496
|
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
|
|
2424
2497
|
agent: agent.systemPrompt,
|
|
@@ -2447,9 +2520,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2447
2520
|
agentId: id,
|
|
2448
2521
|
agentDisplayName: agent.name,
|
|
2449
2522
|
enableLsp: lspEnabled,
|
|
2523
|
+
enableIrc: options.enableIrc,
|
|
2450
2524
|
skipPythonPreflight,
|
|
2451
2525
|
enableMCP,
|
|
2452
|
-
mcpManager
|
|
2526
|
+
mcpManager,
|
|
2453
2527
|
customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
|
|
2454
2528
|
localProtocolOptions: options.localProtocolOptions,
|
|
2455
2529
|
telemetry: subagentTelemetry,
|
|
@@ -2524,6 +2598,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2524
2598
|
spawns: spawnsEnv,
|
|
2525
2599
|
readSummarize: agent.readSummarize,
|
|
2526
2600
|
outputSchema,
|
|
2601
|
+
outputSchemaMode: options.outputSchemaMode,
|
|
2602
|
+
restrictToolNames: restrictToolNames || undefined,
|
|
2527
2603
|
});
|
|
2528
2604
|
|
|
2529
2605
|
abortSignal.addEventListener(
|
|
@@ -2720,6 +2796,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2720
2796
|
assignment,
|
|
2721
2797
|
modelOverride,
|
|
2722
2798
|
outputSchema,
|
|
2799
|
+
outputSchemaMode: options.outputSchemaMode,
|
|
2800
|
+
outputSchemaSource: options.outputSchemaSource,
|
|
2723
2801
|
signal,
|
|
2724
2802
|
artifactsDir: options.artifactsDir,
|
|
2725
2803
|
eventBus: options.eventBus,
|