@timurproko/a1 0.1.8-dev.332 → 0.1.8-dev.368

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 (110) hide show
  1. package/README.md +40 -11
  2. package/dist/composition/owned-ui.d.ts +2 -0
  3. package/dist/composition/owned-ui.js +20 -3
  4. package/dist/contracts/owned-ui/index.d.ts +2 -0
  5. package/dist/contracts/owned-ui/index.js +2 -0
  6. package/dist/contracts/owned-ui/model.d.ts +38 -3
  7. package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
  8. package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
  9. package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
  10. package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
  11. package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
  12. package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
  13. package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
  14. package/dist/contracts/owned-ui/validation.js +45 -2
  15. package/dist/contracts/presentation/index.d.ts +8 -0
  16. package/dist/features/owned-ui/settings-app.js +1 -1
  17. package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
  18. package/dist/features/prompt-history/image-sidecar.js +130 -0
  19. package/dist/features/prompt-history/index.d.ts +2 -0
  20. package/dist/features/prompt-history/index.js +1 -0
  21. package/dist/features/prompt-history/paths.d.ts +1 -0
  22. package/dist/features/prompt-history/paths.js +2 -1
  23. package/dist/features/prompt-history/store.d.ts +1 -1
  24. package/dist/features/prompt-history/store.js +46 -2
  25. package/dist/features/prompt-history/worker.js +1 -1
  26. package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
  27. package/dist/features/prompt-suggestions/diagnostics.js +82 -0
  28. package/dist/features/prompt-suggestions/index.d.ts +1 -0
  29. package/dist/features/prompt-suggestions/index.js +1 -0
  30. package/dist/foundation/release/bootstrap.d.ts +2 -0
  31. package/dist/foundation/release/bootstrap.js +54 -18
  32. package/dist/foundation/release/index.d.ts +1 -0
  33. package/dist/foundation/release/index.js +1 -0
  34. package/dist/foundation/release/update-launch.d.ts +6 -0
  35. package/dist/foundation/release/update-launch.js +17 -0
  36. package/dist/foundation/release/update.d.ts +2 -0
  37. package/dist/foundation/release/update.js +20 -21
  38. package/dist/foundation/supervision/main.js +5 -2
  39. package/dist/foundation/supervision/server.js +40 -13
  40. package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
  41. package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
  42. package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
  43. package/dist/integrations/pi/components/prompt-input-port.js +1 -0
  44. package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
  45. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  46. package/dist/integrations/pi/components/shell-footer-status.js +9 -5
  47. package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
  48. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
  49. package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
  51. package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
  52. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
  53. package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
  54. package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
  55. package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
  56. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
  57. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
  60. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  61. package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
  62. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  63. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  64. package/dist/integrations/pi/engine/adapter.js +181 -84
  65. package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
  66. package/dist/integrations/pi/engine/tool-rendering.js +122 -0
  67. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
  68. package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
  69. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
  70. package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
  71. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
  72. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
  73. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
  74. package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
  75. package/dist/integrations/pi/session-ui/session-shell.js +51 -31
  76. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
  77. package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
  78. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  79. package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
  80. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
  81. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
  82. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  83. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
  84. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
  85. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
  86. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/linux-x64/manifest.json +1 -1
  89. package/dist/native/win32-x64/manifest.json +2 -2
  90. package/dist/native/win32-x64/process-guardian.exe +0 -0
  91. package/dist/product-identity.d.ts +1 -1
  92. package/dist/product-identity.js +1 -1
  93. package/dist/product-identity.json +1 -0
  94. package/dist/ui/components/index.d.ts +1 -0
  95. package/dist/ui/components/index.js +1 -0
  96. package/dist/ui/components/line-input.d.ts +2 -3
  97. package/dist/ui/components/line-input.js +4 -20
  98. package/dist/ui/components/mouse.d.ts +1 -1
  99. package/dist/ui/components/mouse.js +4 -3
  100. package/dist/ui/components/prompt-input.d.ts +32 -0
  101. package/dist/ui/components/prompt-input.js +54 -0
  102. package/docs/architecture/boundaries.md +1 -0
  103. package/docs/architecture/history-editor-provenance.md +5 -1
  104. package/docs/architecture/project-structure.md +1 -0
  105. package/docs/architecture/prompt-suggestions.md +68 -0
  106. package/docs/architecture/toolchain.md +1 -1
  107. package/docs/architecture/ui-reference-provenance.md +23 -0
  108. package/docs/ci-release-runbook.md +45 -7
  109. package/docs/features/modal-content-interaction.md +37 -0
  110. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Adapted from @earendil-works/pi-coding-agent 0.84.2
3
3
  * packages/coding-agent/src/modes/interactive/components/footer.ts (MIT).
4
- * Modifications: consumes neutral owned-UI view data rather than a concrete AgentSession.
4
+ * Modifications: consumes neutral owned-UI view data rather than a concrete AgentSession;
5
+ * bare A1 colors the effective level-name span while the comparison profile retains pinned styling.
5
6
  */
6
7
  import { isAbsolute, relative, resolve, sep } from "node:path";
7
8
  import { truncateToWidth, visibleWidth } from "#pi-tui";
@@ -9,9 +10,11 @@ import { piTheme } from "../../theme.js";
9
10
  export class SessionFooter {
10
11
  getView;
11
12
  cwd;
12
- constructor(getView, cwd) {
13
+ profile;
14
+ constructor(getView, cwd, profile = "pi") {
13
15
  this.getView = getView;
14
16
  this.cwd = cwd;
17
+ this.profile = profile;
15
18
  }
16
19
  invalidate() { }
17
20
  dispose() { }
@@ -60,12 +63,14 @@ export class SessionFooter {
60
63
  leftWidth = visibleWidth(left);
61
64
  }
62
65
  const modelName = view.activeModel?.modelId ?? "no-model";
63
- const rightWithoutProvider = view.activeModel === null || view.thinkingLevel === "off"
64
- ? modelName
65
- : `${modelName} • ${view.thinkingLevel}`;
66
+ const rightWithoutProvider = this.profile === "a1"
67
+ ? view.activeModel === null ? theme.fg("dim", modelName)
68
+ : theme.fg("dim", `${modelName} • `) + theme.getThinkingBorderColor(view.thinkingLevel)(view.thinkingLevel)
69
+ : view.activeModel === null || view.thinkingLevel === "off" ? modelName : `${modelName} • ${view.thinkingLevel}`;
66
70
  let right = rightWithoutProvider;
67
71
  if ((view.status.footer?.availableProviderCount ?? 1) > 1 && view.activeModel) {
68
- right = `(${view.activeModel.providerId}) ${rightWithoutProvider}`;
72
+ const provider = `(${view.activeModel.providerId}) `;
73
+ right = (this.profile === "a1" ? theme.fg("dim", provider) : provider) + rightWithoutProvider;
69
74
  if (leftWidth + 2 + visibleWidth(right) > width)
70
75
  right = rightWithoutProvider;
71
76
  }
@@ -85,7 +90,7 @@ export class SessionFooter {
85
90
  }
86
91
  const lines = [
87
92
  truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")),
88
- theme.fg("dim", left) + theme.fg("dim", line.slice(left.length)),
93
+ theme.fg("dim", left) + (this.profile === "a1" ? line.slice(left.length) : theme.fg("dim", line.slice(left.length))),
89
94
  ];
90
95
  const statuses = view.status.footer?.extensionStatuses ?? [];
91
96
  if (statuses.length > 0) {
@@ -500,7 +500,7 @@ export class HistoryEditorCore {
500
500
  // Render top border (with scroll indicator if scrolled down)
501
501
  if (this.persistentHistory && this.historyIndex >= 0) {
502
502
  const overflow = this.scrollOffset > 0 ? ` · ↑ ${this.scrollOffset} more` : "";
503
- const label = `─── History ${this.history.length - this.historyIndex}/${this.history.length}${overflow} `;
503
+ const label = `─── ${this.history.length - this.historyIndex}/${this.history.length}${overflow} `;
504
504
  const shown = truncateToWidth(label, width);
505
505
  const remaining = Math.max(0, width - visibleWidth(shown));
506
506
  result.push(this.borderColor(shown.slice(0, 4))
@@ -1,6 +1,6 @@
1
1
  import type { PiSessionForkPrompt, PiSessionSelection } from "./session-selection.js";
2
2
  import { type AgentSession, type AgentSessionRuntime, type AgentSessionServices, type SessionInfo } from "@earendil-works/pi-coding-agent";
3
- import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, type OwnedUiCommand, type OwnedUiCommandOutcome, type OwnedUiImageAttachment, type OwnedUiPromptSuggestionGeneratorPort, type OwnedUiPromptSuggestionRequest, type OwnedUiPromptSuggestionResult, type OwnedUiEvent, type OwnedUiSessionViewModel, type OwnedUiSnapshot } from "../../../contracts/owned-ui/index.js";
3
+ import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, type OwnedUiCommand, type OwnedUiCommandOutcome, type OwnedUiImageAttachment, type OwnedUiPromptSuggestionGeneratorPort, type OwnedUiPromptSuggestionReasoning, type OwnedUiPromptSuggestionRequest, type OwnedUiPromptSuggestionResult, type OwnedUiEvent, type OwnedUiSessionViewModel, type OwnedUiSnapshot } from "../../../contracts/owned-ui/index.js";
4
4
  import { type PiAuthenticationProviderOption, type PiBashWorkflowResult, type PiPinnedSettingsCallback, type PiPinnedSettingsSnapshot, type PiWorkflowAutocompleteCommand, type PiWorkflowHost, type PiWorkflowInteractionHost, type PiWorkflowOption, type PiWorkflowRequest, type PiWorkflowResult } from "./workflows.js";
5
5
  import { PiSettingsIntegration } from "./settings-integration.js";
6
6
  import type { PiSettingOwnerHandlers } from "./settings-effects.js";
@@ -145,6 +145,7 @@ export declare class PiEngineAdapter implements OwnedUiPromptSuggestionGenerator
145
145
  currentSessionFile(): string | null;
146
146
  currentSessionResumeMetadata(): PiSessionResumeMetadata | null;
147
147
  get disposed(): boolean;
148
+ suggestionReasoningPolicy(): OwnedUiPromptSuggestionReasoning;
148
149
  generate(request: OwnedUiPromptSuggestionRequest): Promise<OwnedUiPromptSuggestionResult>;
149
150
  start(): Promise<OwnedUiSessionViewModel>;
150
151
  onEvent(listener: (event: OwnedUiEvent) => void): () => void;
@@ -8,8 +8,9 @@ import { promisify } from "node:util";
8
8
  import { PRODUCT_IDENTITY } from "../../../product-identity.js";
9
9
  import { configureOwnedHttpDispatcher } from "./http-dispatcher.js";
10
10
  import { readPinnedCommandChangelog } from "./changelog.js";
11
+ import { toolRenderingInput } from "./tool-rendering.js";
11
12
  import { copyToClipboard, CredentialSynchronizationError, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, VERSION, } from "@earendil-works/pi-coding-agent";
12
- import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiPromptSuggestionRequest, assertOwnedUiPromptSuggestionResult, assertOwnedUiSnapshot, normalizePromptSuggestionCandidate, } from "../../../contracts/owned-ui/index.js";
13
+ import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, acceptsTranscriptUpdate, transcriptToolState, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiPromptSuggestionRequest, assertOwnedUiPromptSuggestionResult, assertOwnedUiSnapshot, normalizePromptSuggestionCandidate, } from "../../../contracts/owned-ui/index.js";
13
14
  import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
14
15
  import { createPiRuntimeIntegration } from "./runtime-integration.js";
15
16
  import { PiSessionCommandIntegration } from "./session-integration.js";
@@ -216,6 +217,17 @@ export class PiEngineAdapter {
216
217
  get disposed() {
217
218
  return this.#disposed;
218
219
  }
220
+ suggestionReasoningPolicy() {
221
+ const session = this.#session;
222
+ if (!session?.model)
223
+ return "unavailable";
224
+ if (!session.model.reasoning)
225
+ return "ordinary";
226
+ const levels = session.getAvailableThinkingLevels?.() ?? [];
227
+ // Compatibility: capability order, not the user's current setting or a provider-name heuristic.
228
+ return ["off", "minimal", "low", "medium", "high", "xhigh", "max"]
229
+ .find(level => levels.includes(level)) ?? "unavailable";
230
+ }
219
231
  async generate(request) {
220
232
  assertOwnedUiPromptSuggestionRequest(request);
221
233
  const identity = request.identity;
@@ -230,52 +242,50 @@ export class PiEngineAdapter {
230
242
  || activeModel === null
231
243
  || identity.model.providerId !== activeModel.providerId
232
244
  || identity.model.modelId !== activeModel.modelId) {
233
- return { identity, text: null };
245
+ return { identity, outcome: request.signal.aborted ? "cancelled" : "unavailable", text: null };
234
246
  }
235
247
  const model = session.model;
236
248
  const agentState = session.agent.state;
237
- if (model === undefined || typeof runtime.services.modelRuntime.completeSimple !== "function") {
238
- return { identity, text: null };
249
+ const policy = this.suggestionReasoningPolicy();
250
+ if (model === undefined || policy === "unavailable" || typeof runtime.services.modelRuntime.completeSimple !== "function") {
251
+ return { identity, outcome: "unavailable", text: null };
239
252
  }
240
253
  const messages = agentState.messages.filter(message => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
241
- const reasoning = session.thinkingLevel === "minimal"
242
- || session.thinkingLevel === "low"
243
- || session.thinkingLevel === "medium"
244
- || session.thinkingLevel === "high"
245
- || session.thinkingLevel === "xhigh"
246
- || session.thinkingLevel === "max"
247
- ? session.thinkingLevel
248
- : undefined;
249
- const response = await runtime.services.modelRuntime.completeSimple(model, {
250
- systemPrompt: agentState.systemPrompt,
251
- messages: [
252
- ...messages,
253
- { role: "user", content: CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, timestamp: Date.now() },
254
- ],
255
- tools: agentState.tools,
256
- }, {
257
- signal: request.signal,
258
- ...(reasoning === undefined ? {} : { reasoning }),
259
- });
260
- if (isRecord(response)
261
- && (stringValue(response.errorMessage) !== undefined
262
- || stringValue(response.stopReason) === "error"
263
- || stringValue(response.stopReason) === "aborted")) {
264
- const result = { identity, text: null };
265
- assertOwnedUiPromptSuggestionResult(result);
266
- return result;
254
+ const reasoning = policy === "ordinary" || policy === "off" ? undefined : policy;
255
+ let response;
256
+ try {
257
+ response = await runtime.services.modelRuntime.completeSimple(model, {
258
+ systemPrompt: agentState.systemPrompt,
259
+ messages: [
260
+ ...messages,
261
+ { role: "user", content: CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, timestamp: Date.now() },
262
+ ],
263
+ tools: agentState.tools,
264
+ }, {
265
+ signal: request.signal,
266
+ ...(reasoning === undefined ? {} : { reasoning }),
267
+ });
268
+ }
269
+ catch {
270
+ return { identity, outcome: request.signal.aborted ? "cancelled" : "provider-failure", text: null };
271
+ }
272
+ if (request.signal.aborted || (isRecord(response) && response.stopReason === "aborted")) {
273
+ return { identity, outcome: "cancelled", text: null };
274
+ }
275
+ if (!isRecord(response) || stringValue(response.errorMessage) !== undefined || response.stopReason === "error") {
276
+ return { identity, outcome: "provider-failure", text: null };
267
277
  }
268
278
  const content = Array.isArray(response.content) ? response.content : [];
269
- if (content.some(block => isRecord(block) && block.type === "toolCall")) {
270
- const result = { identity, text: null };
271
- assertOwnedUiPromptSuggestionResult(result);
272
- return result;
273
- }
274
- const textBlock = content.find(block => isRecord(block) && block.type === "text" && typeof block.text === "string");
275
- const result = {
276
- identity,
277
- text: normalizePromptSuggestionCandidate(isRecord(textBlock) && typeof textBlock.text === "string" ? textBlock.text : null),
278
- };
279
+ if (response.stopReason === "length" || content.some(block => isRecord(block) && block.type === "toolCall")) {
280
+ return { identity, outcome: "rejected", text: null };
281
+ }
282
+ // Security: validate all text, not just the first block of a multi-part response.
283
+ const rawText = content.filter(block => isRecord(block) && block.type === "text")
284
+ .map(block => stringValue(block.text) ?? "").join("\n");
285
+ const text = normalizePromptSuggestionCandidate(rawText);
286
+ const result = text === null
287
+ ? { identity, outcome: rawText.trim() ? "rejected" : "empty", text: null }
288
+ : { identity, outcome: "candidate", text };
279
289
  assertOwnedUiPromptSuggestionResult(result);
280
290
  return result;
281
291
  }
@@ -1989,6 +1999,8 @@ export class PiEngineAdapter {
1989
1999
  this.#reconcileActiveModelAvailability();
1990
2000
  this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
1991
2001
  this.#transcriptImageAssets.clear();
2002
+ // Invariant: a new binding cannot inherit arguments/results from a reused invocation id.
2003
+ this.#setTranscript([]);
1992
2004
  this.#rebuildTranscript(session.messages, "finalized");
1993
2005
  const generation = this.#sessionGeneration;
1994
2006
  this.#unsubscribe = session.subscribe(event => {
@@ -2119,6 +2131,7 @@ export class PiEngineAdapter {
2119
2131
  }
2120
2132
  case "message_end":
2121
2133
  this.#upsertMessageBlock(event.message, "finalized");
2134
+ this.#settleFailedDeclarations(event.message);
2122
2135
  // Compatibility: preserve the same semantic boundary v2 counted. Transcript block
2123
2136
  // finalization is intentionally not a substitute: rebuilds, retries,
2124
2137
  // thinking parts, and tool rows can all finalize independently.
@@ -2164,13 +2177,18 @@ export class PiEngineAdapter {
2164
2177
  case "agent_end": {
2165
2178
  if (event.type === "agent_end" && event.willRetry === true)
2166
2179
  return;
2167
- const finalMessages = Array.isArray(event.messages) && event.messages.length > 0
2168
- ? event.messages
2169
- : this.#session?.messages ?? [];
2170
- if (finalMessages.length > 0)
2180
+ // Protocol: agent_end is run-local; only settlement reads the complete session scope.
2181
+ const finalMessages = event.type === "agent_settled"
2182
+ ? this.#session?.messages ?? []
2183
+ : Array.isArray(event.messages) ? event.messages : [];
2184
+ if (event.type === "agent_end")
2185
+ this.#mergeRunTranscript(finalMessages);
2186
+ else if (finalMessages.length > 0)
2171
2187
  this.#rebuildTranscript(finalMessages, "finalized");
2172
- else
2173
- this.#setTranscript(this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized", revision: block.revision + 1 } : block));
2188
+ // Invariant: missing final messages must not erase accumulated content or invent tool outcomes.
2189
+ if (finalMessages.length === 0)
2190
+ this.#setTranscript(this.#transcript.map(block => block.status === "live" && transcriptToolState(block) === undefined
2191
+ ? { ...block, status: "finalized", revision: block.revision + 1 } : block));
2174
2192
  // Compatibility: ending a turn leaves the working state, as the recorded pinned baseline does, but
2175
2193
  // it leaves only that state: a compaction or retry being shown outlives the turn
2176
2194
  // that ended under it. Settlement ends the run, and with it every state — the
@@ -2286,6 +2304,29 @@ export class PiEngineAdapter {
2286
2304
  autoCompactEnabled: this.#runtime?.services.settingsManager?.getCompactionEnabled?.() ?? true,
2287
2305
  };
2288
2306
  }
2307
+ // Invariant: run-local completion may restate messages, but never owns transcript membership.
2308
+ #mergeRunTranscript(messages) {
2309
+ const positions = new Map();
2310
+ const sessionOccurrences = new Map();
2311
+ for (const [index, message] of (this.#session?.messages ?? []).entries()) {
2312
+ const key = messageFallbackKey(message, index);
2313
+ const occurrence = sessionOccurrences.get(key) ?? 0;
2314
+ sessionOccurrences.set(key, occurrence + 1);
2315
+ positions.set(message, { index, occurrence });
2316
+ }
2317
+ const runOccurrences = new Map();
2318
+ for (const [index, message] of messages.entries()) {
2319
+ const key = messageFallbackKey(message, index);
2320
+ const occurrence = runOccurrences.get(key) ?? 0;
2321
+ runOccurrences.set(key, occurrence + 1);
2322
+ const position = positions.get(message) ?? { index, occurrence };
2323
+ for (const block of this.#messageBlocks(message, "finalized", position.index, position.occurrence)) {
2324
+ this.#upsertTranscriptBlock(block);
2325
+ }
2326
+ this.#settleFailedDeclarations(message);
2327
+ }
2328
+ }
2329
+ // Invariant: full replacement is reserved for session-authoritative scope (bind/settlement).
2289
2330
  #rebuildTranscript(messages, status) {
2290
2331
  const blocks = [];
2291
2332
  const blockIndexes = new Map();
@@ -2294,7 +2335,7 @@ export class PiEngineAdapter {
2294
2335
  const key = messageFallbackKey(message, index);
2295
2336
  const occurrence = occurrences.get(key) ?? 0;
2296
2337
  occurrences.set(key, occurrence + 1);
2297
- for (const block of this.#messageBlocks(message, status, index, occurrence)) {
2338
+ for (const block of this.#messageBlocks(message, status, index, occurrence, id => blocks[blockIndexes.get(id) ?? -1] ?? this.#transcriptBlock(id))) {
2298
2339
  const existingIndex = blockIndexes.get(block.id);
2299
2340
  if (existingIndex === undefined) {
2300
2341
  blockIndexes.set(block.id, blocks.length);
@@ -2318,7 +2359,8 @@ export class PiEngineAdapter {
2318
2359
  // it — otherwise every turn that ends re-renders the whole session.
2319
2360
  this.#setTranscript(blocks.map(block => {
2320
2361
  const existing = this.#transcriptBlock(block.id);
2321
- return existing !== undefined && sameBlockContent(existing, block) ? existing : block;
2362
+ const next = existing === undefined ? block : retainCompletedArguments(existing, block);
2363
+ return existing !== undefined && sameBlockContent(existing, next) ? existing : next;
2322
2364
  }));
2323
2365
  }
2324
2366
  #upsertMessageBlock(message, status) {
@@ -2328,7 +2370,7 @@ export class PiEngineAdapter {
2328
2370
  this.#upsertTranscriptBlock(block);
2329
2371
  return first;
2330
2372
  }
2331
- #messageBlocks(message, status, fallbackIndex, occurrence) {
2373
+ #messageBlocks(message, status, fallbackIndex, occurrence, currentBlock = id => this.#transcriptBlock(id)) {
2332
2374
  if (!isRecord(message) || typeof message.role !== "string")
2333
2375
  return [];
2334
2376
  const baseId = this.#messageBlockId(message, fallbackIndex, status, occurrence);
@@ -2409,25 +2451,24 @@ export class PiEngineAdapter {
2409
2451
  : this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
2410
2452
  if (toolCallId !== undefined)
2411
2453
  this.#toolBlockIds.set(toolCallId, blockId);
2412
- const existing = this.#transcriptBlock(blockId);
2454
+ const existing = currentBlock(blockId);
2413
2455
  const existingPayload = isRecord(existing?.payload) ? existing.payload : undefined;
2456
+ const payload = {
2457
+ role: "toolResult", toolCallId: toolCallId ?? null,
2458
+ toolName: stringValue(message.toolName) ?? stringValue(existingPayload?.toolName) ?? "unknown",
2459
+ argsComplete: true, partialResult: status === "live", isError: message.isError === true,
2460
+ };
2461
+ const rendering = toolRenderingInput({ args: undefined, previousArgs: existing?.toolRendering,
2462
+ result: message, payload, image: part => this.#imageReferences([part], "tool-result")[0] });
2414
2463
  return [{
2415
2464
  id: blockId,
2416
2465
  kind: "tool-result",
2417
2466
  status,
2467
+ toolState: { argsComplete: true, execution: status === "live" ? "running" : message.isError === true ? "failed" : "succeeded" },
2418
2468
  revision: this.#nextBlockRevision(blockId),
2419
2469
  title: stringValue(message.toolName) ?? existing?.title ?? "Tool result",
2420
- text: textFromContent(message.content),
2421
- imageReferences: this.#imageReferences(message.content, "tool-result"),
2422
- payload: {
2423
- ...(existingPayload ?? {}),
2424
- role: "toolResult",
2425
- toolCallId: toolCallId ?? null,
2426
- toolName: stringValue(message.toolName) ?? stringValue(existingPayload?.toolName) ?? "unknown",
2427
- argsComplete: true,
2428
- isError: message.isError === true,
2429
- details: jsonSummary(message.details),
2430
- },
2470
+ ...rendering,
2471
+ payload,
2431
2472
  }];
2432
2473
  }
2433
2474
  if (message.role !== "assistant" || !Array.isArray(message.content))
@@ -2457,23 +2498,61 @@ export class PiEngineAdapter {
2457
2498
  const toolCallId = stringValue(item.id) ?? `${baseId}:${blocks.length}`;
2458
2499
  const blockId = this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
2459
2500
  this.#toolBlockIds.set(toolCallId, blockId);
2501
+ const failure = status === "finalized" ? this.#declarationFailure(message) : undefined;
2502
+ const payload = { toolCallId, toolName: stringValue(item.name) ?? "unknown",
2503
+ argsComplete: failure === undefined && status === "finalized",
2504
+ ...(failure === undefined ? {} : { isError: true }) };
2505
+ const rendering = toolRenderingInput({ args: item.arguments, payload,
2506
+ ...(failure === undefined ? {} : { result: { content: [{ type: "text", text: failure.text }] } }),
2507
+ image: () => undefined });
2460
2508
  blocks.push({
2461
2509
  id: blockId,
2462
- kind: "tool-call",
2463
- status,
2510
+ kind: failure === undefined ? "tool-call" : "tool-result",
2511
+ status: failure === undefined ? "live" : "finalized",
2512
+ toolState: {
2513
+ argsComplete: failure === undefined && status === "finalized",
2514
+ execution: failure?.execution ?? "pending",
2515
+ },
2464
2516
  revision: this.#nextBlockRevision(blockId),
2465
2517
  title: stringValue(item.name) ?? "Tool",
2466
- text: jsonSummary(item.arguments).summary,
2467
- payload: {
2468
- toolCallId,
2469
- toolName: stringValue(item.name) ?? "unknown",
2470
- arguments: jsonSummary(item.arguments),
2471
- argsComplete: status === "finalized",
2472
- },
2518
+ ...rendering,
2519
+ text: failure?.text ?? jsonSummary(item.arguments).summary,
2520
+ payload,
2473
2521
  });
2474
2522
  }
2475
2523
  return blocks;
2476
2524
  }
2525
+ #declarationFailure(message) {
2526
+ // Provenance: pinned interactive-mode message_end and history reconstruction settle pending tools.
2527
+ if (message.role !== "assistant")
2528
+ return undefined;
2529
+ if (message.stopReason === "aborted") {
2530
+ const attempts = this.#session?.retryAttempt ?? 0;
2531
+ return { execution: "aborted", text: attempts > 0
2532
+ ? `Aborted after ${attempts} retry attempt${attempts > 1 ? "s" : ""}` : "Operation aborted" };
2533
+ }
2534
+ return message.stopReason === "error"
2535
+ ? { execution: "failed", text: stringValue(message.errorMessage) || "Error" } : undefined;
2536
+ }
2537
+ #settleFailedDeclarations(message) {
2538
+ if (!isRecord(message))
2539
+ return;
2540
+ const failure = this.#declarationFailure(message);
2541
+ if (failure === undefined)
2542
+ return;
2543
+ for (const block of this.#transcript) {
2544
+ const state = transcriptToolState(block);
2545
+ if (state === undefined || state.execution !== "pending" && state.execution !== "running")
2546
+ continue;
2547
+ const payload = { ...(isRecord(block.payload) ? block.payload : {}), partialResult: false, isError: true };
2548
+ this.#upsertTranscriptBlock({
2549
+ ...block, kind: "tool-result", status: "finalized", revision: block.revision + 1,
2550
+ ...toolRenderingInput({ args: undefined, previousArgs: block.toolRendering, payload,
2551
+ result: { content: [{ type: "text", text: failure.text }] }, image: () => undefined }),
2552
+ toolState: { ...state, execution: failure.execution }, payload,
2553
+ });
2554
+ }
2555
+ }
2477
2556
  #imageReferences(content, source) {
2478
2557
  if (!Array.isArray(content))
2479
2558
  return [];
@@ -2499,34 +2578,37 @@ export class PiEngineAdapter {
2499
2578
  const blockId = this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
2500
2579
  this.#toolBlockIds.set(toolCallId, blockId);
2501
2580
  const ended = event.type === "tool_execution_end";
2581
+ const existing = this.#transcriptBlock(blockId);
2582
+ const state = existing === undefined ? undefined : transcriptToolState(existing);
2583
+ // Invariant: duplicate starts cannot blank accumulated output; late events cannot reopen a settled invocation.
2584
+ if (state !== undefined && (state.execution !== "pending" && state.execution !== "running"
2585
+ || event.type === "tool_execution_start" && state.execution === "running"))
2586
+ return;
2502
2587
  const source = ended ? event.result : event.partialResult;
2588
+ const payload = { toolCallId, toolName: stringValue(event.toolName) ?? "unknown",
2589
+ partialResult: event.type === "tool_execution_update", argsComplete: true, isError: event.isError === true };
2590
+ const rendering = toolRenderingInput({ args: event.args, previousArgs: existing?.toolRendering, result: source,
2591
+ payload, image: part => this.#imageReferences([part], "tool-result")[0] });
2503
2592
  this.#upsertTranscriptBlock({
2504
2593
  id: blockId,
2505
2594
  kind: ended ? "tool-result" : "tool-call",
2506
2595
  status: ended ? "finalized" : "live",
2596
+ toolState: { argsComplete: true, execution: ended ? event.isError === true ? "failed" : "succeeded" : "running" },
2507
2597
  revision: this.#nextBlockRevision(blockId),
2508
2598
  title: stringValue(event.toolName) ?? "Tool",
2509
- text: textFromContent(isRecord(source) ? source.content : source),
2510
- payload: {
2511
- toolCallId,
2512
- toolName: stringValue(event.toolName) ?? "unknown",
2513
- arguments: jsonSummary(event.args),
2514
- // Performance: a partial result repeats the whole accumulated output on every chunk;
2515
- // summarizing it each time would cost quadratic work over the stream.
2516
- result: ended ? jsonSummary(source) : { summary: "", json: null },
2517
- partialResult: event.type === "tool_execution_update",
2518
- argsComplete: ended,
2519
- isError: event.isError === true,
2520
- },
2599
+ ...rendering,
2600
+ payload,
2521
2601
  });
2522
2602
  }
2523
2603
  #upsertTranscriptBlock(block) {
2524
2604
  const index = this.#transcriptIndex.get(block.id);
2525
2605
  if (index !== undefined) {
2526
2606
  const existing = this.#transcript[index];
2527
- // Invariant: an obsolete partial cannot revive finalized content after a completion barrier.
2528
- if (existing?.status === "finalized" && block.status === "live")
2607
+ // Invariant: argument completion is not execution finality; stale phases still stay rejected.
2608
+ if (existing !== undefined && !acceptsTranscriptUpdate(existing, block))
2529
2609
  return;
2610
+ if (existing !== undefined)
2611
+ block = retainCompletedArguments(existing, block);
2530
2612
  // Performance: nothing is emitted for a block that repeats itself, and keeping the
2531
2613
  // revision keeps the rows it already rendered.
2532
2614
  if (existing !== undefined && sameBlockContent(existing, block))
@@ -2676,6 +2758,9 @@ export class PiEngineAdapter {
2676
2758
  this.#lifecycle = this.#disposed ? "stopped" : canResume ? "ready" : "failed";
2677
2759
  this.#editor = { ...this.#editor, submitEnabled: canResume && !this.#disposed };
2678
2760
  this.#viewRevision += 1;
2761
+ // Invariant: overload publishes one authoritative view; settle tool phases before finalizing other live blocks.
2762
+ this.#settleFailedDeclarations({ role: "assistant", stopReason: cancelled ? "aborted" : "error",
2763
+ errorMessage: "Tool result unavailable after UI delivery overload" });
2679
2764
  this.#setTranscript(this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized", revision: block.revision + 1 } : block));
2680
2765
  for (const [correlationId, result] of this.#reservedOutcomes) {
2681
2766
  this.#deliver(this.#event({ type: "command-outcome", correlationId, ...result }));
@@ -3224,6 +3309,16 @@ function compactResourceLabel(path) {
3224
3309
  const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
3225
3310
  return segments.at(-1) ?? path;
3226
3311
  }
3312
+ /** A later error may restate a declaration, but cannot make completed arguments incomplete. */
3313
+ function retainCompletedArguments(current, next) {
3314
+ if (current.toolState?.argsComplete !== true || next.toolState === undefined || next.toolState.argsComplete)
3315
+ return next;
3316
+ return { ...next, toolState: { ...next.toolState, argsComplete: true },
3317
+ ...(next.toolRendering === undefined || current.toolRendering === undefined ? {} : {
3318
+ toolRendering: { ...next.toolRendering, arguments: current.toolRendering.arguments },
3319
+ }),
3320
+ payload: { ...(isRecord(next.payload) ? next.payload : {}), argsComplete: true } };
3321
+ }
3227
3322
  /**
3228
3323
  * Whether two blocks say the same thing. A block that says what it already said is not a
3229
3324
  * new revision: the shell renders a block once per revision, so bumping one it did not
@@ -3234,6 +3329,8 @@ function sameBlockContent(left, right) {
3234
3329
  && left.status === right.status
3235
3330
  && left.title === right.title
3236
3331
  && left.text === right.text
3332
+ && sameValue(left.toolState, right.toolState)
3333
+ && sameValue(left.toolRendering, right.toolRendering)
3237
3334
  && sameValue(left.payload, right.payload)
3238
3335
  && sameValue(left.imageReferences ?? [], right.imageReferences ?? []);
3239
3336
  }
@@ -0,0 +1,13 @@
1
+ import type { OwnedUiToolRendering, OwnedUiTranscriptImageReference } from "../../../contracts/owned-ui/index.js";
2
+ /** Text lives once on the block; content parts refer to UTF-16 slices or opaque image references. */
3
+ export declare function toolRenderingInput(options: {
4
+ readonly args: unknown;
5
+ readonly previousArgs?: OwnedUiToolRendering | undefined;
6
+ readonly result?: unknown;
7
+ readonly payload: Record<string, unknown>;
8
+ readonly image: (value: unknown) => OwnedUiTranscriptImageReference | undefined;
9
+ }): {
10
+ text: string;
11
+ toolRendering: OwnedUiToolRendering;
12
+ imageReferences: readonly OwnedUiTranscriptImageReference[];
13
+ };
@@ -0,0 +1,122 @@
1
+ const MAX_METADATA_BYTES = 64 * 1024;
2
+ /** Copy JSON metadata without diagnostic depth/array truncation or traversing result text/images. */
3
+ function renderingJson(value) {
4
+ let remaining = MAX_METADATA_BYTES;
5
+ const encoded = JSON.stringify(value, function (key, item) {
6
+ if (key && !Array.isArray(this))
7
+ remaining -= jsonStringBytes(key) + 1;
8
+ if (typeof item === "string")
9
+ remaining -= jsonStringBytes(item);
10
+ else if (item === null || typeof item === "boolean" || typeof item === "number" && Number.isFinite(item)) {
11
+ remaining -= String(item).length;
12
+ }
13
+ else if (typeof item === "object")
14
+ remaining -= 2;
15
+ else if (item === undefined)
16
+ return item;
17
+ else
18
+ throw new TypeError("unsupported JSON metadata");
19
+ if (--remaining < 0)
20
+ throw new RangeError("metadata exceeds the supported payload limit");
21
+ return item;
22
+ });
23
+ if (encoded === undefined)
24
+ throw new TypeError("unsupported JSON metadata");
25
+ if (Buffer.byteLength(encoded) > MAX_METADATA_BYTES)
26
+ throw new RangeError("metadata exceeds the supported payload limit");
27
+ return JSON.parse(encoded);
28
+ }
29
+ function jsonStringBytes(value) {
30
+ // Performance: reject very large strings before allocating their escaped representation.
31
+ if (value.length > MAX_METADATA_BYTES)
32
+ throw new RangeError("metadata exceeds the supported payload limit");
33
+ return Buffer.byteLength(JSON.stringify(value));
34
+ }
35
+ /** Text lives once on the block; content parts refer to UTF-16 slices or opaque image references. */
36
+ export function toolRenderingInput(options) {
37
+ const warnings = new Set();
38
+ let args = {};
39
+ try {
40
+ args = options.args === undefined && options.previousArgs !== undefined
41
+ ? options.previousArgs.arguments : renderingJson(options.args ?? {});
42
+ if (options.args === undefined && options.previousArgs?.unavailable?.includes("Tool arguments unavailable")) {
43
+ warnings.add("Tool arguments unavailable: unsupported or oversized rendering data.");
44
+ }
45
+ }
46
+ catch {
47
+ warnings.add("Tool arguments unavailable: unsupported or oversized rendering data.");
48
+ }
49
+ const imageReferences = [];
50
+ const content = [];
51
+ const texts = [];
52
+ let length = 0;
53
+ const source = isRecord(options.result) ? options.result : undefined;
54
+ const rawContent = source === undefined ? options.result : source.content;
55
+ const parts = typeof rawContent === "string" ? [{ type: "text", text: rawContent }] : rawContent;
56
+ if (Array.isArray(parts)) {
57
+ for (const part of parts) {
58
+ if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
59
+ if (texts.length > 0)
60
+ length++;
61
+ const start = length;
62
+ texts.push(part.text);
63
+ length += part.text.length;
64
+ content.push({ type: "text", start, end: length });
65
+ }
66
+ else if (isRecord(part) && part.type === "image") {
67
+ const reference = imageReferences.length < 16 ? options.image(part) : undefined;
68
+ if (reference === undefined)
69
+ warnings.add("Image unavailable: unsupported data or attachment limit exceeded.");
70
+ else {
71
+ content.push({ type: "image", imageIndex: imageReferences.length });
72
+ imageReferences.push(reference);
73
+ }
74
+ }
75
+ else
76
+ warnings.add("Tool content unavailable: unsupported content part.");
77
+ }
78
+ }
79
+ else if (options.result !== undefined)
80
+ warnings.add("Tool content unavailable: malformed result.");
81
+ let details;
82
+ if (source?.details !== undefined) {
83
+ try {
84
+ details = renderingJson(source.details);
85
+ }
86
+ catch {
87
+ warnings.add("Tool details unavailable: unsupported or oversized rendering data.");
88
+ }
89
+ }
90
+ const text = texts.join("\n");
91
+ let toolRendering = {
92
+ arguments: args,
93
+ ...(options.result === undefined ? {} : { result: { content, ...(details === undefined ? {} : { details }) } }),
94
+ };
95
+ const warning = () => [...warnings].join(" ");
96
+ const fits = () => Buffer.byteLength(JSON.stringify({ payload: options.payload, toolRendering: {
97
+ ...toolRendering, ...(warnings.size === 0 ? {} : { unavailable: warning() }),
98
+ } })) <= MAX_METADATA_BYTES;
99
+ if (!fits()) {
100
+ warnings.add("Tool details unavailable: rendering metadata exceeds the supported payload limit.");
101
+ if (toolRendering.result)
102
+ toolRendering = { ...toolRendering, result: { content } };
103
+ }
104
+ if (!fits()) {
105
+ warnings.add("Tool arguments unavailable: rendering metadata exceeds the supported payload limit.");
106
+ toolRendering = { ...toolRendering, arguments: {} };
107
+ }
108
+ if (!fits()) {
109
+ // Rationale: too many content boundaries exceed the metadata budget; retain text/images with an explicit fallback.
110
+ warnings.add("Tool content boundaries unavailable: rendering metadata exceeds the supported payload limit.");
111
+ toolRendering = { ...toolRendering, result: { content: [
112
+ { type: "text", start: 0, end: text.length },
113
+ ...imageReferences.map((_, imageIndex) => ({ type: "image", imageIndex })),
114
+ ] } };
115
+ }
116
+ if (warnings.size > 0)
117
+ toolRendering = { ...toolRendering, unavailable: warning() };
118
+ return { text, toolRendering, imageReferences };
119
+ }
120
+ function isRecord(value) {
121
+ return typeof value === "object" && value !== null && !Array.isArray(value);
122
+ }