@danypops/papyrus 0.34.3 → 0.35.1

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 (47) hide show
  1. package/README.md +5 -189
  2. package/package.json +8 -16
  3. package/src/cli.ts +0 -0
  4. package/src/index.ts +32 -0
  5. package/src/modules/discuss.ts +6 -1
  6. package/extension/src/active-task-continuation.ts +0 -131
  7. package/extension/src/artifact-browser.ts +0 -229
  8. package/extension/src/artifact-detail-format.ts +0 -38
  9. package/extension/src/artifact-detail-view.ts +0 -121
  10. package/extension/src/artifact-format.ts +0 -84
  11. package/extension/src/artifact-relationship-lines.ts +0 -24
  12. package/extension/src/artifact-status-presentation.ts +0 -71
  13. package/extension/src/base-prompt-breakdown.ts +0 -55
  14. package/extension/src/beautiful-mermaid-renderer.ts +0 -68
  15. package/extension/src/bounded-poll.ts +0 -20
  16. package/extension/src/context-budget.ts +0 -503
  17. package/extension/src/context-injection-telemetry.ts +0 -88
  18. package/extension/src/context-view.ts +0 -222
  19. package/extension/src/discuss-ask-layout.ts +0 -193
  20. package/extension/src/discuss-ask-view.ts +0 -1301
  21. package/extension/src/discuss.ts +0 -134
  22. package/extension/src/discussion-detail-view.ts +0 -136
  23. package/extension/src/docs.ts +0 -58
  24. package/extension/src/domain-tools.ts +0 -886
  25. package/extension/src/index.ts +0 -776
  26. package/extension/src/markdown.ts +0 -60
  27. package/extension/src/note-widget.ts +0 -8
  28. package/extension/src/notes.ts +0 -102
  29. package/extension/src/playbook-bridge.ts +0 -91
  30. package/extension/src/playbooks.ts +0 -97
  31. package/extension/src/rules.ts +0 -51
  32. package/extension/src/service-client.ts +0 -29
  33. package/extension/src/session-identity.ts +0 -22
  34. package/extension/src/skill-catalog-footprint.ts +0 -183
  35. package/extension/src/skills.ts +0 -127
  36. package/extension/src/task-context.ts +0 -1
  37. package/extension/src/task-detail-format.ts +0 -110
  38. package/extension/src/task-detail-view.ts +0 -139
  39. package/extension/src/task-focus-events.ts +0 -57
  40. package/extension/src/task-graph.ts +0 -116
  41. package/extension/src/task-presentation.ts +0 -26
  42. package/extension/src/task-widget.ts +0 -70
  43. package/extension/src/tasks.ts +0 -418
  44. package/extension/src/tool-rendering/artifact-card.ts +0 -117
  45. package/extension/src/tool-rendering/artifact-list.ts +0 -179
  46. package/extension/src/tool-rendering/index.ts +0 -109
  47. package/extension/src/tool-rendering/render-model.ts +0 -410
@@ -1,179 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
3
- import { TOOL_COLLAPSED_ROW_LIMIT } from "../../../src/constants.ts";
4
- import { countSummary, expandHint, kindGlyph, statusGlyph, treeConnector } from "./artifact-card.ts";
5
- import type {
6
- ArtifactListToolDetails,
7
- GraphToolDetails,
8
- ToolArtifactSummary,
9
- } from "./render-model.ts";
10
-
11
- function pluralKind(rows: readonly ToolArtifactSummary[]): string {
12
- const kind = rows[0]?.kind ?? "artifact";
13
- if (kind === "task") return "tasks";
14
- if (kind === "doc") return "documents";
15
- if (kind === "skill") return "skills";
16
- if (kind === "rule") return "rules";
17
- return "artifacts";
18
- }
19
-
20
- function statusSummary(rows: readonly ToolArtifactSummary[]): string {
21
- const counts = new Map<string, number>();
22
- for (const row of rows) counts.set(row.status, (counts.get(row.status) ?? 0) + 1);
23
- return [...counts.entries()].map(([status, count]) => `${status} ${count}`).join(" · ");
24
- }
25
-
26
- function rowLine(row: ToolArtifactSummary, expanded: boolean, theme: Theme): string {
27
- const identity = expanded ? `${row.id} ` : "";
28
- return [
29
- theme.fg("muted", `${statusGlyph(row.status)} ${row.status}`),
30
- theme.fg("accent", identity),
31
- theme.fg("text", row.title),
32
- ].join(" ");
33
- }
34
-
35
- function rowMetadata(row: ToolArtifactSummary): string {
36
- return [row.subtype, ...row.labels].filter(Boolean).join(" · ");
37
- }
38
-
39
- /** Bounded collapsed/expanded artifact collection presentation. */
40
- export class ArtifactListCard implements Component {
41
- private details: ArtifactListToolDetails;
42
- private theme: Theme;
43
- private expanded: boolean;
44
- private cachedWidth: number | undefined;
45
- private cachedLines: string[] | undefined;
46
-
47
- constructor(details: ArtifactListToolDetails, theme: Theme, expanded: boolean) {
48
- this.details = details;
49
- this.theme = theme;
50
- this.expanded = expanded;
51
- }
52
-
53
- update(details: ArtifactListToolDetails, theme: Theme, expanded: boolean): void {
54
- this.details = details;
55
- this.theme = theme;
56
- this.expanded = expanded;
57
- this.invalidate();
58
- }
59
-
60
- render(width: number): string[] {
61
- const safeWidth = Math.max(1, width);
62
- if (this.cachedLines && this.cachedWidth === safeWidth) return this.cachedLines;
63
- const rows = this.details.rows;
64
- const noun = pluralKind(rows);
65
- const lines = [truncateToWidth(
66
- this.theme.fg("toolTitle", this.theme.bold(`${countSummary(rows.length, this.details.total)} ${noun}`)),
67
- safeWidth,
68
- )];
69
- if (rows.length === 0) {
70
- lines.push(truncateToWidth(this.theme.fg("dim", `No ${noun}.`), safeWidth));
71
- } else {
72
- lines.push(truncateToWidth(this.theme.fg("muted", statusSummary(rows)), safeWidth));
73
- const display = this.expanded ? rows : rows.slice(0, TOOL_COLLAPSED_ROW_LIMIT);
74
- for (const row of display) {
75
- lines.push(truncateToWidth(rowLine(row, this.expanded, this.theme), safeWidth));
76
- if (this.expanded) {
77
- const metadata = rowMetadata(row);
78
- if (metadata) lines.push(truncateToWidth(this.theme.fg("dim", ` ${metadata}`), safeWidth));
79
- }
80
- }
81
- const omitted = Math.max(0, this.details.total - display.length);
82
- if (omitted > 0) lines.push(truncateToWidth(this.theme.fg("dim", `${omitted} more · ${expandHint()}`), safeWidth));
83
- }
84
- this.cachedWidth = safeWidth;
85
- this.cachedLines = lines;
86
- return lines;
87
- }
88
-
89
- invalidate(): void {
90
- this.cachedWidth = undefined;
91
- this.cachedLines = undefined;
92
- }
93
- }
94
-
95
- interface HierarchyRow {
96
- node: ToolArtifactSummary;
97
- prefix: string;
98
- connector: string;
99
- }
100
-
101
- function hierarchyRows(details: GraphToolDetails): HierarchyRow[] {
102
- const byId = new Map(details.nodes.map((node) => [node.id, node]));
103
- const childIds = new Map<string, string[]>();
104
- const contained = new Set<string>();
105
- for (const edge of details.edges) {
106
- if (edge.relation !== "contains" || !byId.has(edge.from) || !byId.has(edge.to)) continue;
107
- const children = childIds.get(edge.from) ?? [];
108
- children.push(edge.to);
109
- childIds.set(edge.from, children);
110
- contained.add(edge.to);
111
- }
112
- const roots = details.nodes.filter((node) => !contained.has(node.id));
113
- const rows: HierarchyRow[] = [];
114
- const visited = new Set<string>();
115
- const visit = (node: ToolArtifactSummary, prefix: string, connector: string): void => {
116
- if (visited.has(node.id)) return;
117
- visited.add(node.id);
118
- rows.push({ node, prefix, connector });
119
- const children = (childIds.get(node.id) ?? []).map((id) => byId.get(id)).filter((child): child is ToolArtifactSummary => child !== undefined);
120
- children.forEach((child, index) => {
121
- const last = index === children.length - 1;
122
- visit(child, `${prefix}${connector ? (connector === "└─" ? " " : "│ ") : ""}`, treeConnector(last));
123
- });
124
- };
125
- for (const root of roots) visit(root, "", "");
126
- for (const node of details.nodes) visit(node, "", "");
127
- return rows;
128
- }
129
-
130
- /** Bounded task containment preview; dependency graphs use the dedicated graph renderer. */
131
- export class TaskHierarchyPreview implements Component {
132
- private details: GraphToolDetails;
133
- private theme: Theme;
134
- private expanded: boolean;
135
- private cachedWidth: number | undefined;
136
- private cachedLines: string[] | undefined;
137
-
138
- constructor(details: GraphToolDetails, theme: Theme, expanded: boolean) {
139
- this.details = details;
140
- this.theme = theme;
141
- this.expanded = expanded;
142
- }
143
-
144
- update(details: GraphToolDetails, theme: Theme, expanded: boolean): void {
145
- this.details = details;
146
- this.theme = theme;
147
- this.expanded = expanded;
148
- this.invalidate();
149
- }
150
-
151
- render(width: number): string[] {
152
- const safeWidth = Math.max(1, width);
153
- if (this.cachedLines && this.cachedWidth === safeWidth) return this.cachedLines;
154
- const rows = hierarchyRows(this.details);
155
- const lines = [truncateToWidth(
156
- this.theme.fg("toolTitle", this.theme.bold(`${this.details.nodes.length} tasks · ${this.details.edges.length} edges`)),
157
- safeWidth,
158
- )];
159
- for (const row of rows) {
160
- const identity = this.expanded ? `${row.node.id} ` : "";
161
- lines.push(truncateToWidth(
162
- `${row.prefix}${row.connector}${row.connector ? " " : ""}${this.theme.fg("accent", kindGlyph(row.node.kind))} ${this.theme.fg("muted", statusGlyph(row.node.status))} ${this.theme.fg("accent", identity)}${this.theme.fg("text", row.node.title)}`,
163
- safeWidth,
164
- ));
165
- if (this.expanded) {
166
- const metadata = rowMetadata(row.node);
167
- if (metadata) lines.push(truncateToWidth(this.theme.fg("dim", `${row.prefix} ${metadata}`), safeWidth));
168
- }
169
- }
170
- this.cachedWidth = safeWidth;
171
- this.cachedLines = lines;
172
- return lines;
173
- }
174
-
175
- invalidate(): void {
176
- this.cachedWidth = undefined;
177
- this.cachedLines = undefined;
178
- }
179
- }
@@ -1,109 +0,0 @@
1
- import type {
2
- AgentToolResult,
3
- Theme,
4
- ToolRenderResultOptions,
5
- } from "@earendil-works/pi-coding-agent";
6
- import { type Component, Text } from "@earendil-works/pi-tui";
7
- import { ArtifactCard } from "./artifact-card.ts";
8
- import { ArtifactListCard, TaskHierarchyPreview } from "./artifact-list.ts";
9
- import { parsePapyrusToolDetails, type PapyrusToolDetails } from "./render-model.ts";
10
-
11
- const CALL_VALUE_MAX_CHARACTERS = 80;
12
-
13
- export interface PapyrusToolRenderContext {
14
- lastComponent: Component | undefined;
15
- isError: boolean;
16
- }
17
-
18
- function primaryArgument(args: Record<string, unknown>): string | undefined {
19
- // name/title before id: a caller that already knows the name shouldn't have the raw id echoed
20
- // back at it; id only surfaces here when it's genuinely the only identifying argument given.
21
- for (const key of ["name", "title", "id", "text", "query", "kind", "template_id"]) {
22
- const value = args[key];
23
- if (typeof value === "string" && value.trim()) return value.slice(0, CALL_VALUE_MAX_CHARACTERS);
24
- }
25
- return undefined;
26
- }
27
-
28
- /** Compact native call header that never echoes bodies or structured payloads. */
29
- export function renderPapyrusToolCall(label: string, args: Record<string, unknown>, theme: Theme): Component {
30
- const action = typeof args.action === "string" ? args.action : "call";
31
- const primary = primaryArgument(args);
32
- const text = [
33
- theme.fg("toolTitle", theme.bold(label)),
34
- theme.fg("muted", action),
35
- ...(primary ? [theme.fg("accent", primary)] : []),
36
- ].join(" ");
37
- return new Text(text, 0, 0);
38
- }
39
-
40
- function textContent(result: AgentToolResult<unknown>): string {
41
- return result.content
42
- .filter((entry): entry is { type: "text"; text: string } => entry.type === "text")
43
- .map((entry) => entry.text)
44
- .join("\n");
45
- }
46
-
47
- function simpleDetailsText(details: Exclude<PapyrusToolDetails, { kind: "artifact" | "artifact-list" | "graph" }>): string {
48
- switch (details.kind) {
49
- case "transition":
50
- return `✓ ${details.fromStatus} → ${details.toStatus}\n${details.artifact.title}`;
51
- case "gate-run": {
52
- const passed = details.gates.filter((gate) => gate.passed).length;
53
- return [
54
- `${passed}/${details.gates.length} gates passed for "${details.artifactTitle}"`,
55
- ...details.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.type}: ${gate.target}${gate.output ? ` — ${gate.output}` : ""}`),
56
- ].join("\n");
57
- }
58
- case "invocation":
59
- return [
60
- `✓ Run ${details.runId}`,
61
- `${details.created.tasks.length} tasks · ${details.created.docs.length} docs · ${details.created.rules.length} rules`,
62
- ...(details.created.roots.length ? [`Roots: ${details.created.roots.join(", ")}`] : []),
63
- ].join("\n");
64
- case "preview":
65
- return `${details.title}\n${details.content}${details.completeness.truncated ? `\n[truncated ${details.completeness.omitted} characters]` : ""}`;
66
- case "error":
67
- return `${details.code}: ${details.message}`;
68
- }
69
- }
70
-
71
- /** Render structured details for humans while preserving compact model content as fallback. */
72
- export function renderPapyrusToolResult(
73
- result: AgentToolResult<unknown>,
74
- options: ToolRenderResultOptions,
75
- theme: Theme,
76
- context: PapyrusToolRenderContext,
77
- ): Component {
78
- if (options.isPartial) return new Text(theme.fg("warning", "Working…"), 0, 0);
79
- const details = parsePapyrusToolDetails(result.details);
80
- if (!details) return new Text(theme.fg("toolOutput", textContent(result)), 0, 0);
81
-
82
- if (details.kind === "artifact") {
83
- const previous = context.lastComponent instanceof ArtifactCard ? context.lastComponent : undefined;
84
- if (previous) {
85
- previous.update(details, theme, options.expanded);
86
- return previous;
87
- }
88
- return new ArtifactCard(details, theme, options.expanded);
89
- }
90
- if (details.kind === "artifact-list") {
91
- const previous = context.lastComponent instanceof ArtifactListCard ? context.lastComponent : undefined;
92
- if (previous) {
93
- previous.update(details, theme, options.expanded);
94
- return previous;
95
- }
96
- return new ArtifactListCard(details, theme, options.expanded);
97
- }
98
- if (details.kind === "graph") {
99
- const previous = context.lastComponent instanceof TaskHierarchyPreview ? context.lastComponent : undefined;
100
- if (previous) {
101
- previous.update(details, theme, options.expanded);
102
- return previous;
103
- }
104
- return new TaskHierarchyPreview(details, theme, options.expanded);
105
- }
106
-
107
- const color = details.kind === "error" || context.isError ? "error" : "toolOutput";
108
- return new Text(theme.fg(color, simpleDetailsText(details)), 0, 0);
109
- }
@@ -1,410 +0,0 @@
1
- import {
2
- TOOL_DETAILS_BODY_MAX_CHARACTERS,
3
- TOOL_DETAILS_FIELD_MAX_CHARACTERS,
4
- TOOL_DETAILS_MAX_EDGES,
5
- TOOL_DETAILS_MAX_ITEMS,
6
- TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS,
7
- TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS,
8
- TOOL_MODEL_CONTENT_MAX_CHARACTERS,
9
- } from "../../../src/constants.ts";
10
- import type { Artifact } from "../../../src/domain/artifact.ts";
11
-
12
- export const PAPYRUS_TOOL_DETAILS_SCHEMA = "papyrus.tool-details/v1" as const;
13
-
14
- export interface ResultCompleteness {
15
- truncated: boolean;
16
- omitted: number;
17
- }
18
-
19
- export interface ToolArtifactSummary {
20
- id: string;
21
- kind: string;
22
- title: string;
23
- status: string;
24
- subtype: string;
25
- labels: string[];
26
- }
27
-
28
- export interface ToolArtifact extends ToolArtifactSummary {
29
- body: string;
30
- createdAt: string;
31
- updatedAt: string;
32
- }
33
-
34
- interface ToolDetailsBase {
35
- schemaVersion: typeof PAPYRUS_TOOL_DETAILS_SCHEMA;
36
- operation: string;
37
- kind: string;
38
- }
39
-
40
- export interface ArtifactToolDetails extends ToolDetailsBase {
41
- kind: "artifact";
42
- artifact: ToolArtifact;
43
- completeness: ResultCompleteness;
44
- }
45
-
46
- export interface ArtifactListToolDetails extends ToolDetailsBase {
47
- kind: "artifact-list";
48
- rows: ToolArtifactSummary[];
49
- total: number;
50
- completeness: ResultCompleteness;
51
- }
52
-
53
- export interface TransitionToolDetails extends ToolDetailsBase {
54
- kind: "transition";
55
- artifact: ToolArtifactSummary;
56
- fromStatus: string;
57
- toStatus: string;
58
- }
59
-
60
- export interface ToolGraphEdge {
61
- from: string;
62
- relation: string;
63
- to: string;
64
- }
65
-
66
- export interface GraphToolDetails extends ToolDetailsBase {
67
- kind: "graph";
68
- nodes: ToolArtifactSummary[];
69
- edges: ToolGraphEdge[];
70
- nodeCompleteness: ResultCompleteness;
71
- edgeCompleteness: ResultCompleteness;
72
- }
73
-
74
- export interface ToolGateRow {
75
- passed: boolean;
76
- type: string;
77
- target: string;
78
- output: string;
79
- }
80
-
81
- export interface GateRunToolDetails extends ToolDetailsBase {
82
- kind: "gate-run";
83
- artifactId: string;
84
- artifactTitle: string;
85
- gates: ToolGateRow[];
86
- completeness: ResultCompleteness;
87
- }
88
-
89
- export interface ToolInvocationCreated {
90
- tasks: string[];
91
- docs: string[];
92
- rules: string[];
93
- roots: string[];
94
- }
95
-
96
- export interface InvocationToolDetails extends ToolDetailsBase {
97
- kind: "invocation";
98
- runId: string;
99
- created: ToolInvocationCreated;
100
- completeness: ResultCompleteness;
101
- }
102
-
103
- export interface PreviewToolDetails extends ToolDetailsBase {
104
- kind: "preview";
105
- title: string;
106
- content: string;
107
- completeness: ResultCompleteness;
108
- }
109
-
110
- export interface ErrorToolDetails extends ToolDetailsBase {
111
- kind: "error";
112
- code: string;
113
- message: string;
114
- }
115
-
116
- export type PapyrusToolDetails =
117
- | ArtifactToolDetails
118
- | ArtifactListToolDetails
119
- | TransitionToolDetails
120
- | GraphToolDetails
121
- | GateRunToolDetails
122
- | InvocationToolDetails
123
- | PreviewToolDetails
124
- | ErrorToolDetails;
125
-
126
- export interface ModelContent {
127
- text: string;
128
- truncated: boolean;
129
- omitted: number;
130
- }
131
-
132
- function completeness(total: number, returned: number): ResultCompleteness {
133
- const omitted = Math.max(0, total - returned);
134
- return { truncated: omitted > 0, omitted };
135
- }
136
-
137
- function boundedText(value: string, maximum: number): { value: string; completeness: ResultCompleteness } {
138
- const clipped = value.slice(0, maximum);
139
- return { value: clipped, completeness: completeness(value.length, clipped.length) };
140
- }
141
-
142
- function artifactSummary(artifact: Artifact): ToolArtifactSummary {
143
- return {
144
- id: artifact.id,
145
- kind: artifact.kind,
146
- title: artifact.title,
147
- status: artifact.status,
148
- subtype: artifact.subtype,
149
- labels: artifact.labels.slice(0, TOOL_DETAILS_MAX_ITEMS),
150
- };
151
- }
152
-
153
- export function createArtifactDetails(operation: string, artifact: Artifact): ArtifactToolDetails {
154
- const body = boundedText(artifact.body, TOOL_DETAILS_BODY_MAX_CHARACTERS);
155
- return {
156
- schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
157
- kind: "artifact",
158
- operation,
159
- artifact: {
160
- ...artifactSummary(artifact),
161
- body: body.value,
162
- createdAt: artifact.created_at,
163
- updatedAt: artifact.updated_at,
164
- },
165
- completeness: body.completeness,
166
- };
167
- }
168
-
169
- export function createArtifactListDetails(
170
- operation: string,
171
- artifacts: readonly Artifact[],
172
- total = artifacts.length,
173
- ): ArtifactListToolDetails {
174
- const rows = artifacts.slice(0, TOOL_DETAILS_MAX_ITEMS).map(artifactSummary);
175
- return {
176
- schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
177
- kind: "artifact-list",
178
- operation,
179
- rows,
180
- total,
181
- completeness: completeness(Math.max(total, artifacts.length), rows.length),
182
- };
183
- }
184
-
185
- export function createTransitionDetails(
186
- operation: string,
187
- artifact: Artifact,
188
- fromStatus: string,
189
- toStatus: string,
190
- ): TransitionToolDetails {
191
- return {
192
- schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
193
- kind: "transition",
194
- operation,
195
- artifact: artifactSummary(artifact),
196
- fromStatus,
197
- toStatus,
198
- };
199
- }
200
-
201
- export function createGraphDetails(
202
- operation: string,
203
- artifacts: readonly Artifact[],
204
- edges: readonly ToolGraphEdge[],
205
- ): GraphToolDetails {
206
- const nodes = artifacts.slice(0, TOOL_DETAILS_MAX_ITEMS).map(artifactSummary);
207
- const boundedEdges = edges.slice(0, TOOL_DETAILS_MAX_EDGES).map((edge) => ({ ...edge }));
208
- return {
209
- schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
210
- kind: "graph",
211
- operation,
212
- nodes,
213
- edges: boundedEdges,
214
- nodeCompleteness: completeness(artifacts.length, nodes.length),
215
- edgeCompleteness: completeness(edges.length, boundedEdges.length),
216
- };
217
- }
218
-
219
- export function createGateRunDetails(
220
- operation: string,
221
- artifactId: string,
222
- artifactTitle: string,
223
- gates: readonly ToolGateRow[],
224
- ): GateRunToolDetails {
225
- const boundedGates = gates.slice(0, TOOL_DETAILS_MAX_ITEMS).map((gate) => ({
226
- ...gate,
227
- output: gate.output.slice(0, TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS),
228
- }));
229
- return {
230
- schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
231
- kind: "gate-run",
232
- operation,
233
- artifactId,
234
- artifactTitle,
235
- gates: boundedGates,
236
- completeness: completeness(gates.length, boundedGates.length),
237
- };
238
- }
239
-
240
- export function createInvocationDetails(
241
- operation: string,
242
- runId: string,
243
- created: ToolInvocationCreated,
244
- ): InvocationToolDetails {
245
- const bounded: ToolInvocationCreated = {
246
- tasks: created.tasks.slice(0, TOOL_DETAILS_MAX_ITEMS),
247
- docs: created.docs.slice(0, TOOL_DETAILS_MAX_ITEMS),
248
- rules: created.rules.slice(0, TOOL_DETAILS_MAX_ITEMS),
249
- roots: created.roots.slice(0, TOOL_DETAILS_MAX_ITEMS),
250
- };
251
- const total = created.tasks.length + created.docs.length + created.rules.length + created.roots.length;
252
- const returned = bounded.tasks.length + bounded.docs.length + bounded.rules.length + bounded.roots.length;
253
- return {
254
- schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
255
- kind: "invocation",
256
- operation,
257
- runId,
258
- created: bounded,
259
- completeness: completeness(total, returned),
260
- };
261
- }
262
-
263
- export function createPreviewDetails(operation: string, title: string, content: string): PreviewToolDetails {
264
- const bounded = boundedText(content, TOOL_DETAILS_BODY_MAX_CHARACTERS);
265
- return {
266
- schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
267
- kind: "preview",
268
- operation,
269
- title,
270
- content: bounded.value,
271
- completeness: bounded.completeness,
272
- };
273
- }
274
-
275
- export function createErrorDetails(operation: string, code: string, message: string): ErrorToolDetails {
276
- return {
277
- schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
278
- kind: "error",
279
- operation,
280
- code: code.slice(0, TOOL_DETAILS_FIELD_MAX_CHARACTERS),
281
- message: message.slice(0, TOOL_DETAILS_BODY_MAX_CHARACTERS),
282
- };
283
- }
284
-
285
- export function createModelContent(value: string): ModelContent {
286
- if (value.length <= TOOL_MODEL_CONTENT_MAX_CHARACTERS) {
287
- return { text: value, truncated: false, omitted: 0 };
288
- }
289
- let omitted = value.length - TOOL_MODEL_CONTENT_MAX_CHARACTERS;
290
- let marker = "";
291
- let kept = 0;
292
- for (let iteration = 0; iteration < 5; iteration += 1) {
293
- const nextMarker = `\n[truncated ${omitted} characters]`;
294
- const nextKept = Math.max(0, TOOL_MODEL_CONTENT_MAX_CHARACTERS - nextMarker.length);
295
- const nextOmitted = value.length - nextKept;
296
- marker = nextMarker;
297
- kept = nextKept;
298
- if (nextOmitted === omitted) break;
299
- omitted = nextOmitted;
300
- }
301
- return { text: `${value.slice(0, kept)}${marker}`, truncated: true, omitted: value.length - kept };
302
- }
303
-
304
- function isRecord(value: unknown): value is Record<string, unknown> {
305
- return typeof value === "object" && value !== null && !Array.isArray(value);
306
- }
307
-
308
- function isBoundedString(value: unknown, maximum = TOOL_DETAILS_FIELD_MAX_CHARACTERS): value is string {
309
- return typeof value === "string" && value.length <= maximum;
310
- }
311
-
312
- function isStringArray(value: unknown): value is string[] {
313
- return Array.isArray(value) && value.length <= TOOL_DETAILS_MAX_ITEMS && value.every((item) => isBoundedString(item));
314
- }
315
-
316
- function isCompleteness(value: unknown): value is ResultCompleteness {
317
- return isRecord(value) && typeof value.truncated === "boolean" && Number.isSafeInteger(value.omitted) && Number(value.omitted) >= 0;
318
- }
319
-
320
- function isArtifactSummary(value: unknown): value is ToolArtifactSummary {
321
- return isRecord(value)
322
- && isBoundedString(value.id)
323
- && isBoundedString(value.kind)
324
- && isBoundedString(value.title)
325
- && isBoundedString(value.status)
326
- && isBoundedString(value.subtype)
327
- && isStringArray(value.labels);
328
- }
329
-
330
- function isToolArtifact(value: unknown): value is ToolArtifact {
331
- if (!isRecord(value)) return false;
332
- const body = value.body;
333
- const createdAt = value.createdAt;
334
- const updatedAt = value.updatedAt;
335
- return isArtifactSummary(value)
336
- && isBoundedString(body, TOOL_DETAILS_BODY_MAX_CHARACTERS)
337
- && isBoundedString(createdAt)
338
- && isBoundedString(updatedAt);
339
- }
340
-
341
- function isGraphEdge(value: unknown): value is ToolGraphEdge {
342
- return isRecord(value) && isBoundedString(value.from) && isBoundedString(value.relation) && isBoundedString(value.to);
343
- }
344
-
345
- function isGateRow(value: unknown): value is ToolGateRow {
346
- return isRecord(value)
347
- && typeof value.passed === "boolean"
348
- && isBoundedString(value.type)
349
- && isBoundedString(value.target)
350
- && isBoundedString(value.output, TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS);
351
- }
352
-
353
- function isBoundedArray<T>(value: unknown, maximum: number, predicate: (entry: unknown) => entry is T): value is T[] {
354
- return Array.isArray(value) && value.length <= maximum && value.every(predicate);
355
- }
356
-
357
- /** Validate renderer details restored from session history before using them as typed presentation state. */
358
- export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | undefined {
359
- let serializedLength: number;
360
- try {
361
- serializedLength = JSON.stringify(value).length;
362
- } catch {
363
- return undefined;
364
- }
365
- if (serializedLength > TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS || !isRecord(value)
366
- || value.schemaVersion !== PAPYRUS_TOOL_DETAILS_SCHEMA
367
- || !isBoundedString(value.operation)
368
- || !isBoundedString(value.kind)) return undefined;
369
-
370
- switch (value.kind) {
371
- case "artifact":
372
- return isToolArtifact(value.artifact) && isCompleteness(value.completeness)
373
- ? value as unknown as ArtifactToolDetails : undefined;
374
- case "artifact-list":
375
- return isBoundedArray(value.rows, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary)
376
- && Number.isSafeInteger(value.total) && Number(value.total) >= value.rows.length
377
- && isCompleteness(value.completeness)
378
- ? value as unknown as ArtifactListToolDetails : undefined;
379
- case "transition":
380
- return isArtifactSummary(value.artifact) && isBoundedString(value.fromStatus) && isBoundedString(value.toStatus)
381
- ? value as unknown as TransitionToolDetails : undefined;
382
- case "graph":
383
- return isBoundedArray(value.nodes, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary)
384
- && isBoundedArray(value.edges, TOOL_DETAILS_MAX_EDGES, isGraphEdge)
385
- && isCompleteness(value.nodeCompleteness) && isCompleteness(value.edgeCompleteness)
386
- ? value as unknown as GraphToolDetails : undefined;
387
- case "gate-run":
388
- return isBoundedString(value.artifactId)
389
- && isBoundedString(value.artifactTitle)
390
- && isBoundedArray(value.gates, TOOL_DETAILS_MAX_ITEMS, isGateRow)
391
- && isCompleteness(value.completeness)
392
- ? value as unknown as GateRunToolDetails : undefined;
393
- case "invocation": {
394
- if (!isRecord(value.created)) return undefined;
395
- return isBoundedString(value.runId)
396
- && isStringArray(value.created.tasks) && isStringArray(value.created.docs)
397
- && isStringArray(value.created.rules) && isStringArray(value.created.roots)
398
- && isCompleteness(value.completeness)
399
- ? value as unknown as InvocationToolDetails : undefined;
400
- }
401
- case "preview":
402
- return isBoundedString(value.title) && isBoundedString(value.content, TOOL_DETAILS_BODY_MAX_CHARACTERS) && isCompleteness(value.completeness)
403
- ? value as unknown as PreviewToolDetails : undefined;
404
- case "error":
405
- return isBoundedString(value.code) && isBoundedString(value.message, TOOL_DETAILS_BODY_MAX_CHARACTERS)
406
- ? value as unknown as ErrorToolDetails : undefined;
407
- default:
408
- return undefined;
409
- }
410
- }