@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.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 +49 -0
- package/dist/cli.js +3624 -3568
- 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/collab/guest.d.ts +29 -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/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 +30 -1
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- 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 +6 -4
- 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/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- 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/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 +15 -6
- 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 +144 -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 +23 -2
- package/src/modes/controllers/event-controller.ts +106 -0
- 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 +65 -5
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/ui-helpers.ts +41 -23
- package/src/prompts/agents/reviewer.md +11 -10
- 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 +867 -428
- 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 +113 -19
- 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 +16 -3
- 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/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
|
package/src/tools/ask.ts
CHANGED
|
@@ -136,6 +136,45 @@ function stripRecommendedSuffix(label: string): string {
|
|
|
136
136
|
return label.endsWith(RECOMMENDED_SUFFIX) ? label.slice(0, -RECOMMENDED_SUFFIX.length) : label;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
interface CustomInputContext {
|
|
140
|
+
selectionMarker: "radio" | "checkbox";
|
|
141
|
+
checkedIndices?: readonly number[];
|
|
142
|
+
markableCount: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getSelectOptionDescription(option: ExtensionUISelectItem): string | undefined {
|
|
146
|
+
return typeof option === "string" ? undefined : option.description;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatCustomInputTitle(
|
|
150
|
+
question: string,
|
|
151
|
+
options: ExtensionUISelectItem[],
|
|
152
|
+
context: CustomInputContext,
|
|
153
|
+
): string {
|
|
154
|
+
const selectedIndex = options.findIndex(option => getSelectOptionLabel(option) === OTHER_OPTION);
|
|
155
|
+
const lines = [question, ""];
|
|
156
|
+
const checked = new Set(context.checkedIndices ?? []);
|
|
157
|
+
for (let index = 0; index < options.length; index++) {
|
|
158
|
+
const option = options[index]!;
|
|
159
|
+
const label = getSelectOptionLabel(option);
|
|
160
|
+
const isSelected = index === selectedIndex;
|
|
161
|
+
const isMarkable = index < context.markableCount;
|
|
162
|
+
const prefix =
|
|
163
|
+
context.selectionMarker === "radio" && (isMarkable || isSelected)
|
|
164
|
+
? `${isSelected ? theme.radio.selected : theme.radio.unselected} `
|
|
165
|
+
: context.selectionMarker === "checkbox" && isMarkable
|
|
166
|
+
? `${checked.has(index) ? theme.checkbox.checked : theme.checkbox.unchecked} `
|
|
167
|
+
: isSelected
|
|
168
|
+
? `${theme.nav.cursor} `
|
|
169
|
+
: " ";
|
|
170
|
+
lines.push(prefix + label);
|
|
171
|
+
const description = getSelectOptionDescription(option);
|
|
172
|
+
if (description) lines.push(` ${description}`);
|
|
173
|
+
}
|
|
174
|
+
lines.push("", "Enter your response:");
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
139
178
|
// =============================================================================
|
|
140
179
|
// Question Selection Logic
|
|
141
180
|
// =============================================================================
|
|
@@ -250,9 +289,14 @@ async function askSingleQuestion(
|
|
|
250
289
|
return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
|
|
251
290
|
};
|
|
252
291
|
|
|
253
|
-
const promptForCustomInput = async (
|
|
292
|
+
const promptForCustomInput = async (
|
|
293
|
+
title: string,
|
|
294
|
+
optionsToShow: ExtensionUISelectItem[],
|
|
295
|
+
context: CustomInputContext,
|
|
296
|
+
): Promise<{ input: string | undefined }> => {
|
|
254
297
|
const dialogOptions = signal ? { signal } : undefined;
|
|
255
|
-
const
|
|
298
|
+
const editorTitle = formatCustomInputTitle(title, optionsToShow, context);
|
|
299
|
+
const showCustomInput = () => ui.editor(editorTitle, undefined, dialogOptions, { promptStyle: true });
|
|
256
300
|
const input = signal ? await untilAborted(signal, showCustomInput) : await showCustomInput();
|
|
257
301
|
return { input };
|
|
258
302
|
};
|
|
@@ -306,7 +350,11 @@ async function askSingleQuestion(
|
|
|
306
350
|
timedOut = true;
|
|
307
351
|
break;
|
|
308
352
|
}
|
|
309
|
-
const customResult = await promptForCustomInput(
|
|
353
|
+
const customResult = await promptForCustomInput(`${prefix}${promptWithProgress}`, opts, {
|
|
354
|
+
selectionMarker: "checkbox",
|
|
355
|
+
checkedIndices,
|
|
356
|
+
markableCount: questionOptions.length,
|
|
357
|
+
});
|
|
310
358
|
if (customResult.input === undefined) {
|
|
311
359
|
continue;
|
|
312
360
|
}
|
|
@@ -372,7 +420,10 @@ async function askSingleQuestion(
|
|
|
372
420
|
if (selectTimedOut) {
|
|
373
421
|
break;
|
|
374
422
|
}
|
|
375
|
-
const customResult = await promptForCustomInput(
|
|
423
|
+
const customResult = await promptForCustomInput(promptWithProgress, optionsWithNavigation, {
|
|
424
|
+
selectionMarker: "radio",
|
|
425
|
+
markableCount: displayOptions.length,
|
|
426
|
+
});
|
|
376
427
|
if (customResult.input === undefined) {
|
|
377
428
|
continue;
|
|
378
429
|
}
|
package/src/tools/renderers.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
9
9
|
import { goalToolRenderer } from "../goals/tools/goal-tool";
|
|
10
10
|
import { lspToolRenderer } from "../lsp/render";
|
|
11
11
|
import type { Theme } from "../modes/theme/theme";
|
|
12
|
-
import { taskToolRenderer } from "../task/
|
|
12
|
+
import { taskToolRenderer } from "../task/renderer";
|
|
13
13
|
import { webSearchToolRenderer } from "../web/search/render";
|
|
14
14
|
import { askToolRenderer } from "./ask";
|
|
15
15
|
import { astEditToolRenderer } from "./ast-edit";
|
|
@@ -88,7 +88,13 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
88
88
|
reflect: reflectToolRenderer as ToolRenderer,
|
|
89
89
|
search_tool_bm25: searchToolBm25Renderer as ToolRenderer,
|
|
90
90
|
ssh: sshToolRenderer as ToolRenderer,
|
|
91
|
-
|
|
91
|
+
// Lazy getter: `taskToolRenderer` lives in a module that closes an import
|
|
92
|
+
// cycle back here (task/renderer → task/render → … → tools/renderers), so
|
|
93
|
+
// reading it at init order-dependently hits its temporal dead zone. Deferring
|
|
94
|
+
// the read to first access (render time) sidesteps the cycle entirely.
|
|
95
|
+
get task(): ToolRenderer {
|
|
96
|
+
return taskToolRenderer as ToolRenderer;
|
|
97
|
+
},
|
|
92
98
|
todo: todoToolRenderer as ToolRenderer,
|
|
93
99
|
github: githubToolRenderer as ToolRenderer,
|
|
94
100
|
goal: goalToolRenderer as ToolRenderer,
|