@llblab/pi-actors 0.56.1 → 0.57.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.
@@ -84,6 +84,69 @@ export function renderYamlMetadata(value: unknown, byteBudget: number): YamlMeta
84
84
  }
85
85
  }
86
86
 
87
+ export type YamlPart = { full: Record<string, unknown>; selected: Record<string, unknown> };
88
+
89
+ /** One selected metadata node, kept whole or covered by a field/group omission. */
90
+ export function selectYamlNode(key: string, value: unknown, byteBudget: number, total = 1): YamlPart {
91
+ const full = { [key]: value };
92
+ const complete = renderYamlMetadata(full, byteBudget);
93
+ const invalid = complete.state === "unavailable" && complete.reason === "metadata_invalid";
94
+ const omitted = { [key]: invalid ? "[unavailable]" : "[omitted]",
95
+ [key + "_omission"]: { reason: invalid ? "invalid_selected_metadata" : "response_too_large", shown: 0, omitted: total, total } };
96
+ const fallback = renderYamlMetadata(omitted, byteBudget);
97
+ // Seed the shorter complete-or-omitted form: omission scaffolding must never manufacture overflow.
98
+ const selected = complete.state === "available" && fallback.state === "available" &&
99
+ Buffer.byteLength(complete.text) <= Buffer.byteLength(fallback.text) ? full : omitted;
100
+ return { full: invalid ? omitted : full, selected };
101
+ }
102
+
103
+ /** Opaque selected text or string sequence; malformed selections stay unavailable without repair. */
104
+ export function selectYamlField(key: string, value: string | readonly string[], byteBudget: number): YamlPart {
105
+ const values = typeof value === "string" ? [value] : value;
106
+ if (values.every(item => item.isWellFormed()))
107
+ return selectYamlNode(key, typeof value === "string" ? value : [...value], byteBudget, typeof value === "string" ? 1 : value.length);
108
+ const total = typeof value === "string" ? 1 : value.length;
109
+ const omitted = { [key]: "[unavailable]",
110
+ [key + "_omission"]: { reason: "malformed_selected_text", shown: 0, omitted: total, total } };
111
+ return { full: omitted, selected: omitted };
112
+ }
113
+
114
+ /** Grow whole selected fields in priority order while the complete document still fits. */
115
+ export function growYamlDocument(
116
+ render: () => Record<string, unknown>,
117
+ order: readonly YamlPart[],
118
+ byteBudget: number,
119
+ ): string {
120
+ const fits = () => renderYamlMetadata(render(), byteBudget).state === "available";
121
+ for (const part of order) {
122
+ if (part.selected === part.full) continue;
123
+ const previous = part.selected;
124
+ part.selected = part.full;
125
+ if (!fits()) part.selected = previous;
126
+ }
127
+ const rendered = renderYamlMetadata(render(), byteBudget);
128
+ if (rendered.state !== "available")
129
+ throw new Error("Selected YAML metadata document is unavailable: " + rendered.reason);
130
+ return rendered.text;
131
+ }
132
+
133
+ /** Uncapped UI-only flat metadata; never borrow a model-output allowance for notices. */
134
+ export function formatYamlNotice(value: Record<string, string | number | boolean>): string {
135
+ const selected: Record<string, unknown> = {};
136
+ for (const [key, field] of Object.entries(value)) {
137
+ if (typeof field === "string" && !field.isWellFormed()) {
138
+ selected[key] = "[unavailable]";
139
+ selected[key + "_omission"] = { reason: "malformed_selected_text", shown: 0, omitted: 1, total: 1 };
140
+ } else selected[key] = field;
141
+ }
142
+ return yamlMetadataText(selected).replace(/\n$/u, "");
143
+ }
144
+
145
+ /** Close one rendered YAML header; a separate opaque body gets exactly one delimiter pair. */
146
+ export function frameMetadataDocument(header: string, body?: string): string {
147
+ return body === undefined ? header : `---\n${header}---\n${body}`;
148
+ }
149
+
87
150
  export function formatMarkdownField(key: string, value: string, depth = 0): string {
88
151
  const indent = " ".repeat(depth);
89
152
  return /[\r\n]/u.test(value)
@@ -463,6 +526,28 @@ export function recipeRegistryNextActions(
463
526
  return [...new Set(actions)].slice(0, 4);
464
527
  }
465
528
 
529
+ /** Compact metadata-only document text: one leading LF, no trailing LF, shared emitter budget. */
530
+ export function compactYamlDocument(value: Record<string, unknown>): string {
531
+ const text = growYamlDocument(() => value, [], Limits.RUN_DELIVERY_MODEL_MAX_BYTES);
532
+ return `\n${text.replace(/\n$/u, "")}`;
533
+ }
534
+
535
+ /** Only the closed read-only Recipe inspect literals become typed next; other advice stays opaque content. */
536
+ export function nextActionMetadata(action: string, byteBudget: number): YamlPart {
537
+ const recognized = [
538
+ ["inspect target=recipes view=doctor", "doctor", false],
539
+ ["inspect target=recipes view=imports verbose=true", "imports", true],
540
+ ["inspect target=recipes view=summary verbose=true", "summary", true],
541
+ ] as const;
542
+ const match = recognized.find(([literal]) => literal === action);
543
+ if (match) {
544
+ const typed = { next: { tool: "inspect", target: "recipes", view: match[1], ...(match[2] ? { verbose: true } : {}) } };
545
+ return { full: typed, selected: typed };
546
+ }
547
+ const instruction = selectYamlField("instruction", action, byteBudget);
548
+ return { full: { next: instruction.full }, selected: { next: instruction.selected } };
549
+ }
550
+
466
551
  /** Only these existing read-only inspection hints become action nodes; other advice stays content. */
467
552
  export function formatInspectionHint(action: string): string {
468
553
  if (action === "inspect target=recipes view=doctor") return formatInspectNextAction("recipes", "doctor");
@@ -474,21 +559,11 @@ export function formatInspectionHint(action: string): string {
474
559
  export function compactRecipeRegistry(
475
560
  summary: Record<string, unknown>,
476
561
  ): string {
477
- const active = Array.isArray(summary.active) ? summary.active.length : 0;
478
- const shadowed = Array.isArray(summary.shadowed)
479
- ? summary.shadowed.length
480
- : 0;
481
- const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
482
- const disabled = Array.isArray(summary.disabled)
483
- ? summary.disabled.length
484
- : 0;
485
- const diagnostics = Array.isArray(summary.diagnostics)
486
- ? summary.diagnostics.length
487
- : 0;
488
- const drafts = Array.isArray(summary.drafts) ? summary.drafts.length : 0;
489
- const recommendations = Array.isArray(summary.recommendations)
490
- ? summary.recommendations.length
491
- : 0;
562
+ const count = (key: string, source: unknown) => Array.isArray(source) ? source.length : 0;
563
+ const active = count("active", summary.active), shadowed = count("shadowed", summary.shadowed);
564
+ const invalid = count("invalid", summary.invalid), disabled = count("disabled", summary.disabled);
565
+ const diagnostics = count("diagnostics", summary.diagnostics), drafts = count("drafts", summary.drafts);
566
+ const recommendations = count("recommendations", summary.recommendations);
492
567
  const skillCatalogPartial = summary.skill_recipe_catalog_partial === true;
493
568
  const currentPolicy = Array.isArray(summary.active)
494
569
  ? (summary.active as Array<Record<string, unknown>>).filter(
@@ -498,11 +573,13 @@ export function compactRecipeRegistry(
498
573
  const nextActions = Array.isArray(summary.next_actions)
499
574
  ? (summary.next_actions as string[])
500
575
  : [];
501
- const fields = [formatMarkdownField("recipes", "registry"), ...Object.entries({ active, drafts, shadowed, invalid, disabled,
502
- current_policy: currentPolicy, skill_catalog_partial: skillCatalogPartial, recommendations, diagnostics })
503
- .map(([key, value]) => formatMarkdownField(key, String(value)))];
504
- if (nextActions.length) fields.push(formatInspectionHint(nextActions[0]));
505
- return "\n" + fields.join("\n");
576
+ const hint = nextActions.length ? nextActionMetadata(nextActions[0]!, Limits.RUN_DELIVERY_MODEL_MAX_BYTES) : undefined;
577
+ const render = (): Record<string, unknown> => ({ recipes: "registry", active, drafts, shadowed, invalid, disabled,
578
+ current_policy: currentPolicy, skill_catalog_partial: skillCatalogPartial, recommendations, diagnostics,
579
+ ...(hint ? hint.selected : {}) });
580
+ return hint
581
+ ? `\n${growYamlDocument(render, [hint], Limits.RUN_DELIVERY_MODEL_MAX_BYTES).replace(/\n$/u, "")}`
582
+ : compactYamlDocument(render());
506
583
  }
507
584
 
508
585
  export const DEFAULT_INSPECT_LINES = Limits.DEFAULT_INSPECT_LINES;
@@ -84,21 +84,28 @@ function writeSpawnDraftRecipe(
84
84
 
85
85
  /** Compiler-owned pre-launch diagnostics only; selected caller values are not admitted Run ids. */
86
86
  function spawnGuardText(kind: "state_dir" | "namespace" | "id", value?: string): string {
87
- const field = ToolsResponse.formatMarkdownField;
88
87
  const facts = kind === "state_dir"
89
- ? [field("result", "unsupported field"), field("reason", "Run state is runtime-owned; run:<id> remains addressable and retention-safe.")]
90
- : [field("result", kind === "namespace" ? "unsupported Run target" : "invalid Run target"), field("accepted_format", "run:<id>"),
91
- ...(kind === "id" ? [field("id_rule", "nonempty ASCII letters, digits, underscore, dot or hyphen")] : [])];
92
- const render = (selected?: string) => ToolsResponse.withLeadingLineBreak(
93
- [field("field", kind === "state_dir" ? "state_dir" : "as"), ...(selected === undefined ? [] : [selected]), ...facts].join("\n"));
94
- if (value === undefined) return render();
95
- const wellFormed = value.isWellFormed();
96
- if (wellFormed) {
97
- const complete = render(field("value", value));
98
- if (Buffer.byteLength(complete) <= Limits.RUN_DELIVERY_MODEL_MAX_BYTES) return complete;
88
+ ? { result: "unsupported field", reason: "Run state is runtime-owned; run:<id> remains addressable and retention-safe." }
89
+ : { result: kind === "namespace" ? "unsupported Run target" : "invalid Run target", accepted_format: "run:<id>",
90
+ ...(kind === "id" ? { id_rule: "nonempty ASCII letters, digits, underscore, dot or hyphen" } : {}) };
91
+ // Measure the shared emitter's own projection: an unfit selected value must become an omission, not a clip.
92
+ const render = (selected?: Record<string, unknown>): string | undefined => {
93
+ const rendered = ToolsResponse.renderYamlMetadata({ field: kind === "state_dir" ? "state_dir" : "as", ...(selected ?? {}), ...facts },
94
+ Limits.RUN_DELIVERY_MODEL_MAX_BYTES);
95
+ return rendered.state === "available"
96
+ ? ToolsResponse.withLeadingLineBreak(ToolsResponse.frameMetadataDocument(rendered.text))
97
+ : undefined;
98
+ };
99
+ const required = (selected?: Record<string, unknown>) => render(selected) ?? render()!;
100
+ const wellFormed = value !== undefined && value.isWellFormed();
101
+ if (value !== undefined && wellFormed) {
102
+ const complete = render({ value });
103
+ if (complete !== undefined && Buffer.byteLength(complete) <= Limits.RUN_DELIVERY_MODEL_MAX_BYTES) return complete;
99
104
  }
105
+ if (value === undefined) return required();
100
106
  const reason = wellFormed ? "response_too_large" : "malformed_selected_text";
101
- return render(`- value: [${reason === "response_too_large" ? "omitted" : "unavailable"}]\n- value_omission:\n - reason: ${reason}\n - shown: 0\n - omitted: 1\n - total: 1`);
107
+ return required({ value: reason === "response_too_large" ? "[omitted]" : "[unavailable]",
108
+ value_omission: { reason, shown: 0, omitted: 1, total: 1 } });
102
109
  }
103
110
 
104
111
  function runIdFromTarget(target: string | undefined): string | undefined {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.56.1",
3
+ "version": "0.57.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -6,9 +6,12 @@ Preserve the intended logical identity and diagnose through public pi-actors sur
6
6
 
7
7
  ```text
8
8
  inspect target=recipes view=status
9
+ inspect target=recipes view=summary
9
10
  inspect target=recipes view=doctor identity=<skill>/<recipe>
10
11
  ```
11
12
 
13
+ `summary` and `registry` name the same inspection path: identical facts, details and rendering for identical `verbose` input, so only `verbose` selects compact text versus structured output. The compact label still reports `recipes: registry`, and that label is not a separate view to request. Unsupported-view refusals list the actual accepted view names.
14
+
12
15
  Compact Recipe/Run/tool replies use flat AX fields without changing their existing evidence or authorization. Required/optional tool arguments are sequences, with none distinct from unknown. Focused source_location remains one exact literal value. Run compact replies still identify only run/view: request verbose for structured evidence, and tool schema remains structured even without verbose. Compact text selects at most one existing hint; only exact read-only recipes doctor/imports/summary inspection literals become typed next nodes. Other advice stays instruction content with its original Markdown, never executable metadata or authority to rerun work/register a tool. The full original next_actions array remains in details; do not infer callability from an advisory string.
13
16
 
14
17
  Use the focused doctor form for one intended identity. It reports active-Skill ownership, exact resolvability, partial-catalog state, component status, portable source location, resolution generation, any rejection, and bounded next actions. Use the unfiltered doctor only for catalog-wide diagnosis. A partial catalog does not imply every exact component is unavailable. If the owning Skill is inactive, report that blocker rather than locating and running its helper manually.