@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.
Files changed (150) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/cli.js +3624 -3568
  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/collab/guest.d.ts +29 -0
  10. package/dist/types/config/settings-schema.d.ts +81 -0
  11. package/dist/types/debug/log-viewer.d.ts +1 -0
  12. package/dist/types/debug/raw-sse.d.ts +1 -0
  13. package/dist/types/edit/hashline/diff.d.ts +0 -11
  14. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  15. package/dist/types/extensibility/utils.d.ts +12 -0
  16. package/dist/types/mcp/transports/index.d.ts +1 -0
  17. package/dist/types/mcp/transports/sse.d.ts +20 -0
  18. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  19. package/dist/types/modes/components/index.d.ts +1 -0
  20. package/dist/types/modes/components/model-selector.d.ts +9 -1
  21. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  22. package/dist/types/modes/components/status-line/component.d.ts +30 -1
  23. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  24. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  25. package/dist/types/modes/interactive-mode.d.ts +10 -4
  26. package/dist/types/modes/skill-command.d.ts +32 -0
  27. package/dist/types/modes/types.d.ts +7 -2
  28. package/dist/types/session/agent-session.d.ts +58 -10
  29. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  30. package/dist/types/session/messages.d.ts +26 -0
  31. package/dist/types/session/messages.test.d.ts +1 -0
  32. package/dist/types/session/session-entries.d.ts +31 -3
  33. package/dist/types/session/session-history-format.d.ts +6 -0
  34. package/dist/types/session/session-loader.d.ts +9 -1
  35. package/dist/types/session/session-manager.d.ts +6 -4
  36. package/dist/types/session/session-storage.d.ts +11 -0
  37. package/dist/types/session/session-title-slot.d.ts +19 -0
  38. package/dist/types/ssh/connection-manager.d.ts +47 -0
  39. package/dist/types/ssh/utils.d.ts +16 -0
  40. package/dist/types/task/executor.d.ts +3 -16
  41. package/dist/types/task/render.d.ts +0 -5
  42. package/dist/types/task/renderer.d.ts +13 -0
  43. package/dist/types/task/types.d.ts +16 -0
  44. package/dist/types/task/yield-assembly.d.ts +28 -0
  45. package/dist/types/tiny/text.d.ts +8 -0
  46. package/dist/types/tools/render-utils.d.ts +2 -0
  47. package/dist/types/tools/review.d.ts +6 -4
  48. package/dist/types/tools/ssh.d.ts +1 -1
  49. package/dist/types/tools/todo.d.ts +6 -0
  50. package/dist/types/tools/yield.d.ts +8 -3
  51. package/dist/types/utils/thinking-display.d.ts +4 -0
  52. package/package.json +12 -12
  53. package/src/advisor/__tests__/advisor.test.ts +242 -10
  54. package/src/advisor/__tests__/config.test.ts +173 -0
  55. package/src/advisor/advise-tool.ts +11 -6
  56. package/src/advisor/config.ts +256 -0
  57. package/src/advisor/index.ts +1 -0
  58. package/src/advisor/runtime.ts +12 -2
  59. package/src/advisor/transcript-recorder.ts +25 -2
  60. package/src/advisor/watchdog.ts +57 -31
  61. package/src/autoresearch/index.ts +7 -2
  62. package/src/cli/gc-cli.ts +17 -10
  63. package/src/collab/guest.ts +43 -7
  64. package/src/config/model-registry.ts +80 -18
  65. package/src/config/settings-schema.ts +77 -0
  66. package/src/debug/index.ts +32 -7
  67. package/src/debug/log-viewer.ts +111 -53
  68. package/src/debug/raw-sse.ts +68 -48
  69. package/src/discovery/codex.ts +13 -5
  70. package/src/edit/hashline/diff.ts +57 -4
  71. package/src/eval/js/shared/local-module-loader.ts +23 -1
  72. package/src/export/html/template.js +13 -7
  73. package/src/extensibility/extensions/loader.ts +5 -3
  74. package/src/extensibility/extensions/wrapper.ts +9 -3
  75. package/src/extensibility/hooks/loader.ts +3 -3
  76. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  77. package/src/extensibility/plugins/manager.ts +2 -1
  78. package/src/extensibility/tool-event-input.ts +23 -0
  79. package/src/extensibility/utils.ts +74 -0
  80. package/src/internal-urls/docs-index.generated.txt +1 -1
  81. package/src/mcp/client.ts +3 -1
  82. package/src/mcp/manager.ts +12 -5
  83. package/src/mcp/transports/index.ts +1 -0
  84. package/src/mcp/transports/sse.ts +377 -0
  85. package/src/memories/index.ts +15 -6
  86. package/src/modes/components/advisor-config.ts +555 -0
  87. package/src/modes/components/advisor-message.ts +9 -2
  88. package/src/modes/components/agent-hub.ts +9 -4
  89. package/src/modes/components/index.ts +2 -0
  90. package/src/modes/components/model-selector.ts +79 -48
  91. package/src/modes/components/settings-selector.ts +1 -0
  92. package/src/modes/components/status-line/component.ts +144 -5
  93. package/src/modes/components/status-line/segments.ts +46 -21
  94. package/src/modes/components/status-line/types.ts +13 -1
  95. package/src/modes/components/tool-execution.ts +47 -6
  96. package/src/modes/controllers/command-controller.ts +23 -2
  97. package/src/modes/controllers/event-controller.ts +106 -0
  98. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  99. package/src/modes/controllers/input-controller.ts +61 -61
  100. package/src/modes/controllers/selector-controller.ts +100 -9
  101. package/src/modes/interactive-mode.ts +65 -5
  102. package/src/modes/skill-command.ts +116 -0
  103. package/src/modes/types.ts +7 -2
  104. package/src/modes/utils/ui-helpers.ts +41 -23
  105. package/src/prompts/agents/reviewer.md +11 -10
  106. package/src/prompts/review-custom-request.md +1 -2
  107. package/src/prompts/review-request.md +1 -2
  108. package/src/prompts/system/interrupted-thinking.md +7 -0
  109. package/src/prompts/system/recap-user.md +9 -0
  110. package/src/prompts/system/subagent-system-prompt.md +8 -5
  111. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  112. package/src/prompts/system/system-prompt.md +0 -1
  113. package/src/prompts/tools/irc.md +2 -2
  114. package/src/prompts/tools/read.md +2 -2
  115. package/src/sdk.ts +28 -24
  116. package/src/session/agent-session.ts +867 -428
  117. package/src/session/indexed-session-storage.ts +86 -13
  118. package/src/session/messages.test.ts +125 -0
  119. package/src/session/messages.ts +172 -9
  120. package/src/session/redis-session-storage.ts +49 -2
  121. package/src/session/session-entries.ts +39 -2
  122. package/src/session/session-history-format.ts +29 -2
  123. package/src/session/session-listing.ts +54 -24
  124. package/src/session/session-loader.ts +66 -3
  125. package/src/session/session-manager.ts +113 -19
  126. package/src/session/session-persistence.ts +95 -1
  127. package/src/session/session-storage.ts +36 -0
  128. package/src/session/session-title-slot.ts +141 -0
  129. package/src/session/sql-session-storage.ts +71 -11
  130. package/src/slash-commands/builtin-registry.ts +16 -3
  131. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  132. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  133. package/src/ssh/connection-manager.ts +139 -12
  134. package/src/ssh/file-transfer.ts +23 -18
  135. package/src/ssh/ssh-executor.ts +2 -13
  136. package/src/ssh/utils.ts +19 -0
  137. package/src/task/executor.ts +21 -23
  138. package/src/task/render.ts +162 -20
  139. package/src/task/renderer.ts +14 -0
  140. package/src/task/types.ts +17 -0
  141. package/src/task/yield-assembly.ts +207 -0
  142. package/src/tiny/text.ts +23 -0
  143. package/src/tools/ask.ts +55 -4
  144. package/src/tools/render-utils.ts +2 -0
  145. package/src/tools/renderers.ts +8 -2
  146. package/src/tools/review.ts +17 -7
  147. package/src/tools/ssh.ts +8 -4
  148. package/src/tools/todo.ts +17 -1
  149. package/src/tools/yield.ts +140 -31
  150. package/src/utils/thinking-display.ts +15 -0
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Review tools - report_finding for structured code review.
2
+ * Legacy hidden review-finding tool for agents that have not migrated to
3
+ * incremental `yield` sections.
3
4
  *
4
- * Used by the reviewer agent to report findings in a structured way.
5
- * Hidden by default - only enabled when explicitly listed in agent's tools.
6
- * Reviewers finish via `yield` tool with SubmitReviewDetails schema.
5
+ * Hidden by default - only enabled when explicitly listed in an agent's tools.
6
+ * Reviewers now finish via incremental `yield`; this tool remains for
7
+ * compatibility with older or custom review agents.
7
8
  */
8
9
  // ─────────────────────────────────────────────────────────────────────────────
9
10
 
@@ -33,6 +34,10 @@ const PRIORITY_INFO: Record<FindingPriority, FindingPriorityInfo> = {
33
34
 
34
35
  export const PRIORITY_LABELS: FindingPriority[] = ["P0", "P1", "P2", "P3"];
35
36
 
37
+ export function isFindingPriority(value: unknown): value is FindingPriority {
38
+ return value === "P0" || value === "P1" || value === "P2" || value === "P3";
39
+ }
40
+
36
41
  export function getPriorityInfo(priority: FindingPriority): FindingPriorityInfo {
37
42
  return PRIORITY_INFO[priority] ?? { ord: 3, symbol: "status.info", color: "muted" };
38
43
  }
@@ -72,8 +77,13 @@ interface ReportFindingDetails {
72
77
  line_end: number;
73
78
  }
74
79
 
75
- function isFindingPriority(value: unknown): value is FindingPriority {
76
- return value === "P0" || value === "P1" || value === "P2" || value === "P3";
80
+ function normalizeFindingPriority(value: unknown): FindingPriority | undefined {
81
+ if (isFindingPriority(value)) return value;
82
+ if (value === 0) return "P0";
83
+ if (value === 1) return "P1";
84
+ if (value === 2) return "P2";
85
+ if (value === 3) return "P3";
86
+ return undefined;
77
87
  }
78
88
 
79
89
  export function parseReportFindingDetails(value: unknown): ReportFindingDetails | undefined {
@@ -81,7 +91,7 @@ export function parseReportFindingDetails(value: unknown): ReportFindingDetails
81
91
 
82
92
  const title = typeof value.title === "string" ? value.title : undefined;
83
93
  const body = typeof value.body === "string" ? value.body : undefined;
84
- const priority = isFindingPriority(value.priority) ? value.priority : undefined;
94
+ const priority = normalizeFindingPriority(value.priority);
85
95
  const confidence =
86
96
  typeof value.confidence === "number" &&
87
97
  Number.isFinite(value.confidence) &&
package/src/tools/ssh.ts CHANGED
@@ -369,10 +369,14 @@ export const sshToolRenderer = {
369
369
  });
370
370
  },
371
371
  mergeCallAndResult: true,
372
- // Collapsed pending preview caps the command to a viewport-sized tail window
373
- // that shifts while args stream. Expanded output is top-anchored enough for
374
- // the transcript to commit its settled prefix.
375
- provisionalPendingPreview: "collapsed",
372
+ // Pending call preview can re-anchor wholesale when the final result inserts
373
+ // the `Output` section, so no pending SSH rows may commit to native
374
+ // scrollback even when expanded. The expanded pending shape was previously
375
+ // allowed to commit, which left two visible shapes in native scrollback once
376
+ // the result settled: a stale `⏳ SSH: [host]` header above the final frame,
377
+ // and the pending `╰──╯` footer reused in-place as the new `├── Output ──┤`
378
+ // separator with a fresh footer pushed below it.
379
+ provisionalPendingPreview: true,
376
380
  // Partial-result chrome (pending icon and frame state) differs from the
377
381
  // final SSH glyph/state, so the block stays commit-unstable while
378
382
  // `options.isPartial` holds. Without this, a long-running SSH command's
package/src/tools/todo.ts CHANGED
@@ -19,6 +19,8 @@ import { formatErrorDetail, PREVIEW_LIMITS } from "./render-utils";
19
19
  // =============================================================================
20
20
 
21
21
  export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
22
+ /** Operation names accepted by the todo tool and echoed in successful result details. */
23
+ export type TodoOperation = "init" | "start" | "done" | "rm" | "drop" | "append" | "view";
22
24
 
23
25
  export interface TodoItem {
24
26
  content: string;
@@ -36,6 +38,8 @@ export interface TodoCompletionTransition {
36
38
  }
37
39
 
38
40
  export interface TodoToolDetails {
41
+ /** Operation that produced this snapshot; absent on legacy transcript entries. */
42
+ op?: TodoOperation;
39
43
  phases: TodoPhase[];
40
44
  storage: "session" | "memory";
41
45
  completedTasks?: TodoCompletionTransition[];
@@ -134,6 +138,18 @@ function normalizeInProgressTask(phases: TodoPhase[]): void {
134
138
  if (firstPendingTask) firstPendingTask.status = "in_progress";
135
139
  }
136
140
 
141
+ /** Return the active todo task, preferring an in-progress item over the first pending item. */
142
+ export function nextActionableTask(phases: readonly TodoPhase[]): TodoItem | undefined {
143
+ let firstPending: TodoItem | undefined;
144
+ for (const phase of phases) {
145
+ for (const task of phase.tasks) {
146
+ if (task.status === "in_progress") return task;
147
+ if (!firstPending && task.status === "pending") firstPending = task;
148
+ }
149
+ }
150
+ return firstPending;
151
+ }
152
+
137
153
  export const USER_TODO_EDIT_CUSTOM_TYPE = "user_todo_edit";
138
154
 
139
155
  export function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPhase[] {
@@ -626,7 +642,7 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
626
642
  const completedTasks = readOnly || failed ? [] : getCompletionTransitions(previousPhases, updated);
627
643
  if (!readOnly && !failed) this.session.setTodoPhases?.(updated);
628
644
  const storage = this.session.getSessionFile() ? "session" : "memory";
629
- const details: TodoToolDetails = { phases: effective, storage };
645
+ const details: TodoToolDetails = { op: params.op, phases: effective, storage };
630
646
  if (completedTasks.length > 0) details.completedTasks = completedTasks;
631
647
 
632
648
  return {
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Submit result tool for structured subagent output.
2
+ * Result submission tool for subagent output.
3
3
  *
4
- * Subagents must call this tool to finish and return structured JSON output.
4
+ * Subagents can call this tool incrementally or terminally depending on `type`.
5
5
  */
6
6
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
7
  import type { TSchema } from "@oh-my-pi/pi-ai/types";
@@ -17,9 +17,14 @@ import type { ToolSession } from ".";
17
17
  import { buildOutputValidator, formatAllValidationIssues } from "./output-schema-validator";
18
18
 
19
19
  export interface YieldDetails {
20
- data: unknown;
20
+ /** Successful result payload, or omitted when `useLastTurn` requests last-turn extraction. */
21
+ data?: unknown;
21
22
  status: "success" | "aborted";
22
23
  error?: string;
24
+ /** Optional result section/classification supplied by the yield caller. */
25
+ type?: string | string[];
26
+ /** True when the caller intentionally omitted success data so the executor uses the last assistant turn. */
27
+ useLastTurn?: boolean;
23
28
  /**
24
29
  * Set when the yield tool exhausted its in-tool schema-retry budget
25
30
  * (MAX_SCHEMA_RETRIES) and accepted the data anyway. Surfaced so the
@@ -66,30 +71,108 @@ function hasUnresolvedRefs(schema: unknown): boolean {
66
71
  return false;
67
72
  }
68
73
 
74
+ const yieldTypeSchema: Record<string, unknown> = {
75
+ anyOf: [
76
+ { type: "string" },
77
+ {
78
+ type: "array",
79
+ minItems: 1,
80
+ items: { type: "string" },
81
+ },
82
+ ],
83
+ description: "Optional result type. A non-empty string array is incremental; a string is terminal.",
84
+ };
85
+
86
+ function isYieldType(value: unknown): value is string | string[] {
87
+ return (
88
+ typeof value === "string" ||
89
+ (Array.isArray(value) && value.length > 0 && value.every(item => typeof item === "string"))
90
+ );
91
+ }
92
+
93
+ function parseYieldType(value: unknown): string | string[] | undefined {
94
+ // Strict-mode providers (OpenAI/Codex) make the optional `type` property
95
+ // required+nullable, so an untyped final yield arrives as `type: null`.
96
+ if (value === undefined || value === null) return undefined;
97
+ if (isYieldType(value)) return value;
98
+ throw new Error("type must be a string or non-empty array of strings");
99
+ }
100
+
101
+ /**
102
+ * Expand a plain-object `data` schema into a strict union that ALSO accepts each
103
+ * top-level section value (and array element) on its own. Agents that yield
104
+ * incrementally (`type: ["findings"]`, `type: ["confidence"]`, …) submit one
105
+ * section per call, so `data` is a single finding object or a lone verdict value
106
+ * — never the full output object. Without this, strict-mode providers constrain
107
+ * `data` to the whole schema and reject/—under constrained decoding—forbid the
108
+ * partial. Every branch is a typed sub-schema, so strict representability holds;
109
+ * the full-output object stays the first (terminal) branch. The assembled whole
110
+ * is still validated against the full schema at finalization. Non-object / loose
111
+ * schemas are returned unchanged.
112
+ */
113
+ function withSectionVariants(dataSchema: Record<string, unknown>): Record<string, unknown> {
114
+ if (dataSchema.type !== "object") return dataSchema;
115
+ const props = dataSchema.properties;
116
+ if (props === null || typeof props !== "object") return dataSchema;
117
+ const propRecord = props as Record<string, unknown>;
118
+ const { description, ...fullWithoutDescription } = dataSchema;
119
+ const branches: unknown[] = [];
120
+ const seen = new Set<string>();
121
+ const add = (schema: unknown): void => {
122
+ if (schema === null || typeof schema !== "object") return;
123
+ const key = JSON.stringify(schema);
124
+ if (seen.has(key)) return;
125
+ seen.add(key);
126
+ branches.push(schema);
127
+ };
128
+ add(fullWithoutDescription);
129
+ for (const name in propRecord) {
130
+ const prop = propRecord[name];
131
+ add(prop);
132
+ if (prop !== null && typeof prop === "object") {
133
+ const propObj = prop as Record<string, unknown>;
134
+ if (propObj.type === "array") add(propObj.items);
135
+ }
136
+ }
137
+ if (branches.length <= 1) return dataSchema;
138
+ return description !== undefined ? { description, anyOf: branches } : { anyOf: branches };
139
+ }
140
+
69
141
  function wrapYieldParameters(dataSchema: Record<string, unknown>): Record<string, unknown> {
142
+ const successResultSchema = {
143
+ type: "object",
144
+ additionalProperties: false,
145
+ description: "task succeeded",
146
+ properties: { data: dataSchema },
147
+ required: ["data"],
148
+ };
149
+ const errorResultSchema = {
150
+ type: "object",
151
+ additionalProperties: false,
152
+ properties: {
153
+ error: { type: "string", description: "error message" },
154
+ },
155
+ required: ["error"],
156
+ };
157
+ const lastTurnResultSchema = {
158
+ type: "object",
159
+ additionalProperties: false,
160
+ description: "typed task succeeded; data omitted so the last assistant turn is used",
161
+ properties: {},
162
+ required: [],
163
+ };
164
+ // The "an empty `result` (last-turn) requires a `type`" invariant is enforced
165
+ // in `execute()` at runtime, NOT in this schema: a top-level combinator
166
+ // (`allOf`/`anyOf`/`oneOf`/...) makes OpenAI/Codex Responses reject the whole
167
+ // tool with `invalid_function_parameters`, so the wrapper stays a plain object.
70
168
  return {
71
169
  type: "object",
72
170
  additionalProperties: false,
73
171
  description: "submit data or error",
74
172
  properties: {
173
+ type: yieldTypeSchema,
75
174
  result: {
76
- anyOf: [
77
- {
78
- type: "object",
79
- additionalProperties: false,
80
- description: "task succeeded",
81
- properties: { data: dataSchema },
82
- required: ["data"],
83
- },
84
- {
85
- type: "object",
86
- additionalProperties: false,
87
- properties: {
88
- error: { type: "string", description: "error message" },
89
- },
90
- required: ["error"],
91
- },
92
- ],
175
+ anyOf: [successResultSchema, errorResultSchema, lastTurnResultSchema],
93
176
  },
94
177
  },
95
178
  required: ["result"],
@@ -110,9 +193,9 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
110
193
  readonly approval = "read" as const;
111
194
  readonly label = "Submit Result";
112
195
  readonly description =
113
- "Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
114
- 'Pass `result: { data: <your output> }` for success, or `result: { error: "message" }` for failure.\n' +
115
- "The `data`/`error` wrapper is required do not put your output directly in `result`.";
196
+ "Submit subagent output. Omit `type` for the usual final structured result.\n\n" +
197
+ 'Pass `type: ["section"]` to submit an incremental, non-terminal section that accumulates. Pass `type: "result"` to finalize; when `data` is omitted, your last assistant turn becomes the raw final result.\n' +
198
+ 'Use `result: { data: <your output> }` for success, or `result: { error: "message" }` for failure. Keep the `result` wrapper.';
116
199
  readonly parameters: TSchema;
117
200
  strict = true;
118
201
  readonly intent = "omit" as const;
@@ -163,7 +246,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
163
246
  if (hasUnresolvedRefs(resolved)) {
164
247
  throw new Error("schema contains unresolved $ref after dereferencing");
165
248
  }
166
- dataSchema = resolved;
249
+ dataSchema = withSectionVariants(resolved);
167
250
  } else {
168
251
  this.strict = false;
169
252
  dataSchema = looseRecordSchema(
@@ -198,15 +281,21 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
198
281
  if (!rawResult || typeof rawResult !== "object" || Array.isArray(rawResult)) {
199
282
  throw new Error("result must be an object containing either data or error");
200
283
  }
201
-
202
284
  const resultRecord = rawResult as Record<string, unknown>;
203
285
  const errorMessage = typeof resultRecord.error === "string" ? resultRecord.error : undefined;
204
286
  const data = resultRecord.data;
287
+ const yieldType = parseYieldType(raw.type);
288
+ const useLastTurn =
289
+ errorMessage === undefined && data === undefined && yieldType !== undefined && !("error" in resultRecord);
290
+ // Incremental array-typed sections carry partial data (one finding, one
291
+ // field) that cannot satisfy the full output schema; the assembled result
292
+ // is validated as a whole at finalization (executor finalizeSubprocessOutput).
293
+ const isIncremental = Array.isArray(yieldType) && yieldType.length > 0;
205
294
 
206
295
  if (errorMessage !== undefined && data !== undefined) {
207
296
  throw new Error("result cannot contain both data and error");
208
297
  }
209
- if (errorMessage === undefined && data === undefined) {
298
+ if (errorMessage === undefined && data === undefined && yieldType === undefined) {
210
299
  throw new Error(
211
300
  'result must contain either `data` or `error`. Use `{result: {data: <your output>}}` for success or `{result: {error: "message"}}` for failure.',
212
301
  );
@@ -214,11 +303,11 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
214
303
 
215
304
  const status = errorMessage !== undefined ? "aborted" : "success";
216
305
  let schemaValidationOverridden = false;
217
- if (status === "success") {
218
- if (data === undefined || data === null) {
306
+ if (status === "success" && !useLastTurn) {
307
+ if (data === null) {
219
308
  throw new Error("data is required when yield indicates success");
220
309
  }
221
- if (this.#validate) {
310
+ if (this.#validate && !isIncremental) {
222
311
  const parsed = this.#validate(data);
223
312
  if (!parsed.success) {
224
313
  this.#schemaValidationFailures++;
@@ -245,7 +334,14 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
245
334
  : "Result submitted.";
246
335
  return {
247
336
  content: [{ type: "text", text: responseText }],
248
- details: { data, status, error: errorMessage, schemaOverridden: schemaValidationOverridden || undefined },
337
+ details: {
338
+ data,
339
+ status,
340
+ error: errorMessage,
341
+ type: yieldType,
342
+ useLastTurn: useLastTurn || undefined,
343
+ schemaOverridden: schemaValidationOverridden || undefined,
344
+ },
249
345
  };
250
346
  }
251
347
  }
@@ -262,8 +358,21 @@ subprocessToolRegistry.register<YieldDetails>("yield", {
262
358
  data: record.data,
263
359
  status,
264
360
  error: typeof record.error === "string" ? record.error : undefined,
361
+ type: isYieldType(record.type) ? record.type : undefined,
362
+ useLastTurn: record.useLastTurn === true ? true : undefined,
265
363
  schemaOverridden: record.schemaOverridden === true ? true : undefined,
266
364
  };
267
365
  },
268
- shouldTerminate: event => !event.isError,
366
+ shouldTerminate: event => {
367
+ if (event.isError) return false;
368
+ const details = event.result?.details;
369
+ if (!details || typeof details !== "object") return true;
370
+ const record = details as Record<string, unknown>;
371
+ return !(
372
+ record.status === "success" &&
373
+ Array.isArray(record.type) &&
374
+ record.type.length > 0 &&
375
+ record.type.every(item => typeof item === "string")
376
+ );
377
+ },
269
378
  });
@@ -1,3 +1,5 @@
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+
1
3
  export function canonicalizeMessage(text: string | null | undefined): string {
2
4
  if (!text) return "";
3
5
  const trimmed = text.trim();
@@ -79,6 +81,7 @@ export function formatThinkingForDisplay(text: string, proseOnly: boolean): stri
79
81
  return formatted;
80
82
  }
81
83
 
84
+ /** Whether a formatted thinking block has non-placeholder content worth rendering. */
82
85
  export function hasDisplayableThinking(
83
86
  text: string | null | undefined,
84
87
  formattedText: string | null | undefined,
@@ -87,3 +90,15 @@ export function hasDisplayableThinking(
87
90
  if (!formattedText) return false;
88
91
  return formattedText.length > 0 && canonicalizeMessage(text).length > 0;
89
92
  }
93
+
94
+ /** Whether an assistant message contains thinking content the TUI can reveal. */
95
+ export function messageHasDisplayableThinking(message: AgentMessage, proseOnly: boolean): boolean {
96
+ if (message.role !== "assistant") return false;
97
+ for (const content of message.content) {
98
+ if (content.type !== "thinking") continue;
99
+ if (hasDisplayableThinking(content.thinking, formatThinkingForDisplay(content.thinking, proseOnly))) {
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }