@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.4
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 +60 -0
- package/dist/cli.js +3682 -3615
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/collab/replication-shrink.d.ts +39 -0
- package/dist/types/config/provider-globals.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +81 -0
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/memories/index.d.ts +20 -1
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +35 -1
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/command-controller.d.ts +3 -3
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +11 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +9 -3
- package/dist/types/session/agent-session.d.ts +58 -10
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +26 -0
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +8 -7
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +242 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +12 -2
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/autolearn/controller.ts +13 -22
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/cli/update-cli.ts +254 -0
- package/src/collab/guest.ts +43 -7
- package/src/collab/host.ts +13 -8
- package/src/collab/replication-shrink.ts +111 -0
- package/src/config/model-registry.ts +80 -18
- package/src/config/provider-globals.ts +25 -0
- package/src/config/settings-schema.ts +77 -0
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/edit/hashline/diff.ts +57 -4
- package/src/eval/__tests__/julia-prelude.test.ts +2 -2
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/manager.ts +2 -1
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +130 -15
- package/src/memory-backend/local-backend.ts +5 -3
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +150 -5
- package/src/modes/components/status-line/segments.ts +46 -21
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +27 -36
- package/src/modes/controllers/event-controller.ts +113 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +74 -25
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +9 -3
- package/src/modes/utils/ui-helpers.ts +41 -23
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/goals/goal-todo-context.md +12 -0
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +28 -24
- package/src/session/agent-session.ts +899 -429
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +172 -9
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +115 -22
- package/src/session/session-persistence.ts +95 -1
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/slash-commands/builtin-registry.ts +23 -24
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/text.ts +23 -0
- package/src/tools/ask.ts +55 -4
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/prompts/system/autolearn-nudge.md +0 -5
package/src/task/executor.ts
CHANGED
|
@@ -71,7 +71,11 @@ import {
|
|
|
71
71
|
TASK_SUBAGENT_LIFECYCLE_CHANNEL,
|
|
72
72
|
TASK_SUBAGENT_PROGRESS_CHANNEL,
|
|
73
73
|
type TaskToolDetails,
|
|
74
|
+
type YieldItem,
|
|
74
75
|
} from "./types";
|
|
76
|
+
import { arrayValuedLabels, assembleYieldResult } from "./yield-assembly";
|
|
77
|
+
|
|
78
|
+
export type { YieldItem } from "./types";
|
|
75
79
|
|
|
76
80
|
const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
77
81
|
|
|
@@ -517,21 +521,6 @@ function resolveFallbackCompletion(rawOutput: string, outputSchema: unknown): {
|
|
|
517
521
|
return { data: candidate };
|
|
518
522
|
}
|
|
519
523
|
|
|
520
|
-
export interface YieldItem {
|
|
521
|
-
data?: unknown;
|
|
522
|
-
status?: "success" | "aborted";
|
|
523
|
-
error?: string;
|
|
524
|
-
/**
|
|
525
|
-
* Set by the in-tool yield validator when it exhausted its retry budget
|
|
526
|
-
* (MAX_SCHEMA_RETRIES) and accepted a schema-invalid payload anyway.
|
|
527
|
-
* `finalizeSubprocessOutput` honors this by serializing the payload and
|
|
528
|
-
* surfacing a stderr warning, instead of re-emitting `schema_violation`
|
|
529
|
-
* — which would silently swap the subagent's "accepted" view for a
|
|
530
|
-
* different, opaque error blob in the parent's view of the result.
|
|
531
|
-
*/
|
|
532
|
-
schemaOverridden?: boolean;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
524
|
interface FinalizeSubprocessOutputArgs {
|
|
536
525
|
rawOutput: string;
|
|
537
526
|
exitCode: number;
|
|
@@ -541,6 +530,7 @@ interface FinalizeSubprocessOutputArgs {
|
|
|
541
530
|
yieldItems?: YieldItem[];
|
|
542
531
|
reportFindings?: ReviewFinding[];
|
|
543
532
|
outputSchema: unknown;
|
|
533
|
+
lastAssistantText?: string;
|
|
544
534
|
}
|
|
545
535
|
|
|
546
536
|
interface FinalizeSubprocessOutputResult {
|
|
@@ -583,7 +573,7 @@ function buildSchemaViolationOutcome(
|
|
|
583
573
|
|
|
584
574
|
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
|
|
585
575
|
let { rawOutput, exitCode, stderr } = args;
|
|
586
|
-
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
|
|
576
|
+
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema, lastAssistantText } = args;
|
|
587
577
|
let abortedViaYield = false;
|
|
588
578
|
const hasYield = Array.isArray(yieldItems) && yieldItems.length > 0;
|
|
589
579
|
const hadFailureBeforeYield = exitCode !== 0 && stderr.trim().length > 0;
|
|
@@ -600,15 +590,16 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
600
590
|
rawOutput = `{"aborted":true,"error":"${lastYield.error || "Unknown error"}"}`;
|
|
601
591
|
}
|
|
602
592
|
} else {
|
|
603
|
-
const
|
|
604
|
-
if (
|
|
593
|
+
const assembled = assembleYieldResult(yieldItems, lastAssistantText, arrayValuedLabels(outputSchema));
|
|
594
|
+
if (!assembled || assembled.missingData) {
|
|
605
595
|
rawOutput = rawOutput ? `${SUBAGENT_WARNING_NULL_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_NULL_YIELD;
|
|
606
596
|
} else {
|
|
607
597
|
const { validator, error: schemaError } = buildOutputValidator(outputSchema);
|
|
608
|
-
const
|
|
609
|
-
|
|
598
|
+
const completeData = assembled.rawText
|
|
599
|
+
? assembled.data
|
|
600
|
+
: normalizeCompleteData(assembled.data, reportFindings, validator);
|
|
610
601
|
const result =
|
|
611
|
-
schemaError ||
|
|
602
|
+
schemaError || assembled.schemaOverridden
|
|
612
603
|
? { success: true as const }
|
|
613
604
|
: (validator?.validate(completeData) ?? { success: true as const });
|
|
614
605
|
if (!result.success) {
|
|
@@ -619,14 +610,17 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
619
610
|
exitCode = outcome.exitCode;
|
|
620
611
|
} else {
|
|
621
612
|
try {
|
|
622
|
-
rawOutput =
|
|
613
|
+
rawOutput =
|
|
614
|
+
assembled.rawText && typeof completeData === "string"
|
|
615
|
+
? completeData
|
|
616
|
+
: (JSON.stringify(completeData, null, 2) ?? "null");
|
|
623
617
|
} catch (err) {
|
|
624
618
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
625
619
|
rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
|
|
626
620
|
}
|
|
627
621
|
if (!hadFailureBeforeYield) {
|
|
628
622
|
exitCode = 0;
|
|
629
|
-
stderr =
|
|
623
|
+
stderr = assembled.schemaOverridden
|
|
630
624
|
? SUBAGENT_WARNING_SCHEMA_OVERRIDDEN
|
|
631
625
|
: schemaError
|
|
632
626
|
? `invalid output schema: ${schemaError}`
|
|
@@ -925,6 +919,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
925
919
|
cacheRead: 0,
|
|
926
920
|
cacheWrite: 0,
|
|
927
921
|
totalTokens: 0,
|
|
922
|
+
reasoningTokens: 0,
|
|
928
923
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
929
924
|
};
|
|
930
925
|
let hasUsage = false;
|
|
@@ -1315,6 +1310,8 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1315
1310
|
accumulatedUsage.cacheRead += getNumberField(usageRecord, "cacheRead") ?? 0;
|
|
1316
1311
|
accumulatedUsage.cacheWrite += getNumberField(usageRecord, "cacheWrite") ?? 0;
|
|
1317
1312
|
accumulatedUsage.totalTokens += getNumberField(usageRecord, "totalTokens") ?? 0;
|
|
1313
|
+
accumulatedUsage.reasoningTokens =
|
|
1314
|
+
(accumulatedUsage.reasoningTokens ?? 0) + (getNumberField(usageRecord, "reasoningTokens") ?? 0);
|
|
1318
1315
|
if (costRecord) {
|
|
1319
1316
|
accumulatedUsage.cost.input += getNumberField(costRecord, "input") ?? 0;
|
|
1320
1317
|
accumulatedUsage.cost.output += getNumberField(costRecord, "output") ?? 0;
|
|
@@ -1665,6 +1662,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1665
1662
|
yieldItems,
|
|
1666
1663
|
reportFindings,
|
|
1667
1664
|
outputSchema: args.outputSchema,
|
|
1665
|
+
lastAssistantText: monitor.lastAssistantSalvageText(),
|
|
1668
1666
|
});
|
|
1669
1667
|
} finally {
|
|
1670
1668
|
popLoopPhase();
|
package/src/task/render.ts
CHANGED
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
import { framedBlock, renderStatusLine } from "../tui";
|
|
35
35
|
import { repairDoubleEncodedJsonString } from "./repair-args";
|
|
36
36
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
37
|
-
import type { AgentProgress, SingleResult, TaskItem, TaskParams, TaskToolDetails } from "./types";
|
|
37
|
+
import type { AgentProgress, SingleResult, TaskItem, TaskParams, TaskToolDetails, YieldItem } from "./types";
|
|
38
|
+
import { assembleYieldResult } from "./yield-assembly";
|
|
38
39
|
|
|
39
40
|
/** Render context threaded in from `ToolExecutionComponent.#buildRenderContext`. */
|
|
40
41
|
interface TaskRenderContext {
|
|
@@ -143,6 +144,49 @@ function normalizeReportFindings(value: unknown): ReportFindingDetails[] {
|
|
|
143
144
|
return findings;
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/** Reviewer output declares `findings` as an array, so a lone finding section still assembles as a list. */
|
|
148
|
+
const REVIEWER_ARRAY_LABELS: ReadonlySet<string> = new Set(["findings"]);
|
|
149
|
+
|
|
150
|
+
function extractIncrementalReviewResult(
|
|
151
|
+
value: unknown,
|
|
152
|
+
): { summary: SubmitReviewDetails; findings: ReportFindingDetails[] } | undefined {
|
|
153
|
+
const yieldItems: YieldItem[] = normalizeYieldData(value).map(item => ({
|
|
154
|
+
data: item.data,
|
|
155
|
+
type: item.type,
|
|
156
|
+
status: item.status === "aborted" ? "aborted" : item.status === "success" ? "success" : undefined,
|
|
157
|
+
useLastTurn: item.useLastTurn,
|
|
158
|
+
}));
|
|
159
|
+
const assembled = assembleYieldResult(yieldItems, undefined, REVIEWER_ARRAY_LABELS);
|
|
160
|
+
const data = assembled?.data;
|
|
161
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return undefined;
|
|
162
|
+
const record = data as Record<string, unknown>;
|
|
163
|
+
const overallCorrectness = record.overall_correctness;
|
|
164
|
+
const explanation = record.explanation;
|
|
165
|
+
const confidence = record.confidence;
|
|
166
|
+
if (
|
|
167
|
+
(overallCorrectness !== "correct" && overallCorrectness !== "incorrect") ||
|
|
168
|
+
typeof explanation !== "string" ||
|
|
169
|
+
typeof confidence !== "number"
|
|
170
|
+
) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
summary: {
|
|
175
|
+
overall_correctness: overallCorrectness,
|
|
176
|
+
explanation,
|
|
177
|
+
confidence,
|
|
178
|
+
},
|
|
179
|
+
findings: normalizeReportFindings(record.findings),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface RenderYieldItem {
|
|
184
|
+
data?: unknown;
|
|
185
|
+
type?: string | string[];
|
|
186
|
+
status?: string;
|
|
187
|
+
useLastTurn?: boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
146
190
|
/**
|
|
147
191
|
* Normalize the `yield` slot of `extractedToolData` into an array of
|
|
148
192
|
* yield-detail records. The subprocess executor always populates this slot as
|
|
@@ -153,14 +197,82 @@ function normalizeReportFindings(value: unknown): ReportFindingDetails[] {
|
|
|
153
197
|
* A single object is wrapped as a 1-element array so the review verdict still
|
|
154
198
|
* renders; non-object primitives drop out.
|
|
155
199
|
*/
|
|
156
|
-
function normalizeYieldData(value: unknown):
|
|
157
|
-
|
|
158
|
-
|
|
200
|
+
function normalizeYieldData(value: unknown): RenderYieldItem[] {
|
|
201
|
+
const items = Array.isArray(value) ? value : value !== null && typeof value === "object" ? [value] : [];
|
|
202
|
+
const normalized: RenderYieldItem[] = [];
|
|
203
|
+
for (const item of items) {
|
|
204
|
+
if (item === null || typeof item !== "object") continue;
|
|
205
|
+
const record = item as Record<string, unknown>;
|
|
206
|
+
const typeValue = record.type;
|
|
207
|
+
let type: RenderYieldItem["type"];
|
|
208
|
+
if (typeof typeValue === "string") {
|
|
209
|
+
type = typeValue;
|
|
210
|
+
} else if (Array.isArray(typeValue)) {
|
|
211
|
+
const labels: string[] = [];
|
|
212
|
+
let allLabels = true;
|
|
213
|
+
for (const label of typeValue) {
|
|
214
|
+
if (typeof label !== "string") {
|
|
215
|
+
allLabels = false;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
labels.push(label);
|
|
219
|
+
}
|
|
220
|
+
if (allLabels) type = labels;
|
|
221
|
+
}
|
|
222
|
+
normalized.push({
|
|
223
|
+
data: record.data,
|
|
224
|
+
type,
|
|
225
|
+
status: typeof record.status === "string" ? record.status : undefined,
|
|
226
|
+
useLastTurn: record.useLastTurn === true ? true : undefined,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return normalized;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getRenderYieldLabels(type: RenderYieldItem["type"]): string[] {
|
|
233
|
+
if (typeof type === "string") {
|
|
234
|
+
const label = type.trim();
|
|
235
|
+
return label ? [label] : [];
|
|
236
|
+
}
|
|
237
|
+
if (!Array.isArray(type)) return [];
|
|
238
|
+
const labels: string[] = [];
|
|
239
|
+
for (const value of type) {
|
|
240
|
+
const label = value.trim();
|
|
241
|
+
if (label) labels.push(label);
|
|
242
|
+
}
|
|
243
|
+
return labels;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatYieldPreview(item: RenderYieldItem): string {
|
|
247
|
+
if (item.useLastTurn === true && item.data === undefined) return "last assistant turn";
|
|
248
|
+
if (item.data === undefined) return "last assistant turn";
|
|
249
|
+
if (typeof item.data === "string") return previewLine(replaceTabs(item.data), 70);
|
|
250
|
+
try {
|
|
251
|
+
return previewLine(replaceTabs(JSON.stringify(item.data) ?? "null"), 70);
|
|
252
|
+
} catch {
|
|
253
|
+
return previewLine(replaceTabs(String(item.data)), 70);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderTypedYieldSections(value: unknown, continuePrefix: string, expanded: boolean, theme: Theme): string[] {
|
|
258
|
+
const typedItems: Array<{ item: RenderYieldItem; labels: string[] }> = [];
|
|
259
|
+
for (const item of normalizeYieldData(value)) {
|
|
260
|
+
const labels = getRenderYieldLabels(item.type);
|
|
261
|
+
if (labels.length === 0) continue;
|
|
262
|
+
typedItems.push({ item, labels });
|
|
159
263
|
}
|
|
160
|
-
|
|
161
|
-
|
|
264
|
+
const displayCount = expanded ? typedItems.length : 3;
|
|
265
|
+
const lines: string[] = [];
|
|
266
|
+
for (const { item, labels } of typedItems.slice(-displayCount)) {
|
|
267
|
+
const terminal = !Array.isArray(item.type);
|
|
268
|
+
const prefix = terminal ? "yield" : "yield+";
|
|
269
|
+
const label = `${prefix}[${labels.join(", ")}]`;
|
|
270
|
+
lines.push(`${continuePrefix}${theme.fg("dim", label)}: ${theme.fg("dim", formatYieldPreview(item))}`);
|
|
271
|
+
}
|
|
272
|
+
if (typedItems.length > displayCount) {
|
|
273
|
+
lines.push(`${continuePrefix}${theme.fg("dim", formatMoreItems(typedItems.length - displayCount, "yield"))}`);
|
|
162
274
|
}
|
|
163
|
-
return
|
|
275
|
+
return lines;
|
|
164
276
|
}
|
|
165
277
|
|
|
166
278
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
@@ -808,10 +920,24 @@ function renderAgentProgress(
|
|
|
808
920
|
|
|
809
921
|
// Render extracted tool data inline (e.g., review findings)
|
|
810
922
|
if (progress.extractedToolData) {
|
|
811
|
-
// For completed tasks,
|
|
923
|
+
// For completed tasks, prefer review verdicts assembled from incremental
|
|
924
|
+
// yield sections. Fall back to the legacy `report_finding` side-channel.
|
|
812
925
|
if (progress.status === "completed") {
|
|
813
926
|
const completeData = normalizeYieldData(progress.extractedToolData.yield);
|
|
927
|
+
const incrementalReview = extractIncrementalReviewResult(progress.extractedToolData.yield);
|
|
814
928
|
const reportFindingData = normalizeReportFindings(progress.extractedToolData.report_finding);
|
|
929
|
+
if (incrementalReview) {
|
|
930
|
+
lines.push(
|
|
931
|
+
...renderReviewResult(
|
|
932
|
+
incrementalReview.summary,
|
|
933
|
+
incrementalReview.findings,
|
|
934
|
+
continuePrefix,
|
|
935
|
+
expanded,
|
|
936
|
+
theme,
|
|
937
|
+
),
|
|
938
|
+
);
|
|
939
|
+
return lines; // Review result handles its own rendering
|
|
940
|
+
}
|
|
815
941
|
const reviewData = completeData
|
|
816
942
|
.map(c => c.data as SubmitReviewDetails)
|
|
817
943
|
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
|
|
@@ -825,6 +951,11 @@ function renderAgentProgress(
|
|
|
825
951
|
|
|
826
952
|
for (const toolName in progress.extractedToolData) {
|
|
827
953
|
const dataArray = progress.extractedToolData[toolName];
|
|
954
|
+
if (toolName === "yield") {
|
|
955
|
+
lines.push(...renderTypedYieldSections(dataArray, continuePrefix, expanded, theme));
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
|
|
828
959
|
// Handle report_finding with tree formatting
|
|
829
960
|
if (toolName === "report_finding") {
|
|
830
961
|
const findings = normalizeReportFindings(dataArray);
|
|
@@ -1068,23 +1199,30 @@ function renderAgentResult(
|
|
|
1068
1199
|
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", previewLine(result.abortReason, 80))}`,
|
|
1069
1200
|
);
|
|
1070
1201
|
}
|
|
1071
|
-
// Check for review result
|
|
1072
|
-
//
|
|
1202
|
+
// Check for review result, preferring incremental yield sections and falling
|
|
1203
|
+
// back to the legacy `report_finding` side-channel.
|
|
1073
1204
|
// `normalizeYieldData` guards against a stray non-array `yield` slot —
|
|
1074
1205
|
// optional chaining on `.map` only short-circuits on null/undefined and
|
|
1075
1206
|
// would otherwise crash the renderer with `TypeError: completeData?.map
|
|
1076
1207
|
// is not a function` when the slot is a plain object (see issue #1987).
|
|
1077
1208
|
const completeData = normalizeYieldData(result.extractedToolData?.yield);
|
|
1078
1209
|
const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
|
|
1210
|
+
const incrementalReview = extractIncrementalReviewResult(result.extractedToolData?.yield);
|
|
1211
|
+
|
|
1212
|
+
if (incrementalReview) {
|
|
1213
|
+
lines.push(
|
|
1214
|
+
...renderReviewResult(incrementalReview.summary, incrementalReview.findings, continuePrefix, expanded, theme),
|
|
1215
|
+
);
|
|
1216
|
+
return lines;
|
|
1217
|
+
}
|
|
1079
1218
|
|
|
1080
|
-
// Extract review verdict from yield
|
|
1219
|
+
// Extract review verdict from legacy yield summary objects if present.
|
|
1081
1220
|
const reviewData = completeData
|
|
1082
1221
|
.map(c => c.data as SubmitReviewDetails)
|
|
1083
1222
|
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
|
|
1084
1223
|
const submitReviewData = reviewData.length > 0 ? reviewData : undefined;
|
|
1085
1224
|
|
|
1086
1225
|
if (submitReviewData) {
|
|
1087
|
-
// Use combined review renderer
|
|
1088
1226
|
const summary = submitReviewData[submitReviewData.length - 1];
|
|
1089
1227
|
const findings = reportFindingData;
|
|
1090
1228
|
lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
|
|
@@ -1105,9 +1243,18 @@ function renderAgentResult(
|
|
|
1105
1243
|
let hasCustomRendering = false;
|
|
1106
1244
|
const deferredToolLines: string[] = [];
|
|
1107
1245
|
if (result.extractedToolData) {
|
|
1108
|
-
for (const
|
|
1246
|
+
for (const toolName in result.extractedToolData) {
|
|
1247
|
+
const dataArray = result.extractedToolData[toolName];
|
|
1248
|
+
if (toolName === "yield") {
|
|
1249
|
+
const yieldLines = renderTypedYieldSections(dataArray, continuePrefix, expanded, theme);
|
|
1250
|
+
if (yieldLines.length > 0) {
|
|
1251
|
+
hasCustomRendering = true;
|
|
1252
|
+
lines.push(...yieldLines);
|
|
1253
|
+
}
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1109
1256
|
// Skip review tools - handled above
|
|
1110
|
-
if (toolName === "
|
|
1257
|
+
if (toolName === "report_finding") continue;
|
|
1111
1258
|
|
|
1112
1259
|
const isTaskTool = toolName === "task";
|
|
1113
1260
|
if (isTaskTool && (dataArray as unknown[]).length > 0) {
|
|
@@ -1541,6 +1688,7 @@ function renderNestedTaskTree(
|
|
|
1541
1688
|
return lines;
|
|
1542
1689
|
}
|
|
1543
1690
|
|
|
1691
|
+
// Register task tool subprocess handler
|
|
1544
1692
|
subprocessToolRegistry.register<TaskToolDetails>("task", {
|
|
1545
1693
|
extractData: event => {
|
|
1546
1694
|
const details = event.result?.details;
|
|
@@ -1551,9 +1699,3 @@ subprocessToolRegistry.register<TaskToolDetails>("task", {
|
|
|
1551
1699
|
return new Text(lines.join("\n"), 0, 0);
|
|
1552
1700
|
},
|
|
1553
1701
|
});
|
|
1554
|
-
|
|
1555
|
-
export const taskToolRenderer = {
|
|
1556
|
-
renderCall,
|
|
1557
|
-
renderResult,
|
|
1558
|
-
mergeCallAndResult: true,
|
|
1559
|
-
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task tool renderer export.
|
|
3
|
+
*
|
|
4
|
+
* Separated from render.ts to avoid circular dependency issues with
|
|
5
|
+
* tools/renderers.ts. This module has no side effects and can be safely
|
|
6
|
+
* imported without triggering the subprocessToolRegistry registration.
|
|
7
|
+
*/
|
|
8
|
+
import { renderCall, renderResult } from "./render";
|
|
9
|
+
|
|
10
|
+
export const taskToolRenderer = {
|
|
11
|
+
renderCall,
|
|
12
|
+
renderResult,
|
|
13
|
+
mergeCallAndResult: true,
|
|
14
|
+
} as const;
|
package/src/task/types.ts
CHANGED
|
@@ -257,6 +257,23 @@ export interface AgentDefinition {
|
|
|
257
257
|
filePath?: string;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/** Details extracted from a subagent `yield` tool call for final-result assembly and task rendering. */
|
|
261
|
+
export interface YieldItem {
|
|
262
|
+
data?: unknown;
|
|
263
|
+
status?: "success" | "aborted";
|
|
264
|
+
error?: string;
|
|
265
|
+
/** A string label is terminal; a non-empty array of labels is incremental. */
|
|
266
|
+
type?: string | string[];
|
|
267
|
+
/** Resolve this yield's payload from the latest durable assistant text instead of `data`. */
|
|
268
|
+
useLastTurn?: boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Set by the in-tool yield validator when it exhausted its retry budget and
|
|
271
|
+
* accepted schema-invalid data anyway. The executor preserves that override
|
|
272
|
+
* during post-mortem validation.
|
|
273
|
+
*/
|
|
274
|
+
schemaOverridden?: boolean;
|
|
275
|
+
}
|
|
276
|
+
|
|
260
277
|
/** Progress tracking for a single agent */
|
|
261
278
|
export interface AgentProgress {
|
|
262
279
|
index: number;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure assembly of subagent `yield` calls into the final payload consumed by
|
|
3
|
+
* output-schema validation.
|
|
4
|
+
*
|
|
5
|
+
* Lives apart from the subagent runtime in `executor.ts` so the rendering path
|
|
6
|
+
* (`render.ts` → `extractIncrementalReviewResult`) can assemble incremental
|
|
7
|
+
* yields without importing that runtime's dependency graph (`sdk`,
|
|
8
|
+
* `session-manager`, the TUI tool renderers). It has no side effects and
|
|
9
|
+
* depends only on the yield type and the output-schema validator.
|
|
10
|
+
*/
|
|
11
|
+
import { buildOutputValidator } from "../tools/output-schema-validator";
|
|
12
|
+
import type { YieldItem } from "./types";
|
|
13
|
+
|
|
14
|
+
/** Outcome of folding a run's yield calls into one payload, with provenance flags. */
|
|
15
|
+
interface AssembledYieldResult {
|
|
16
|
+
data: unknown;
|
|
17
|
+
schemaOverridden: boolean;
|
|
18
|
+
rawText: boolean;
|
|
19
|
+
missingData: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isIncrementalYieldType(type: YieldItem["type"]): type is string[] {
|
|
23
|
+
return Array.isArray(type) && type.length > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getYieldLabels(type: YieldItem["type"]): string[] {
|
|
27
|
+
if (typeof type === "string") {
|
|
28
|
+
const label = type.trim();
|
|
29
|
+
return label ? [label] : [];
|
|
30
|
+
}
|
|
31
|
+
if (!Array.isArray(type)) return [];
|
|
32
|
+
const labels: string[] = [];
|
|
33
|
+
for (const value of type) {
|
|
34
|
+
if (typeof value !== "string") continue;
|
|
35
|
+
const label = value.trim();
|
|
36
|
+
if (label) labels.push(label);
|
|
37
|
+
}
|
|
38
|
+
return labels;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveYieldPayload(
|
|
42
|
+
item: YieldItem,
|
|
43
|
+
lastAssistantText: string | undefined,
|
|
44
|
+
labels: string[],
|
|
45
|
+
): { value: unknown; fromLastAssistantText: boolean; missingData: boolean } {
|
|
46
|
+
const hasData = item.data !== undefined;
|
|
47
|
+
const shouldUseLastTurn = item.useLastTurn === true || (labels.length > 0 && !hasData);
|
|
48
|
+
if (shouldUseLastTurn && lastAssistantText !== undefined) {
|
|
49
|
+
return {
|
|
50
|
+
value: lastAssistantText,
|
|
51
|
+
fromLastAssistantText: true,
|
|
52
|
+
missingData: lastAssistantText.length === 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
value: item.data,
|
|
57
|
+
fromLastAssistantText: false,
|
|
58
|
+
missingData: item.data === undefined || item.data === null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function appendYieldSection(
|
|
63
|
+
sections: Record<string, unknown>,
|
|
64
|
+
sectionCounts: Map<string, number>,
|
|
65
|
+
label: string,
|
|
66
|
+
value: unknown,
|
|
67
|
+
forceArray: boolean,
|
|
68
|
+
): void {
|
|
69
|
+
const count = sectionCounts.get(label) ?? 0;
|
|
70
|
+
const existing = sections[label];
|
|
71
|
+
if (count === 0) {
|
|
72
|
+
sections[label] = forceArray ? [value] : value;
|
|
73
|
+
} else if (Array.isArray(existing)) {
|
|
74
|
+
existing.push(value);
|
|
75
|
+
} else {
|
|
76
|
+
sections[label] = [existing, value];
|
|
77
|
+
}
|
|
78
|
+
sectionCounts.set(label, count + 1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True when `value` is a JSON-schema node whose instances are arrays. */
|
|
82
|
+
function isArrayTypedSchema(value: unknown): boolean {
|
|
83
|
+
if (value === null || typeof value !== "object") return false;
|
|
84
|
+
const record = value as Record<string, unknown>;
|
|
85
|
+
if (record.type === "array") return true;
|
|
86
|
+
if (Array.isArray(record.type) && record.type.includes("array")) return true;
|
|
87
|
+
for (const key of ["anyOf", "oneOf", "allOf"] as const) {
|
|
88
|
+
const variants = record[key];
|
|
89
|
+
if (Array.isArray(variants) && variants.some(isArrayTypedSchema)) return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Top-level output-schema property names declared as arrays (JTD `elements` →
|
|
96
|
+
* JSON `type: "array"`). An incremental yield section for such a label
|
|
97
|
+
* accumulates into a list even when the agent emits exactly one — otherwise a
|
|
98
|
+
* single `type: ["findings"]` yield would assemble as a bare object and fail
|
|
99
|
+
* array-typed schema validation.
|
|
100
|
+
*/
|
|
101
|
+
export function arrayValuedLabels(outputSchema: unknown): ReadonlySet<string> {
|
|
102
|
+
const labels = new Set<string>();
|
|
103
|
+
// Use the JTD-converted JSON Schema (matches what validation runs against):
|
|
104
|
+
// JTD `optionalProperties.findings.elements` becomes `properties.findings`
|
|
105
|
+
// with `type: "array"`, which raw `normalizeSchema` would not expose.
|
|
106
|
+
const { jsonSchema } = buildOutputValidator(outputSchema);
|
|
107
|
+
if (jsonSchema === undefined) return labels;
|
|
108
|
+
const properties = jsonSchema.properties;
|
|
109
|
+
if (properties === null || typeof properties !== "object") return labels;
|
|
110
|
+
const propRecord = properties as Record<string, unknown>;
|
|
111
|
+
for (const key in propRecord) {
|
|
112
|
+
if (isArrayTypedSchema(propRecord[key])) labels.add(key);
|
|
113
|
+
}
|
|
114
|
+
return labels;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Assemble typed yield calls into the final payload consumed by schema validation.
|
|
119
|
+
*
|
|
120
|
+
* A non-empty array `type` contributes an incremental section and never decides
|
|
121
|
+
* termination by itself. A string `type` with omitted `data` makes the last
|
|
122
|
+
* assistant turn the raw terminal result. Other string-typed yields contribute
|
|
123
|
+
* the terminal labelled section. Untyped terminal yields keep the historical
|
|
124
|
+
* "last yield wins" behavior unless no terminal yield exists, in which case
|
|
125
|
+
* accumulated typed sections finalize on idle.
|
|
126
|
+
*/
|
|
127
|
+
export function assembleYieldResult(
|
|
128
|
+
yieldItems: YieldItem[],
|
|
129
|
+
lastAssistantText?: string,
|
|
130
|
+
arrayLabels?: ReadonlySet<string>,
|
|
131
|
+
): AssembledYieldResult | undefined {
|
|
132
|
+
if (yieldItems.length === 0) return undefined;
|
|
133
|
+
let terminalItem: YieldItem | undefined;
|
|
134
|
+
for (let index = yieldItems.length - 1; index >= 0; index--) {
|
|
135
|
+
const item = yieldItems[index];
|
|
136
|
+
if (!item) continue;
|
|
137
|
+
if (!isIncrementalYieldType(item.type)) {
|
|
138
|
+
terminalItem = item;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
let hasTypedSections = false;
|
|
143
|
+
for (const item of yieldItems) {
|
|
144
|
+
if (getYieldLabels(item.type).length > 0) {
|
|
145
|
+
hasTypedSections = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (terminalItem && typeof terminalItem.type === "string" && terminalItem.data === undefined) {
|
|
150
|
+
const resolved = resolveYieldPayload(terminalItem, lastAssistantText, getYieldLabels(terminalItem.type));
|
|
151
|
+
return {
|
|
152
|
+
data: resolved.value,
|
|
153
|
+
schemaOverridden: terminalItem.schemaOverridden === true,
|
|
154
|
+
rawText: resolved.fromLastAssistantText && typeof resolved.value === "string",
|
|
155
|
+
missingData: resolved.missingData,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (!hasTypedSections && terminalItem) {
|
|
159
|
+
const resolved = resolveYieldPayload(terminalItem, lastAssistantText, []);
|
|
160
|
+
return {
|
|
161
|
+
data: resolved.value,
|
|
162
|
+
schemaOverridden: terminalItem.schemaOverridden === true,
|
|
163
|
+
rawText: resolved.fromLastAssistantText && typeof resolved.value === "string",
|
|
164
|
+
missingData: resolved.missingData,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const sections: Record<string, unknown> = {};
|
|
169
|
+
const sectionCounts = new Map<string, number>();
|
|
170
|
+
let schemaOverridden = false;
|
|
171
|
+
let missingData = false;
|
|
172
|
+
let hasSections = false;
|
|
173
|
+
|
|
174
|
+
for (const item of yieldItems) {
|
|
175
|
+
if (item.status === "aborted") continue;
|
|
176
|
+
schemaOverridden ||= item.schemaOverridden === true;
|
|
177
|
+
const labels = getYieldLabels(item.type);
|
|
178
|
+
if (labels.length === 0) continue;
|
|
179
|
+
const resolved = resolveYieldPayload(item, lastAssistantText, labels);
|
|
180
|
+
missingData ||= resolved.missingData;
|
|
181
|
+
const incremental = isIncrementalYieldType(item.type);
|
|
182
|
+
for (const label of labels) {
|
|
183
|
+
appendYieldSection(
|
|
184
|
+
sections,
|
|
185
|
+
sectionCounts,
|
|
186
|
+
label,
|
|
187
|
+
resolved.value,
|
|
188
|
+
incremental && (arrayLabels?.has(label) ?? false),
|
|
189
|
+
);
|
|
190
|
+
hasSections = true;
|
|
191
|
+
}
|
|
192
|
+
if (!isIncrementalYieldType(item.type)) break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (hasSections) {
|
|
196
|
+
return { data: sections, schemaOverridden, rawText: false, missingData };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!terminalItem) return undefined;
|
|
200
|
+
const resolved = resolveYieldPayload(terminalItem, lastAssistantText, []);
|
|
201
|
+
return {
|
|
202
|
+
data: resolved.value,
|
|
203
|
+
schemaOverridden: terminalItem.schemaOverridden === true,
|
|
204
|
+
rawText: resolved.fromLastAssistantText && typeof resolved.value === "string",
|
|
205
|
+
missingData: resolved.missingData,
|
|
206
|
+
};
|
|
207
|
+
}
|
package/src/tiny/text.ts
CHANGED
|
@@ -43,6 +43,29 @@ export function formatTitleUserMessage(message: string): string {
|
|
|
43
43
|
return `<user-message>\n${prepareTitleInput(message)}\n</user-message>`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** Single recent conversation turn supplied to title refresh after replanning. */
|
|
47
|
+
export interface TitleConversationTurn {
|
|
48
|
+
role: "user" | "assistant";
|
|
49
|
+
text?: string;
|
|
50
|
+
thinking?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Format recent user/assistant context for title generation after a todo replan. */
|
|
54
|
+
export function formatTitleConversationContext(turns: readonly TitleConversationTurn[]): string {
|
|
55
|
+
const formattedTurns: string[] = [];
|
|
56
|
+
for (const turn of turns) {
|
|
57
|
+
const sections: string[] = [];
|
|
58
|
+
const text = turn.text?.trim();
|
|
59
|
+
if (text) sections.push(text);
|
|
60
|
+
const thinking = turn.role === "assistant" ? turn.thinking?.trim() : undefined;
|
|
61
|
+
if (thinking) sections.push(`<thinking>\n${thinking}\n</thinking>`);
|
|
62
|
+
if (sections.length === 0) continue;
|
|
63
|
+
formattedTurns.push(`<${turn.role}>\n${sections.join("\n\n")}\n</${turn.role}>`);
|
|
64
|
+
}
|
|
65
|
+
if (formattedTurns.length === 0) return "";
|
|
66
|
+
return prepareTitleInput(`<conversation>\n${formattedTurns.join("\n\n")}\n</conversation>`);
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
/**
|
|
47
70
|
* Greeting / acknowledgement / filler tokens. A first user message composed
|
|
48
71
|
* entirely of these (or of bare numbers / punctuation / emoji) carries no
|