@danypops/pi-papyrus 0.59.2 → 0.59.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.
@@ -53,6 +53,8 @@ function simpleDetailsText(details: Exclude<PapyrusToolDetails, { kind: "artifac
53
53
  ].join("\n");
54
54
  case "preview":
55
55
  return `${details.title}\n${details.content}${details.completeness.truncated ? `\n[truncated ${details.completeness.omitted} characters]` : ""}`;
56
+ case "semantic-text":
57
+ return details.text;
56
58
  case "error":
57
59
  return `${details.code}: ${details.message}`;
58
60
  case "execution-plan":
@@ -18,6 +18,7 @@ import { type GraphToolDetails, isGraphEdge } from "./graph.ts";
18
18
  import type { LeaseToolDetails } from "./lease.ts";
19
19
  import type { ErrorToolDetails, NoFocusToolDetails, PreviewToolDetails } from "./misc.ts";
20
20
  import type { InvocationToolDetails, PlaybookInvocationToolDetails, PlaybookMissingArgumentsToolDetails } from "./playbook.ts";
21
+ import type { SemanticTextToolDetails } from "./semantic-text.ts";
21
22
  import {
22
23
  isArtifactSummary,
23
24
  isBoundedArray,
@@ -51,7 +52,8 @@ export type PapyrusToolDetails =
51
52
  | DiscussionToolDetails
52
53
  | TaskCompletionToolDetails
53
54
  | NoFocusToolDetails
54
- | LeaseToolDetails;
55
+ | LeaseToolDetails
56
+ | SemanticTextToolDetails;
55
57
 
56
58
  /** Validate renderer details restored from session history before using them as typed presentation state. */
57
59
  export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | undefined {
@@ -176,6 +178,10 @@ export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | un
176
178
  (value.note === undefined || isBoundedString(value.note))
177
179
  ? (value as unknown as LeaseToolDetails)
178
180
  : undefined;
181
+ case "semantic-text":
182
+ return isBoundedString(value.text, TOOL_DETAILS_BODY_MAX_CHARACTERS) && isCompleteness(value.completeness)
183
+ ? (value as unknown as SemanticTextToolDetails)
184
+ : undefined;
179
185
  default:
180
186
  return undefined;
181
187
  }
@@ -212,6 +218,7 @@ export {
212
218
  type PlaybookMissingArgumentsToolDetails,
213
219
  type ToolInvocationCreated,
214
220
  } from "./playbook.ts";
221
+ export { createSemanticTextDetails, type SemanticTextToolDetails } from "./semantic-text.ts";
215
222
  export {
216
223
  type ArtifactFocusAnnotation,
217
224
  createModelContent,
@@ -0,0 +1,20 @@
1
+ import { TOOL_DETAILS_BODY_MAX_CHARACTERS } from "@danypops/papyrus";
2
+ import { boundedText, PAPYRUS_TOOL_DETAILS_SCHEMA, type ResultCompleteness, type ToolDetailsBase } from "./shared.ts";
3
+
4
+ /** Carries an operation's bounded semantic text channel for human presentation. */
5
+ export interface SemanticTextToolDetails extends ToolDetailsBase {
6
+ kind: "semantic-text";
7
+ text: string;
8
+ completeness: ResultCompleteness;
9
+ }
10
+
11
+ export function createSemanticTextDetails(operation: string, text: string): SemanticTextToolDetails {
12
+ const bounded = boundedText(text, TOOL_DETAILS_BODY_MAX_CHARACTERS);
13
+ return {
14
+ schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
15
+ kind: "semantic-text",
16
+ operation,
17
+ text: bounded.value,
18
+ completeness: bounded.completeness,
19
+ };
20
+ }
@@ -33,8 +33,9 @@ import {
33
33
  createNoFocusDetails,
34
34
  createPlaybookInvocationDetails,
35
35
  createPlaybookMissingArgumentsDetails,
36
- createPreviewDetails,
36
+ createSemanticTextDetails,
37
37
  createTaskCompletionDetails,
38
+ type PapyrusToolDetails,
38
39
  parsePapyrusToolDetails,
39
40
  } from "../../tool-rendering/render-model.ts";
40
41
  import { recordRenderDiagnostic, shapeFingerprint } from "../render-diagnostics.ts";
@@ -52,7 +53,16 @@ import {
52
53
  renderPlaybookInvocationResult,
53
54
  renderPlaybookMissingArguments,
54
55
  } from "./playbook.ts";
55
- import { boundedJsonPreview, focusAnnotation, isArtifact, isArtifactArray, isTaskFocus, renderNoFocusedTask } from "./shared.ts";
56
+ import {
57
+ boundedJsonPreview,
58
+ focusAnnotation,
59
+ isArtifact,
60
+ isArtifactArray,
61
+ isSemanticTextOutput,
62
+ isTaskFocus,
63
+ renderNoFocusedTask,
64
+ semanticText,
65
+ } from "./shared.ts";
56
66
  import { isTaskCompletion, renderTaskCompletion } from "./task-completion.ts";
57
67
  import { isTaskExecutionPlan, renderTaskExecutionPlan } from "./task-execution.ts";
58
68
 
@@ -133,17 +143,16 @@ export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor):
133
143
  * before Pi ever persists it -- the seam papyrusVehicleRenderers' own renderResult never had
134
144
  * (it only converts shape at render time, from whatever the legacy {vehicle, output} path
135
145
  * already persisted verbatim, lease tokens and all). Every branch here mirrors the same
136
- * shape-detection papyrusVehicleRenderers's own renderResult uses, so the two stay in lockstep;
137
- * anything genuinely unmatched still becomes a real, bounded PreviewToolDetails rather than an
138
- * unprojected raw passthrough -- the one requirement this whole seam exists to satisfy.
146
+ * shape-detection papyrusVehicleRenderers's own renderResult uses, plus the server's explicit
147
+ * semantic text channel. An output matching no legal discriminated-union variant fails closed
148
+ * instead of silently persisting and rendering raw JSON.
139
149
  */
140
- function projectPapyrusPresentation(descriptor: VehicleOperationDescriptor, output: unknown): JsonValue {
141
- if (isArtifactArray(output)) return createArtifactListDetails(descriptor.name, output) as unknown as JsonValue;
142
- if (isArtifact(output)) return createArtifactDetails(descriptor.name, output) as unknown as JsonValue;
143
- if (isTaskFocus(output)) return createArtifactDetails(descriptor.name, output.artifact, focusAnnotation(output)) as unknown as JsonValue;
144
- if (output === null && descriptor.name === "tasks.focused") return createNoFocusDetails(descriptor.name) as unknown as JsonValue;
145
- if (isTaskExecutionPlan(output))
146
- return createExecutionPlanDetails(descriptor.name, output.nodes, output.layers, output.cycleIds) as unknown as JsonValue;
150
+ function projectPapyrusPresentation(descriptor: VehicleOperationDescriptor, output: unknown): PapyrusToolDetails {
151
+ if (isArtifactArray(output)) return createArtifactListDetails(descriptor.name, output);
152
+ if (isArtifact(output)) return createArtifactDetails(descriptor.name, output);
153
+ if (isTaskFocus(output)) return createArtifactDetails(descriptor.name, output.artifact, focusAnnotation(output));
154
+ if (output === null && descriptor.name === "tasks.focused") return createNoFocusDetails(descriptor.name);
155
+ if (isTaskExecutionPlan(output)) return createExecutionPlanDetails(descriptor.name, output.nodes, output.layers, output.cycleIds);
147
156
  if (isPlaybookInvocationResult(output)) {
148
157
  return createPlaybookInvocationDetails(descriptor.name, {
149
158
  playbookId: output.playbookId,
@@ -152,18 +161,18 @@ function projectPapyrusPresentation(descriptor: VehicleOperationDescriptor, outp
152
161
  rootTaskIds: output.rootTaskIds,
153
162
  entryTaskId: output.entryTaskId,
154
163
  execution: output.execution,
155
- }) as unknown as JsonValue;
164
+ });
156
165
  }
157
166
  if (isPlaybookMissingArguments(output)) {
158
- return createPlaybookMissingArgumentsDetails(descriptor.name, output.playbookId, output.missingArguments) as unknown as JsonValue;
167
+ return createPlaybookMissingArgumentsDetails(descriptor.name, output.playbookId, output.missingArguments);
159
168
  }
160
- if (isDiscussionAndRounds(output))
161
- return createDiscussionDetails(descriptor.name, output.rounds, output.discussion) as unknown as JsonValue;
162
- if (isDiscussionRoundsOnly(output)) return createDiscussionDetails(descriptor.name, output.rounds) as unknown as JsonValue;
163
- if (isDiscussionListOutput(output)) return createArtifactListDetails(descriptor.name, output.discussions) as unknown as JsonValue;
164
- if (isTaskCompletion(output)) return createTaskCompletionDetails(descriptor.name, output) as unknown as JsonValue;
165
- if (isTaskLeaseView(output)) return createLeaseDetails(descriptor.name, output) as unknown as JsonValue;
166
- return createPreviewDetails(descriptor.name, descriptor.name, boundedJsonPreview(output)) as unknown as JsonValue;
169
+ if (isDiscussionAndRounds(output)) return createDiscussionDetails(descriptor.name, output.rounds, output.discussion);
170
+ if (isDiscussionRoundsOnly(output)) return createDiscussionDetails(descriptor.name, output.rounds);
171
+ if (isDiscussionListOutput(output)) return createArtifactListDetails(descriptor.name, output.discussions);
172
+ if (isTaskCompletion(output)) return createTaskCompletionDetails(descriptor.name, output);
173
+ if (isTaskLeaseView(output)) return createLeaseDetails(descriptor.name, output);
174
+ if (isSemanticTextOutput(output)) return createSemanticTextDetails(descriptor.name, semanticText(output));
175
+ throw new Error(`${descriptor.name} produced no legal presentation variant`);
167
176
  }
168
177
 
169
178
  function renderFromPapyrusPresentation(
@@ -194,15 +203,19 @@ function renderFromPapyrusPresentation(
194
203
  return renderLease(presentation, theme);
195
204
  case "preview":
196
205
  return new Text(theme.fg("toolOutput", presentation.content), 0, 0);
206
+ case "semantic-text":
207
+ return new Text(theme.fg("toolOutput", presentation.text), 0, 0);
197
208
  case "transition":
198
209
  case "graph":
199
210
  case "gate-run":
200
211
  case "invocation":
201
212
  case "error":
202
- // Reachable only if a future caller starts producing these kinds through this seam
203
- // (today's Papyrus Vehicle outputs never do) -- a bounded JSON preview is still a
204
- // real, safe rendering rather than a crash.
213
+ // These variants are produced by native tools and remain valid when replayed through this shared renderer.
205
214
  return new Text(theme.fg("toolOutput", boundedJsonPreview(presentation)), 0, 0);
215
+ default: {
216
+ const exhaustive: never = presentation;
217
+ return exhaustive;
218
+ }
206
219
  }
207
220
  }
208
221
 
@@ -218,7 +231,8 @@ export function papyrusVehiclePresentations(descriptor: VehicleOperationDescript
218
231
  return {
219
232
  projector: {
220
233
  maxBytes: TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS,
221
- project: (output: unknown, _request: PiVehicleInvocationRequest) => projectPapyrusPresentation(descriptor, output),
234
+ project: (output: unknown, _request: PiVehicleInvocationRequest) =>
235
+ projectPapyrusPresentation(descriptor, output) as unknown as JsonValue,
222
236
  },
223
237
  renderResult(result, options, theme, context) {
224
238
  if (!options.isPartial && !context.isError) {
@@ -72,6 +72,31 @@ export function renderNoFocusedTask(theme: Theme): Component {
72
72
  return new Text(theme.fg("dim", "No focused task."), 0, 0);
73
73
  }
74
74
 
75
+ export interface SemanticTextOutput {
76
+ content: Array<{ type: "text"; text: string }>;
77
+ }
78
+
79
+ /** Recognizes the server's explicit model-facing semantic channel without projecting sibling payload fields. */
80
+ export function isSemanticTextOutput(value: unknown): value is SemanticTextOutput {
81
+ if (typeof value !== "object" || value === null) return false;
82
+ const content = (value as Record<string, unknown>).content;
83
+ return (
84
+ Array.isArray(content) &&
85
+ content.length > 0 &&
86
+ content.every(
87
+ (block) =>
88
+ typeof block === "object" &&
89
+ block !== null &&
90
+ (block as Record<string, unknown>).type === "text" &&
91
+ typeof (block as Record<string, unknown>).text === "string",
92
+ )
93
+ );
94
+ }
95
+
96
+ export function semanticText(output: SemanticTextOutput): string {
97
+ return output.content.map((block) => block.text).join("\n");
98
+ }
99
+
75
100
  /** What renderDiscussionAndRounds/renderTaskCompletion actually read -- satisfied by both a raw
76
101
  * Artifact (the live duck-typed output path) and the leaner projected ToolArtifactSummary (the
77
102
  * typed-DTO path), with no cast needed at either call site. */
@@ -23,7 +23,6 @@
23
23
  * had died.
24
24
  */
25
25
 
26
- import { createAgentNotifier } from "@danypops/vehicle-client-pi/agent-poll-ticker";
27
26
  import { createReconnectingVehicleClient, daemonInstanceIdentity } from "@danypops/vehicle-client/daemon-client";
28
27
  import { RemoteVehicleClient } from "@danypops/vehicle-client/http";
29
28
  import {
@@ -32,6 +31,7 @@ import {
32
31
  registerVehicleToolsWhenReady,
33
32
  type VehicleReadyEvent,
34
33
  } from "@danypops/vehicle-client-pi";
34
+ import { createAgentNotifier } from "@danypops/vehicle-client-pi/agent-poll-ticker";
35
35
  import { VehicleApprovalOutcomePoll } from "@danypops/vehicle-client-pi/vehicle-approval-outcome-poll";
36
36
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
37
37
  import { discussLiveFollowUp } from "../discuss/discuss-live-follow-up.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-papyrus",
3
- "version": "0.59.2",
3
+ "version": "0.59.3",
4
4
  "description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
5
5
  "license": "MIT",
6
6
  "type": "module",