@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli.js +3682 -3615
  3. package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
  4. package/dist/types/advisor/advise-tool.d.ts +8 -4
  5. package/dist/types/advisor/config.d.ts +88 -0
  6. package/dist/types/advisor/index.d.ts +1 -0
  7. package/dist/types/advisor/transcript-recorder.d.ts +13 -2
  8. package/dist/types/advisor/watchdog.d.ts +20 -0
  9. package/dist/types/cli/update-cli.d.ts +15 -0
  10. package/dist/types/collab/guest.d.ts +29 -0
  11. package/dist/types/collab/replication-shrink.d.ts +39 -0
  12. package/dist/types/config/provider-globals.d.ts +7 -0
  13. package/dist/types/config/settings-schema.d.ts +81 -0
  14. package/dist/types/debug/log-viewer.d.ts +1 -0
  15. package/dist/types/debug/raw-sse.d.ts +1 -0
  16. package/dist/types/edit/hashline/diff.d.ts +0 -11
  17. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  18. package/dist/types/extensibility/utils.d.ts +12 -0
  19. package/dist/types/mcp/transports/index.d.ts +1 -0
  20. package/dist/types/mcp/transports/sse.d.ts +20 -0
  21. package/dist/types/memories/index.d.ts +20 -1
  22. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  23. package/dist/types/modes/components/index.d.ts +1 -0
  24. package/dist/types/modes/components/model-selector.d.ts +9 -1
  25. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  26. package/dist/types/modes/components/status-line/component.d.ts +35 -1
  27. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  28. package/dist/types/modes/controllers/command-controller.d.ts +3 -3
  29. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  30. package/dist/types/modes/interactive-mode.d.ts +11 -4
  31. package/dist/types/modes/skill-command.d.ts +32 -0
  32. package/dist/types/modes/types.d.ts +9 -3
  33. package/dist/types/session/agent-session.d.ts +58 -10
  34. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  35. package/dist/types/session/messages.d.ts +26 -0
  36. package/dist/types/session/messages.test.d.ts +1 -0
  37. package/dist/types/session/session-entries.d.ts +31 -3
  38. package/dist/types/session/session-history-format.d.ts +6 -0
  39. package/dist/types/session/session-loader.d.ts +9 -1
  40. package/dist/types/session/session-manager.d.ts +8 -7
  41. package/dist/types/session/session-storage.d.ts +11 -0
  42. package/dist/types/session/session-title-slot.d.ts +19 -0
  43. package/dist/types/ssh/connection-manager.d.ts +47 -0
  44. package/dist/types/ssh/utils.d.ts +16 -0
  45. package/dist/types/task/executor.d.ts +3 -16
  46. package/dist/types/task/render.d.ts +0 -5
  47. package/dist/types/task/renderer.d.ts +13 -0
  48. package/dist/types/task/types.d.ts +16 -0
  49. package/dist/types/task/yield-assembly.d.ts +28 -0
  50. package/dist/types/tiny/text.d.ts +8 -0
  51. package/dist/types/tools/render-utils.d.ts +2 -0
  52. package/dist/types/tools/review.d.ts +6 -4
  53. package/dist/types/tools/ssh.d.ts +1 -1
  54. package/dist/types/tools/todo.d.ts +6 -0
  55. package/dist/types/tools/yield.d.ts +8 -3
  56. package/dist/types/utils/thinking-display.d.ts +4 -0
  57. package/package.json +12 -12
  58. package/src/advisor/__tests__/advisor.test.ts +242 -10
  59. package/src/advisor/__tests__/config.test.ts +173 -0
  60. package/src/advisor/advise-tool.ts +11 -6
  61. package/src/advisor/config.ts +256 -0
  62. package/src/advisor/index.ts +1 -0
  63. package/src/advisor/runtime.ts +12 -2
  64. package/src/advisor/transcript-recorder.ts +25 -2
  65. package/src/advisor/watchdog.ts +57 -31
  66. package/src/autolearn/controller.ts +13 -22
  67. package/src/autoresearch/index.ts +7 -2
  68. package/src/cli/gc-cli.ts +17 -10
  69. package/src/cli/update-cli.ts +254 -0
  70. package/src/collab/guest.ts +43 -7
  71. package/src/collab/host.ts +13 -8
  72. package/src/collab/replication-shrink.ts +111 -0
  73. package/src/config/model-registry.ts +80 -18
  74. package/src/config/provider-globals.ts +25 -0
  75. package/src/config/settings-schema.ts +77 -0
  76. package/src/debug/index.ts +32 -7
  77. package/src/debug/log-viewer.ts +111 -53
  78. package/src/debug/raw-sse.ts +68 -48
  79. package/src/discovery/codex.ts +13 -5
  80. package/src/edit/hashline/diff.ts +57 -4
  81. package/src/eval/__tests__/julia-prelude.test.ts +2 -2
  82. package/src/eval/js/shared/local-module-loader.ts +23 -1
  83. package/src/export/html/template.js +13 -7
  84. package/src/extensibility/extensions/loader.ts +5 -3
  85. package/src/extensibility/extensions/wrapper.ts +9 -3
  86. package/src/extensibility/hooks/loader.ts +3 -3
  87. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  88. package/src/extensibility/plugins/manager.ts +2 -1
  89. package/src/extensibility/tool-event-input.ts +23 -0
  90. package/src/extensibility/utils.ts +74 -0
  91. package/src/internal-urls/docs-index.generated.txt +1 -1
  92. package/src/mcp/client.ts +3 -1
  93. package/src/mcp/manager.ts +12 -5
  94. package/src/mcp/transports/index.ts +1 -0
  95. package/src/mcp/transports/sse.ts +377 -0
  96. package/src/memories/index.ts +130 -15
  97. package/src/memory-backend/local-backend.ts +5 -3
  98. package/src/modes/components/advisor-config.ts +555 -0
  99. package/src/modes/components/advisor-message.ts +9 -2
  100. package/src/modes/components/agent-hub.ts +9 -4
  101. package/src/modes/components/index.ts +2 -0
  102. package/src/modes/components/model-selector.ts +79 -48
  103. package/src/modes/components/settings-selector.ts +1 -0
  104. package/src/modes/components/status-line/component.ts +150 -5
  105. package/src/modes/components/status-line/segments.ts +46 -21
  106. package/src/modes/components/status-line/types.ts +13 -1
  107. package/src/modes/components/tool-execution.ts +47 -6
  108. package/src/modes/controllers/command-controller.ts +27 -36
  109. package/src/modes/controllers/event-controller.ts +113 -1
  110. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  111. package/src/modes/controllers/input-controller.ts +61 -61
  112. package/src/modes/controllers/selector-controller.ts +100 -9
  113. package/src/modes/interactive-mode.ts +74 -25
  114. package/src/modes/skill-command.ts +116 -0
  115. package/src/modes/types.ts +9 -3
  116. package/src/modes/utils/ui-helpers.ts +41 -23
  117. package/src/prompts/agents/reviewer.md +11 -10
  118. package/src/prompts/goals/goal-todo-context.md +12 -0
  119. package/src/prompts/review-custom-request.md +1 -2
  120. package/src/prompts/review-request.md +1 -2
  121. package/src/prompts/system/interrupted-thinking.md +7 -0
  122. package/src/prompts/system/recap-user.md +9 -0
  123. package/src/prompts/system/subagent-system-prompt.md +8 -5
  124. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  125. package/src/prompts/system/system-prompt.md +0 -1
  126. package/src/prompts/tools/irc.md +2 -2
  127. package/src/prompts/tools/read.md +2 -2
  128. package/src/sdk.ts +28 -24
  129. package/src/session/agent-session.ts +899 -429
  130. package/src/session/indexed-session-storage.ts +86 -13
  131. package/src/session/messages.test.ts +125 -0
  132. package/src/session/messages.ts +172 -9
  133. package/src/session/redis-session-storage.ts +49 -2
  134. package/src/session/session-entries.ts +39 -2
  135. package/src/session/session-history-format.ts +29 -2
  136. package/src/session/session-listing.ts +54 -24
  137. package/src/session/session-loader.ts +66 -3
  138. package/src/session/session-manager.ts +115 -22
  139. package/src/session/session-persistence.ts +95 -1
  140. package/src/session/session-storage.ts +36 -0
  141. package/src/session/session-title-slot.ts +141 -0
  142. package/src/session/sql-session-storage.ts +71 -11
  143. package/src/slash-commands/builtin-registry.ts +23 -24
  144. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  145. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  146. package/src/ssh/connection-manager.ts +139 -12
  147. package/src/ssh/file-transfer.ts +23 -18
  148. package/src/ssh/ssh-executor.ts +2 -13
  149. package/src/ssh/utils.ts +19 -0
  150. package/src/task/executor.ts +21 -23
  151. package/src/task/render.ts +162 -20
  152. package/src/task/renderer.ts +14 -0
  153. package/src/task/types.ts +17 -0
  154. package/src/task/yield-assembly.ts +207 -0
  155. package/src/tiny/text.ts +23 -0
  156. package/src/tools/ask.ts +55 -4
  157. package/src/tools/render-utils.ts +2 -0
  158. package/src/tools/renderers.ts +8 -2
  159. package/src/tools/review.ts +17 -7
  160. package/src/tools/ssh.ts +8 -4
  161. package/src/tools/todo.ts +17 -1
  162. package/src/tools/yield.ts +140 -31
  163. package/src/utils/thinking-display.ts +15 -0
  164. package/src/prompts/system/autolearn-nudge.md +0 -5
@@ -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 submitData = lastYield?.data;
604
- if (submitData === null || submitData === undefined) {
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 overridden = lastYield?.schemaOverridden === true;
609
- const completeData = normalizeCompleteData(submitData, reportFindings, validator);
598
+ const completeData = assembled.rawText
599
+ ? assembled.data
600
+ : normalizeCompleteData(assembled.data, reportFindings, validator);
610
601
  const result =
611
- schemaError || overridden
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 = JSON.stringify(completeData, null, 2) ?? "null";
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 = overridden
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();
@@ -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): Array<{ data: unknown }> {
157
- if (Array.isArray(value)) {
158
- return value.filter((item): item is { data: unknown } => item !== null && typeof item === "object");
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
- if (value !== null && typeof value === "object") {
161
- return [value as { data: unknown }];
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, check for review verdict from yield tool
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 (yield with review schema + report_finding)
1072
- // Check for review result (yield with review schema + report_finding).
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 tool's data field if it matches SubmitReviewDetails
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 [toolName, dataArray] of Object.entries(result.extractedToolData)) {
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 === "yield" || toolName === "report_finding") continue;
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