@danypops/pi-papyrus 0.49.1 → 0.50.0
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.
|
@@ -55,6 +55,23 @@ function simpleDetailsText(details: Exclude<PapyrusToolDetails, { kind: "artifac
|
|
|
55
55
|
return `${details.title}\n${details.content}${details.completeness.truncated ? `\n[truncated ${details.completeness.omitted} characters]` : ""}`;
|
|
56
56
|
case "error":
|
|
57
57
|
return `${details.code}: ${details.message}`;
|
|
58
|
+
case "execution-plan":
|
|
59
|
+
return `${details.nodes.length} task(s) across ${details.layers.length} layer(s)${details.cycleIds.length ? `, ${details.cycleIds.length} in a cycle` : ""}`;
|
|
60
|
+
case "playbook-invocation":
|
|
61
|
+
return [
|
|
62
|
+
`✓ Run ${details.runId}`,
|
|
63
|
+
`${details.created.tasks.length} tasks · ${details.created.docs.length} docs · ${details.created.rules.length} rules`,
|
|
64
|
+
].join("\n");
|
|
65
|
+
case "playbook-missing-arguments":
|
|
66
|
+
return `Missing required argument(s): ${details.missingArguments.join(", ")}`;
|
|
67
|
+
case "discussion":
|
|
68
|
+
return `${details.discussion ? `${details.discussion.title}\n` : ""}${details.rounds.length} round(s)`;
|
|
69
|
+
case "task-completion":
|
|
70
|
+
return `${details.completed ? "✓" : "✗"} ${details.artifact.title}`;
|
|
71
|
+
case "no-focus":
|
|
72
|
+
return "No focused task.";
|
|
73
|
+
case "lease":
|
|
74
|
+
return `${details.taskName} — ${details.taskTitle}\nowner ${details.owner}, expires ${details.leaseExpiresAt}`;
|
|
58
75
|
}
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -127,6 +127,105 @@ export interface ErrorToolDetails extends ToolDetailsBase {
|
|
|
127
127
|
message: string;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
export interface ExecutionPlanNode {
|
|
131
|
+
id: string;
|
|
132
|
+
title: string;
|
|
133
|
+
status: string;
|
|
134
|
+
active: boolean;
|
|
135
|
+
state: string;
|
|
136
|
+
layer: number | null;
|
|
137
|
+
prerequisiteIds: string[];
|
|
138
|
+
successorIds: string[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ExecutionPlanToolDetails extends ToolDetailsBase {
|
|
142
|
+
kind: "execution-plan";
|
|
143
|
+
nodes: ExecutionPlanNode[];
|
|
144
|
+
layers: string[][];
|
|
145
|
+
cycleIds: string[];
|
|
146
|
+
completeness: ResultCompleteness;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface PlaybookInvocationCreated {
|
|
150
|
+
docs: string[];
|
|
151
|
+
rules: string[];
|
|
152
|
+
tasks: string[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface PlaybookInvocationToolDetails extends ToolDetailsBase {
|
|
156
|
+
kind: "playbook-invocation";
|
|
157
|
+
playbookId: string;
|
|
158
|
+
runId: string;
|
|
159
|
+
created: PlaybookInvocationCreated;
|
|
160
|
+
rootTaskIds: string[];
|
|
161
|
+
entryTaskId: string;
|
|
162
|
+
execution: { nodes: ExecutionPlanNode[]; layers: string[][]; cycleIds: string[] };
|
|
163
|
+
completeness: ResultCompleteness;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface PlaybookMissingArgumentsToolDetails extends ToolDetailsBase {
|
|
167
|
+
kind: "playbook-missing-arguments";
|
|
168
|
+
playbookId: string;
|
|
169
|
+
missingArguments: string[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface DiscussionRoundSummary {
|
|
173
|
+
roundNumber: number;
|
|
174
|
+
actor: string;
|
|
175
|
+
content: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface DiscussionToolDetails extends ToolDetailsBase {
|
|
179
|
+
kind: "discussion";
|
|
180
|
+
/** Absent for discuss.rounds, which returns rounds with no parent discussion in its own output. */
|
|
181
|
+
discussion?: ToolArtifactSummary;
|
|
182
|
+
rounds: DiscussionRoundSummary[];
|
|
183
|
+
completeness: ResultCompleteness;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface TaskGateResultSummary {
|
|
187
|
+
passed: boolean;
|
|
188
|
+
output: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface TaskChecklistReviewSummary {
|
|
192
|
+
item: string;
|
|
193
|
+
accepted: boolean;
|
|
194
|
+
reason?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface TaskBlockageSummary {
|
|
198
|
+
artifact: ToolArtifactSummary;
|
|
199
|
+
dependencyIds: string[];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface TaskCompletionToolDetails extends ToolDetailsBase {
|
|
203
|
+
kind: "task-completion";
|
|
204
|
+
artifact: ToolArtifactSummary;
|
|
205
|
+
gates: TaskGateResultSummary[];
|
|
206
|
+
checklist: TaskChecklistReviewSummary[];
|
|
207
|
+
completed: boolean;
|
|
208
|
+
focused?: ToolArtifactSummary;
|
|
209
|
+
blocked: TaskBlockageSummary[];
|
|
210
|
+
completeness: ResultCompleteness;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface NoFocusToolDetails extends ToolDetailsBase {
|
|
214
|
+
kind: "no-focus";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** tasks.claim/heartbeat_lease/release_lease/lease's own name-first view -- deliberately never carries the raw lease token; the model channel (which needs the real token for a later heartbeat/release call) is a separate, independent channel from this persisted, human-facing one. */
|
|
218
|
+
export interface LeaseToolDetails extends ToolDetailsBase {
|
|
219
|
+
kind: "lease";
|
|
220
|
+
taskName: string;
|
|
221
|
+
taskTitle: string;
|
|
222
|
+
owner: string;
|
|
223
|
+
claimedAt: string;
|
|
224
|
+
leaseExpiresAt: string;
|
|
225
|
+
heartbeatAt?: string;
|
|
226
|
+
note?: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
130
229
|
export type PapyrusToolDetails =
|
|
131
230
|
| ArtifactToolDetails
|
|
132
231
|
| ArtifactListToolDetails
|
|
@@ -135,7 +234,14 @@ export type PapyrusToolDetails =
|
|
|
135
234
|
| GateRunToolDetails
|
|
136
235
|
| InvocationToolDetails
|
|
137
236
|
| PreviewToolDetails
|
|
138
|
-
| ErrorToolDetails
|
|
237
|
+
| ErrorToolDetails
|
|
238
|
+
| ExecutionPlanToolDetails
|
|
239
|
+
| PlaybookInvocationToolDetails
|
|
240
|
+
| PlaybookMissingArgumentsToolDetails
|
|
241
|
+
| DiscussionToolDetails
|
|
242
|
+
| TaskCompletionToolDetails
|
|
243
|
+
| NoFocusToolDetails
|
|
244
|
+
| LeaseToolDetails;
|
|
139
245
|
|
|
140
246
|
export interface ModelContent {
|
|
141
247
|
text: string;
|
|
@@ -153,7 +259,9 @@ function boundedText(value: string, maximum: number): { value: string; completen
|
|
|
153
259
|
return { value: clipped, completeness: completeness(value.length, clipped.length) };
|
|
154
260
|
}
|
|
155
261
|
|
|
156
|
-
|
|
262
|
+
type ArtifactSummarySource = Pick<Artifact, "id" | "kind" | "title" | "status" | "subtype" | "labels"> & { alias?: string };
|
|
263
|
+
|
|
264
|
+
function artifactSummary(artifact: ArtifactSummarySource): ToolArtifactSummary {
|
|
157
265
|
return {
|
|
158
266
|
id: artifact.id,
|
|
159
267
|
alias: artifact.alias,
|
|
@@ -290,6 +398,171 @@ export function createErrorDetails(operation: string, code: string, message: str
|
|
|
290
398
|
};
|
|
291
399
|
}
|
|
292
400
|
|
|
401
|
+
function boundedStringArray(values: readonly string[]): string[] {
|
|
402
|
+
return values.slice(0, TOOL_DETAILS_MAX_ITEMS);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function boundedExecutionPlanNodes(nodes: readonly ExecutionPlanNode[]): ExecutionPlanNode[] {
|
|
406
|
+
return nodes.slice(0, TOOL_DETAILS_MAX_ITEMS).map((node) => ({
|
|
407
|
+
...node,
|
|
408
|
+
prerequisiteIds: boundedStringArray(node.prerequisiteIds),
|
|
409
|
+
successorIds: boundedStringArray(node.successorIds),
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function boundedLayers(layers: readonly (readonly string[])[]): string[][] {
|
|
414
|
+
return layers.slice(0, TOOL_DETAILS_MAX_ITEMS).map((layer) => boundedStringArray(layer));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function createExecutionPlanDetails(
|
|
418
|
+
operation: string,
|
|
419
|
+
nodes: readonly ExecutionPlanNode[],
|
|
420
|
+
layers: readonly (readonly string[])[],
|
|
421
|
+
cycleIds: readonly string[],
|
|
422
|
+
): ExecutionPlanToolDetails {
|
|
423
|
+
const boundedNodes = boundedExecutionPlanNodes(nodes);
|
|
424
|
+
return {
|
|
425
|
+
schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
|
|
426
|
+
kind: "execution-plan",
|
|
427
|
+
operation,
|
|
428
|
+
nodes: boundedNodes,
|
|
429
|
+
layers: boundedLayers(layers),
|
|
430
|
+
cycleIds: boundedStringArray(cycleIds),
|
|
431
|
+
completeness: completeness(nodes.length, boundedNodes.length),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function createPlaybookInvocationDetails(
|
|
436
|
+
operation: string,
|
|
437
|
+
fields: {
|
|
438
|
+
playbookId: string;
|
|
439
|
+
runId: string;
|
|
440
|
+
created: PlaybookInvocationCreated;
|
|
441
|
+
rootTaskIds: readonly string[];
|
|
442
|
+
entryTaskId: string;
|
|
443
|
+
execution: { nodes: readonly ExecutionPlanNode[]; layers: readonly (readonly string[])[]; cycleIds: readonly string[] };
|
|
444
|
+
},
|
|
445
|
+
): PlaybookInvocationToolDetails {
|
|
446
|
+
const boundedNodes = boundedExecutionPlanNodes(fields.execution.nodes);
|
|
447
|
+
return {
|
|
448
|
+
schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
|
|
449
|
+
kind: "playbook-invocation",
|
|
450
|
+
operation,
|
|
451
|
+
playbookId: fields.playbookId,
|
|
452
|
+
runId: fields.runId,
|
|
453
|
+
created: {
|
|
454
|
+
docs: boundedStringArray(fields.created.docs),
|
|
455
|
+
rules: boundedStringArray(fields.created.rules),
|
|
456
|
+
tasks: boundedStringArray(fields.created.tasks),
|
|
457
|
+
},
|
|
458
|
+
rootTaskIds: boundedStringArray(fields.rootTaskIds),
|
|
459
|
+
entryTaskId: fields.entryTaskId,
|
|
460
|
+
execution: {
|
|
461
|
+
nodes: boundedNodes,
|
|
462
|
+
layers: boundedLayers(fields.execution.layers),
|
|
463
|
+
cycleIds: boundedStringArray(fields.execution.cycleIds),
|
|
464
|
+
},
|
|
465
|
+
completeness: completeness(fields.execution.nodes.length, boundedNodes.length),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function createPlaybookMissingArgumentsDetails(
|
|
470
|
+
operation: string,
|
|
471
|
+
playbookId: string,
|
|
472
|
+
missingArguments: readonly string[],
|
|
473
|
+
): PlaybookMissingArgumentsToolDetails {
|
|
474
|
+
return {
|
|
475
|
+
schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
|
|
476
|
+
kind: "playbook-missing-arguments",
|
|
477
|
+
operation,
|
|
478
|
+
playbookId,
|
|
479
|
+
missingArguments: boundedStringArray(missingArguments),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function createDiscussionDetails(
|
|
484
|
+
operation: string,
|
|
485
|
+
rounds: readonly DiscussionRoundSummary[],
|
|
486
|
+
discussion?: ArtifactSummarySource,
|
|
487
|
+
): DiscussionToolDetails {
|
|
488
|
+
const boundedRounds = rounds.slice(0, TOOL_DETAILS_MAX_ITEMS).map((round) => ({
|
|
489
|
+
...round,
|
|
490
|
+
content: round.content.slice(0, TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS),
|
|
491
|
+
}));
|
|
492
|
+
return {
|
|
493
|
+
schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
|
|
494
|
+
kind: "discussion",
|
|
495
|
+
operation,
|
|
496
|
+
...(discussion ? { discussion: artifactSummary(discussion) } : {}),
|
|
497
|
+
rounds: boundedRounds,
|
|
498
|
+
completeness: completeness(rounds.length, boundedRounds.length),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function createTaskCompletionDetails(
|
|
503
|
+
operation: string,
|
|
504
|
+
fields: {
|
|
505
|
+
artifact: Artifact;
|
|
506
|
+
gates: readonly TaskGateResultSummary[];
|
|
507
|
+
checklist: readonly TaskChecklistReviewSummary[];
|
|
508
|
+
completed: boolean;
|
|
509
|
+
focused: Artifact | null;
|
|
510
|
+
blocked: readonly { artifact: Artifact; dependencyIds: readonly string[] }[];
|
|
511
|
+
},
|
|
512
|
+
): TaskCompletionToolDetails {
|
|
513
|
+
const boundedGates = fields.gates.slice(0, TOOL_DETAILS_MAX_ITEMS).map((gate) => ({
|
|
514
|
+
...gate,
|
|
515
|
+
output: gate.output.slice(0, TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS),
|
|
516
|
+
}));
|
|
517
|
+
const boundedChecklist = fields.checklist.slice(0, TOOL_DETAILS_MAX_ITEMS);
|
|
518
|
+
const boundedBlocked = fields.blocked.slice(0, TOOL_DETAILS_MAX_ITEMS).map((entry) => ({
|
|
519
|
+
artifact: artifactSummary(entry.artifact),
|
|
520
|
+
dependencyIds: boundedStringArray(entry.dependencyIds),
|
|
521
|
+
}));
|
|
522
|
+
return {
|
|
523
|
+
schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
|
|
524
|
+
kind: "task-completion",
|
|
525
|
+
operation,
|
|
526
|
+
artifact: artifactSummary(fields.artifact),
|
|
527
|
+
gates: boundedGates,
|
|
528
|
+
checklist: boundedChecklist,
|
|
529
|
+
completed: fields.completed,
|
|
530
|
+
...(fields.focused ? { focused: artifactSummary(fields.focused) } : {}),
|
|
531
|
+
blocked: boundedBlocked,
|
|
532
|
+
completeness: completeness(fields.blocked.length, boundedBlocked.length),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function createNoFocusDetails(operation: string): NoFocusToolDetails {
|
|
537
|
+
return { schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA, kind: "no-focus", operation };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function createLeaseDetails(
|
|
541
|
+
operation: string,
|
|
542
|
+
lease: {
|
|
543
|
+
taskName: string;
|
|
544
|
+
taskTitle: string;
|
|
545
|
+
owner: string;
|
|
546
|
+
claimedAt: string;
|
|
547
|
+
leaseExpiresAt: string;
|
|
548
|
+
heartbeatAt?: string;
|
|
549
|
+
note?: string;
|
|
550
|
+
},
|
|
551
|
+
): LeaseToolDetails {
|
|
552
|
+
return {
|
|
553
|
+
schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
|
|
554
|
+
kind: "lease",
|
|
555
|
+
operation,
|
|
556
|
+
taskName: lease.taskName,
|
|
557
|
+
taskTitle: lease.taskTitle,
|
|
558
|
+
owner: lease.owner,
|
|
559
|
+
claimedAt: lease.claimedAt,
|
|
560
|
+
leaseExpiresAt: lease.leaseExpiresAt,
|
|
561
|
+
...(lease.heartbeatAt ? { heartbeatAt: lease.heartbeatAt } : {}),
|
|
562
|
+
...(lease.note ? { note: lease.note.slice(0, TOOL_DETAILS_FIELD_MAX_CHARACTERS) } : {}),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
293
566
|
export function createModelContent(value: string): ModelContent {
|
|
294
567
|
if (value.length <= TOOL_MODEL_CONTENT_MAX_CHARACTERS) {
|
|
295
568
|
return { text: value, truncated: false, omitted: 0 };
|
|
@@ -377,6 +650,54 @@ function isBoundedArray<T>(value: unknown, maximum: number, predicate: (entry: u
|
|
|
377
650
|
return Array.isArray(value) && value.length <= maximum && value.every(predicate);
|
|
378
651
|
}
|
|
379
652
|
|
|
653
|
+
function isExecutionPlanNode(value: unknown): value is ExecutionPlanNode {
|
|
654
|
+
return (
|
|
655
|
+
isRecord(value) &&
|
|
656
|
+
isBoundedString(value.id) &&
|
|
657
|
+
isBoundedString(value.title) &&
|
|
658
|
+
isBoundedString(value.status) &&
|
|
659
|
+
typeof value.active === "boolean" &&
|
|
660
|
+
isBoundedString(value.state) &&
|
|
661
|
+
(value.layer === null || Number.isSafeInteger(value.layer)) &&
|
|
662
|
+
isStringArray(value.prerequisiteIds) &&
|
|
663
|
+
isStringArray(value.successorIds)
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function isLayers(value: unknown): value is string[][] {
|
|
668
|
+
return isBoundedArray(value, TOOL_DETAILS_MAX_ITEMS, (entry): entry is string[] => isStringArray(entry));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function isExecution(value: unknown): value is { nodes: ExecutionPlanNode[]; layers: string[][]; cycleIds: string[] } {
|
|
672
|
+
return (
|
|
673
|
+
isRecord(value) &&
|
|
674
|
+
isBoundedArray(value.nodes, TOOL_DETAILS_MAX_ITEMS, isExecutionPlanNode) &&
|
|
675
|
+
isLayers(value.layers) &&
|
|
676
|
+
isStringArray(value.cycleIds)
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function isDiscussionRoundSummary(value: unknown): value is DiscussionRoundSummary {
|
|
681
|
+
return isRecord(value) && Number.isSafeInteger(value.roundNumber) && isBoundedString(value.actor) && isBoundedString(value.content);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function isTaskGateResultSummary(value: unknown): value is TaskGateResultSummary {
|
|
685
|
+
return isRecord(value) && typeof value.passed === "boolean" && isBoundedString(value.output, TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isTaskChecklistReviewSummary(value: unknown): value is TaskChecklistReviewSummary {
|
|
689
|
+
return (
|
|
690
|
+
isRecord(value) &&
|
|
691
|
+
isBoundedString(value.item) &&
|
|
692
|
+
typeof value.accepted === "boolean" &&
|
|
693
|
+
(value.reason === undefined || isBoundedString(value.reason))
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function isTaskBlockageSummary(value: unknown): value is TaskBlockageSummary {
|
|
698
|
+
return isRecord(value) && isArtifactSummary(value.artifact) && isStringArray(value.dependencyIds);
|
|
699
|
+
}
|
|
700
|
+
|
|
380
701
|
/** Validate renderer details restored from session history before using them as typed presentation state. */
|
|
381
702
|
export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | undefined {
|
|
382
703
|
let serializedLength: number;
|
|
@@ -447,6 +768,59 @@ export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | un
|
|
|
447
768
|
return isBoundedString(value.code) && isBoundedString(value.message, TOOL_DETAILS_BODY_MAX_CHARACTERS)
|
|
448
769
|
? (value as unknown as ErrorToolDetails)
|
|
449
770
|
: undefined;
|
|
771
|
+
case "execution-plan":
|
|
772
|
+
return isBoundedArray(value.nodes, TOOL_DETAILS_MAX_ITEMS, isExecutionPlanNode) &&
|
|
773
|
+
isLayers(value.layers) &&
|
|
774
|
+
isStringArray(value.cycleIds) &&
|
|
775
|
+
isCompleteness(value.completeness)
|
|
776
|
+
? (value as unknown as ExecutionPlanToolDetails)
|
|
777
|
+
: undefined;
|
|
778
|
+
case "playbook-invocation": {
|
|
779
|
+
if (!isRecord(value.created)) return undefined;
|
|
780
|
+
return isBoundedString(value.playbookId) &&
|
|
781
|
+
isBoundedString(value.runId) &&
|
|
782
|
+
isStringArray(value.created.docs) &&
|
|
783
|
+
isStringArray(value.created.rules) &&
|
|
784
|
+
isStringArray(value.created.tasks) &&
|
|
785
|
+
isStringArray(value.rootTaskIds) &&
|
|
786
|
+
isBoundedString(value.entryTaskId) &&
|
|
787
|
+
isExecution(value.execution) &&
|
|
788
|
+
isCompleteness(value.completeness)
|
|
789
|
+
? (value as unknown as PlaybookInvocationToolDetails)
|
|
790
|
+
: undefined;
|
|
791
|
+
}
|
|
792
|
+
case "playbook-missing-arguments":
|
|
793
|
+
return isBoundedString(value.playbookId) && isStringArray(value.missingArguments)
|
|
794
|
+
? (value as unknown as PlaybookMissingArgumentsToolDetails)
|
|
795
|
+
: undefined;
|
|
796
|
+
case "discussion":
|
|
797
|
+
return (value.discussion === undefined || isArtifactSummary(value.discussion)) &&
|
|
798
|
+
isBoundedArray(value.rounds, TOOL_DETAILS_MAX_ITEMS, isDiscussionRoundSummary) &&
|
|
799
|
+
isCompleteness(value.completeness)
|
|
800
|
+
? (value as unknown as DiscussionToolDetails)
|
|
801
|
+
: undefined;
|
|
802
|
+
case "task-completion":
|
|
803
|
+
return isArtifactSummary(value.artifact) &&
|
|
804
|
+
isBoundedArray(value.gates, TOOL_DETAILS_MAX_ITEMS, isTaskGateResultSummary) &&
|
|
805
|
+
isBoundedArray(value.checklist, TOOL_DETAILS_MAX_ITEMS, isTaskChecklistReviewSummary) &&
|
|
806
|
+
typeof value.completed === "boolean" &&
|
|
807
|
+
(value.focused === undefined || isArtifactSummary(value.focused)) &&
|
|
808
|
+
isBoundedArray(value.blocked, TOOL_DETAILS_MAX_ITEMS, isTaskBlockageSummary) &&
|
|
809
|
+
isCompleteness(value.completeness)
|
|
810
|
+
? (value as unknown as TaskCompletionToolDetails)
|
|
811
|
+
: undefined;
|
|
812
|
+
case "no-focus":
|
|
813
|
+
return value as unknown as NoFocusToolDetails;
|
|
814
|
+
case "lease":
|
|
815
|
+
return isBoundedString(value.taskName) &&
|
|
816
|
+
isBoundedString(value.taskTitle) &&
|
|
817
|
+
isBoundedString(value.owner) &&
|
|
818
|
+
isBoundedString(value.claimedAt) &&
|
|
819
|
+
isBoundedString(value.leaseExpiresAt) &&
|
|
820
|
+
(value.heartbeatAt === undefined || isBoundedString(value.heartbeatAt)) &&
|
|
821
|
+
(value.note === undefined || isBoundedString(value.note))
|
|
822
|
+
? (value as unknown as LeaseToolDetails)
|
|
823
|
+
: undefined;
|
|
450
824
|
default:
|
|
451
825
|
return undefined;
|
|
452
826
|
}
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { Artifact } from "@danypops/papyrus";
|
|
16
|
-
import { TOOL_COLLAPSED_ROW_LIMIT } from "@danypops/papyrus";
|
|
17
|
-
import type { VehicleToolRenderers } from "@danypops/vehicle-client-pi";
|
|
16
|
+
import { TOOL_COLLAPSED_ROW_LIMIT, TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS } from "@danypops/papyrus";
|
|
17
|
+
import type { PiVehicleInvocationRequest, PiVehiclePresentationContract, VehicleToolRenderers } from "@danypops/vehicle-client-pi";
|
|
18
18
|
import { renderVehicleCall, renderVehicleResult } from "@danypops/vehicle-client-pi/vehicle-render";
|
|
19
|
-
import type { VehicleOperationDescriptor } from "@danypops/vehicle-core";
|
|
19
|
+
import type { JsonValue, VehicleOperationDescriptor } from "@danypops/vehicle-core";
|
|
20
20
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { type Component, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
22
22
|
import {
|
|
@@ -30,7 +30,20 @@ import {
|
|
|
30
30
|
} from "malevich-tui-components";
|
|
31
31
|
import { ArtifactCard, detailViewTheme, expandHint, measure, statusColor, statusGlyph } from "../tool-rendering/artifact-card.ts";
|
|
32
32
|
import { ArtifactListCard } from "../tool-rendering/artifact-list.ts";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
type ArtifactFocusAnnotation,
|
|
35
|
+
createArtifactDetails,
|
|
36
|
+
createArtifactListDetails,
|
|
37
|
+
createDiscussionDetails,
|
|
38
|
+
createExecutionPlanDetails,
|
|
39
|
+
createLeaseDetails,
|
|
40
|
+
createNoFocusDetails,
|
|
41
|
+
createPlaybookInvocationDetails,
|
|
42
|
+
createPlaybookMissingArgumentsDetails,
|
|
43
|
+
createPreviewDetails,
|
|
44
|
+
createTaskCompletionDetails,
|
|
45
|
+
parsePapyrusToolDetails,
|
|
46
|
+
} from "../tool-rendering/render-model.ts";
|
|
34
47
|
import { recordRenderDiagnostic, shapeFingerprint } from "./render-diagnostics.ts";
|
|
35
48
|
|
|
36
49
|
function isArtifact(value: unknown): value is Artifact {
|
|
@@ -244,6 +257,12 @@ interface DiscussionAndRoundsOutput {
|
|
|
244
257
|
rounds: DiscussionRoundOutput[];
|
|
245
258
|
}
|
|
246
259
|
|
|
260
|
+
/** What renderDiscussionAndRounds actually reads -- satisfied by both a raw Artifact (the live duck-typed output path) and the leaner projected ToolArtifactSummary (the typed-DTO path), with no cast needed at either call site. */
|
|
261
|
+
interface RenderableDiscussionParent {
|
|
262
|
+
title: string;
|
|
263
|
+
status: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
247
266
|
function isDiscussionAndRounds(value: unknown): value is DiscussionAndRoundsOutput {
|
|
248
267
|
if (typeof value !== "object" || value === null) return false;
|
|
249
268
|
const row = value as Record<string, unknown>;
|
|
@@ -277,7 +296,11 @@ function roundsSection(rounds: readonly DiscussionRoundOutput[]): DetailSection
|
|
|
277
296
|
};
|
|
278
297
|
}
|
|
279
298
|
|
|
280
|
-
function renderDiscussionAndRounds(
|
|
299
|
+
function renderDiscussionAndRounds(
|
|
300
|
+
output: { discussion: RenderableDiscussionParent; rounds: readonly DiscussionRoundOutput[] },
|
|
301
|
+
theme: Theme,
|
|
302
|
+
expanded: boolean,
|
|
303
|
+
): Component {
|
|
281
304
|
const discussion = output.discussion;
|
|
282
305
|
return statelessComponent((width) => {
|
|
283
306
|
const safeWidth = Math.max(1, width);
|
|
@@ -331,6 +354,16 @@ interface TaskCompletionOutput {
|
|
|
331
354
|
blocked: TaskBlockageOutput[];
|
|
332
355
|
}
|
|
333
356
|
|
|
357
|
+
/** What renderTaskCompletion actually reads -- satisfied by both the raw duck-typed TaskCompletionOutput and the leaner projected TaskCompletionToolDetails. */
|
|
358
|
+
interface RenderableTaskCompletion {
|
|
359
|
+
artifact: RenderableDiscussionParent;
|
|
360
|
+
gates: readonly { passed: boolean; output: string }[];
|
|
361
|
+
checklist: readonly TaskChecklistReviewOutput[];
|
|
362
|
+
completed: boolean;
|
|
363
|
+
focused?: RenderableDiscussionParent | null;
|
|
364
|
+
blocked: readonly { artifact: RenderableDiscussionParent; dependencyIds: readonly string[] }[];
|
|
365
|
+
}
|
|
366
|
+
|
|
334
367
|
function isTaskCompletion(value: unknown): value is TaskCompletionOutput {
|
|
335
368
|
if (typeof value !== "object" || value === null) return false;
|
|
336
369
|
const row = value as Record<string, unknown>;
|
|
@@ -344,7 +377,7 @@ function isTaskCompletion(value: unknown): value is TaskCompletionOutput {
|
|
|
344
377
|
);
|
|
345
378
|
}
|
|
346
379
|
|
|
347
|
-
function renderTaskCompletion(result:
|
|
380
|
+
function renderTaskCompletion(result: RenderableTaskCompletion, theme: Theme, expanded: boolean): Component {
|
|
348
381
|
const task = result.artifact;
|
|
349
382
|
return statelessComponent((width) => {
|
|
350
383
|
const safeWidth = Math.max(1, width);
|
|
@@ -384,6 +417,69 @@ function renderTaskCompletion(result: TaskCompletionOutput, theme: Theme, expand
|
|
|
384
417
|
});
|
|
385
418
|
}
|
|
386
419
|
|
|
420
|
+
/** tasks.claim/heartbeat_lease/release_lease/lease's own name-first view. Detected the same
|
|
421
|
+
* name-independent, shape-based way as the others in this file. */
|
|
422
|
+
interface TaskLeaseViewOutput {
|
|
423
|
+
taskName: string;
|
|
424
|
+
taskTitle: string;
|
|
425
|
+
owner: string;
|
|
426
|
+
token: string;
|
|
427
|
+
claimedAt: string;
|
|
428
|
+
leaseExpiresAt: string;
|
|
429
|
+
heartbeatAt?: string;
|
|
430
|
+
note?: string;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function isTaskLeaseView(value: unknown): value is TaskLeaseViewOutput {
|
|
434
|
+
if (typeof value !== "object" || value === null) return false;
|
|
435
|
+
const row = value as Record<string, unknown>;
|
|
436
|
+
return (
|
|
437
|
+
typeof row.taskName === "string" &&
|
|
438
|
+
typeof row.taskTitle === "string" &&
|
|
439
|
+
typeof row.owner === "string" &&
|
|
440
|
+
typeof row.token === "string" &&
|
|
441
|
+
typeof row.claimedAt === "string" &&
|
|
442
|
+
typeof row.leaseExpiresAt === "string" &&
|
|
443
|
+
(row.heartbeatAt === undefined || typeof row.heartbeatAt === "string") &&
|
|
444
|
+
(row.note === undefined || typeof row.note === "string")
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Renders a lease's own safe fields -- never the raw token, which the model channel (not this persisted, human-facing one) carries for a later heartbeat/release call. */
|
|
449
|
+
function renderLease(
|
|
450
|
+
lease: {
|
|
451
|
+
taskName: string;
|
|
452
|
+
taskTitle: string;
|
|
453
|
+
owner: string;
|
|
454
|
+
claimedAt: string;
|
|
455
|
+
leaseExpiresAt: string;
|
|
456
|
+
heartbeatAt?: string;
|
|
457
|
+
note?: string;
|
|
458
|
+
},
|
|
459
|
+
theme: Theme,
|
|
460
|
+
): Component {
|
|
461
|
+
return statelessComponent((width) => {
|
|
462
|
+
const fields: DetailField[] = [
|
|
463
|
+
{ label: "Task", value: `${lease.taskName} \u2014 ${lease.taskTitle}` },
|
|
464
|
+
{ label: "Owner", value: lease.owner },
|
|
465
|
+
{ label: "Expires", value: lease.leaseExpiresAt },
|
|
466
|
+
...(lease.heartbeatAt ? [{ label: "Last heartbeat", value: lease.heartbeatAt }] : []),
|
|
467
|
+
...(lease.note ? [{ label: "Note", value: lease.note }] : []),
|
|
468
|
+
];
|
|
469
|
+
return buildDetailLines(Math.max(1, width), { fields, alignFields: true, theme: detailViewTheme(theme), measure });
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Deliberately does not catch: a value JSON.stringify can't serialize (e.g. a circular
|
|
474
|
+
* reference) has no safe textual fallback, so this propagates and the projector's own caller
|
|
475
|
+
* (invokeVehicleOperation) fails the whole call closed rather than persisting a placeholder --
|
|
476
|
+
* the same "never silently substitute raw/unsafe output" contract every other projection
|
|
477
|
+
* failure already carries. */
|
|
478
|
+
function boundedJsonPreview(value: unknown): string {
|
|
479
|
+
const text = JSON.stringify(value, null, 2);
|
|
480
|
+
return typeof text === "string" ? text : String(value);
|
|
481
|
+
}
|
|
482
|
+
|
|
387
483
|
export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor): VehicleToolRenderers {
|
|
388
484
|
return {
|
|
389
485
|
// Pure pass-through to the generic renderer -- the only reason this exists at all is
|
|
@@ -455,3 +551,105 @@ export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor):
|
|
|
455
551
|
},
|
|
456
552
|
};
|
|
457
553
|
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Projects a raw Papyrus operation output into a bounded, versioned PapyrusToolDetails DTO
|
|
557
|
+
* before Pi ever persists it -- the seam papyrusVehicleRenderers' own renderResult never had
|
|
558
|
+
* (it only converts shape at render time, from whatever the legacy {vehicle, output} path
|
|
559
|
+
* already persisted verbatim, lease tokens and all). Every branch here mirrors the same
|
|
560
|
+
* shape-detection papyrusVehicleRenderers's own renderResult uses, so the two stay in lockstep;
|
|
561
|
+
* anything genuinely unmatched still becomes a real, bounded PreviewToolDetails rather than an
|
|
562
|
+
* unprojected raw passthrough -- the one requirement this whole seam exists to satisfy.
|
|
563
|
+
*/
|
|
564
|
+
function projectPapyrusPresentation(descriptor: VehicleOperationDescriptor, output: unknown): JsonValue {
|
|
565
|
+
if (isArtifactArray(output)) return createArtifactListDetails(descriptor.name, output) as unknown as JsonValue;
|
|
566
|
+
if (isArtifact(output)) return createArtifactDetails(descriptor.name, output) as unknown as JsonValue;
|
|
567
|
+
if (isTaskFocus(output)) return createArtifactDetails(descriptor.name, output.artifact, focusAnnotation(output)) as unknown as JsonValue;
|
|
568
|
+
if (output === null && descriptor.name === "tasks.focused") return createNoFocusDetails(descriptor.name) as unknown as JsonValue;
|
|
569
|
+
if (isTaskExecutionPlan(output))
|
|
570
|
+
return createExecutionPlanDetails(descriptor.name, output.nodes, output.layers, output.cycleIds) as unknown as JsonValue;
|
|
571
|
+
if (isPlaybookInvocationResult(output)) {
|
|
572
|
+
return createPlaybookInvocationDetails(descriptor.name, {
|
|
573
|
+
playbookId: output.playbookId,
|
|
574
|
+
runId: output.runId,
|
|
575
|
+
created: output.created,
|
|
576
|
+
rootTaskIds: output.rootTaskIds,
|
|
577
|
+
entryTaskId: output.entryTaskId,
|
|
578
|
+
execution: output.execution,
|
|
579
|
+
}) as unknown as JsonValue;
|
|
580
|
+
}
|
|
581
|
+
if (isPlaybookMissingArguments(output)) {
|
|
582
|
+
return createPlaybookMissingArgumentsDetails(descriptor.name, output.playbookId, output.missingArguments) as unknown as JsonValue;
|
|
583
|
+
}
|
|
584
|
+
if (isDiscussionAndRounds(output))
|
|
585
|
+
return createDiscussionDetails(descriptor.name, output.rounds, output.discussion) as unknown as JsonValue;
|
|
586
|
+
if (isDiscussionRoundsOnly(output)) return createDiscussionDetails(descriptor.name, output.rounds) as unknown as JsonValue;
|
|
587
|
+
if (isDiscussionListOutput(output)) return createArtifactListDetails(descriptor.name, output.discussions) as unknown as JsonValue;
|
|
588
|
+
if (isTaskCompletion(output)) return createTaskCompletionDetails(descriptor.name, output) as unknown as JsonValue;
|
|
589
|
+
if (isTaskLeaseView(output)) return createLeaseDetails(descriptor.name, output) as unknown as JsonValue;
|
|
590
|
+
return createPreviewDetails(descriptor.name, descriptor.name, boundedJsonPreview(output)) as unknown as JsonValue;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function renderFromPapyrusPresentation(
|
|
594
|
+
presentation: NonNullable<ReturnType<typeof parsePapyrusToolDetails>>,
|
|
595
|
+
theme: Theme,
|
|
596
|
+
expanded: boolean,
|
|
597
|
+
): Component {
|
|
598
|
+
switch (presentation.kind) {
|
|
599
|
+
case "artifact-list":
|
|
600
|
+
return new ArtifactListCard(presentation, theme, expanded);
|
|
601
|
+
case "artifact":
|
|
602
|
+
return new ArtifactCard(presentation, theme, expanded);
|
|
603
|
+
case "no-focus":
|
|
604
|
+
return renderNoFocusedTask(theme);
|
|
605
|
+
case "execution-plan":
|
|
606
|
+
return renderTaskExecutionPlan(presentation, theme, expanded);
|
|
607
|
+
case "playbook-invocation":
|
|
608
|
+
return renderPlaybookInvocationResult(presentation, theme, expanded);
|
|
609
|
+
case "playbook-missing-arguments":
|
|
610
|
+
return renderPlaybookMissingArguments(presentation, theme);
|
|
611
|
+
case "discussion":
|
|
612
|
+
return presentation.discussion
|
|
613
|
+
? renderDiscussionAndRounds({ discussion: presentation.discussion, rounds: presentation.rounds }, theme, expanded)
|
|
614
|
+
: renderDiscussionRoundsOnly(presentation, theme);
|
|
615
|
+
case "task-completion":
|
|
616
|
+
return renderTaskCompletion(presentation, theme, expanded);
|
|
617
|
+
case "lease":
|
|
618
|
+
return renderLease(presentation, theme);
|
|
619
|
+
case "preview":
|
|
620
|
+
return new Text(theme.fg("toolOutput", presentation.content), 0, 0);
|
|
621
|
+
case "transition":
|
|
622
|
+
case "graph":
|
|
623
|
+
case "gate-run":
|
|
624
|
+
case "invocation":
|
|
625
|
+
case "error":
|
|
626
|
+
// Reachable only if a future caller starts producing these kinds through this seam
|
|
627
|
+
// (today's Papyrus Vehicle outputs never do) -- a bounded JSON preview is still a
|
|
628
|
+
// real, safe rendering rather than a crash.
|
|
629
|
+
return new Text(theme.fg("toolOutput", boundedJsonPreview(presentation)), 0, 0);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Pairs the projector above with a renderResult that reads the already-projected,
|
|
635
|
+
* already-bounded `details.presentation` DTO instead of raw `details.output` -- the seam
|
|
636
|
+
* pi-papyrus task "project typed bounded render details before Vehicle persists" exists for.
|
|
637
|
+
* Falls back to papyrusVehicleRenderers' own renderResult (which still reads `details.output`)
|
|
638
|
+
* for a partial/progress update, an error result, or a historical session row persisted before
|
|
639
|
+
* this seam existed -- both keep working exactly as before, unchanged.
|
|
640
|
+
*/
|
|
641
|
+
export function papyrusVehiclePresentations(descriptor: VehicleOperationDescriptor): PiVehiclePresentationContract {
|
|
642
|
+
return {
|
|
643
|
+
projector: {
|
|
644
|
+
maxBytes: TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS,
|
|
645
|
+
project: (output: unknown, _request: PiVehicleInvocationRequest) => projectPapyrusPresentation(descriptor, output),
|
|
646
|
+
},
|
|
647
|
+
renderResult(result, options, theme, context) {
|
|
648
|
+
if (!options.isPartial && !context.isError) {
|
|
649
|
+
const presentation = parsePapyrusToolDetails((result.details as { presentation?: unknown } | undefined)?.presentation);
|
|
650
|
+
if (presentation) return renderFromPapyrusPresentation(presentation, theme, options.expanded);
|
|
651
|
+
}
|
|
652
|
+
return papyrusVehicleRenderers(descriptor).renderResult!(result, options, theme, context);
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
}
|
|
@@ -32,7 +32,7 @@ import { currentVehicleClientTarget } from "../service-client.ts";
|
|
|
32
32
|
import { sessionSecretField } from "../session-identity.ts";
|
|
33
33
|
import { emitTaskFocusEvent } from "../task/task-focus-events.ts";
|
|
34
34
|
import { recordRenderDiagnostic, shapeFingerprint } from "./render-diagnostics.ts";
|
|
35
|
-
import { papyrusVehicleRenderers } from "./vehicle-artifact-renderers.ts";
|
|
35
|
+
import { papyrusVehiclePresentations, papyrusVehicleRenderers } from "./vehicle-artifact-renderers.ts";
|
|
36
36
|
|
|
37
37
|
const REGISTERED_PERMISSIONS = [
|
|
38
38
|
"notes:read",
|
|
@@ -135,6 +135,12 @@ export function registerNotesVehicle(pi: ExtensionAPI): Promise<RegisteredPiVehi
|
|
|
135
135
|
permissions: REGISTERED_PERMISSIONS,
|
|
136
136
|
principal: { id: "pi-papyrus" },
|
|
137
137
|
renderers: papyrusVehicleRenderers,
|
|
138
|
+
// papyrusVehiclePresentations projects a bounded, versioned PapyrusToolDetails DTO
|
|
139
|
+
// before Pi persists this call's details -- renderers above still supplies renderCall
|
|
140
|
+
// (createTool sources renderCall/renderResult independently; presentations' own
|
|
141
|
+
// renderResult takes priority over renderers' renderResult, and falls back to it for a
|
|
142
|
+
// partial/error result or a historical session row persisted before this seam existed).
|
|
143
|
+
presentations: papyrusVehiclePresentations,
|
|
138
144
|
shell: { coreOperations: CORE_OPERATIONS },
|
|
139
145
|
// playbooks.invoke's own module handler, and tasks.focus/pause/unpause/clear_focus's
|
|
140
146
|
// own module handlers, authorize an internal Task Focus write via
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@danypops/jittor": "^0.14.0",
|
|
21
21
|
"@danypops/papyrus": "^0.47.0",
|
|
22
22
|
"@danypops/vehicle-client": "^0.7.0",
|
|
23
|
-
"@danypops/vehicle-client-pi": "^0.22.
|
|
23
|
+
"@danypops/vehicle-client-pi": "^0.22.1",
|
|
24
24
|
"@danypops/vehicle-core": "^0.12.3",
|
|
25
25
|
"@danypops/vehicle-server": "^0.18.1",
|
|
26
26
|
"beautiful-mermaid": "1.1.3",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@danypops/pi-tui-harness": "^0.0.2",
|
|
31
|
+
"@danypops/vehicle-conformance": "^0.3.0",
|
|
31
32
|
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
32
33
|
"bun-types": "latest",
|
|
33
34
|
"typebox": "^1.3.6",
|