@llblab/pi-actors 0.56.0 → 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.
Files changed (38) hide show
  1. package/AGENTS.md +9 -9
  2. package/BACKLOG.md +4 -23
  3. package/CHANGELOG.md +13 -0
  4. package/README.md +5 -2
  5. package/dist/lib/execution-output.js +13 -2
  6. package/dist/lib/observability.js +9 -14
  7. package/dist/lib/registry.js +132 -157
  8. package/dist/lib/run-ui-runtime.js +18 -27
  9. package/dist/lib/runs-process-windows-mi.d.ts +71 -0
  10. package/dist/lib/runs-process-windows-mi.js +146 -3
  11. package/dist/lib/tools-inspect.js +109 -59
  12. package/dist/lib/tools-local.js +16 -8
  13. package/dist/lib/tools-message.js +76 -65
  14. package/dist/lib/tools-response.d.ts +18 -0
  15. package/dist/lib/tools-response.js +88 -21
  16. package/dist/lib/tools-spawn.js +19 -12
  17. package/dist/skills/actors/SKILL.md +3 -3
  18. package/dist/skills/actors/references/diagnostics.md +3 -0
  19. package/docs/README.md +1 -0
  20. package/docs/actor-inspector.md +2 -2
  21. package/docs/agent-experience.md +9 -1
  22. package/docs/inactive-foundations-inventory.md +73 -0
  23. package/docs/inspection.md +9 -8
  24. package/docs/output-projection-map.md +16 -14
  25. package/docs/windows-process-observation.md +2 -2
  26. package/lib/execution-output.ts +13 -2
  27. package/lib/observability.ts +9 -11
  28. package/lib/registry.ts +138 -164
  29. package/lib/run-ui-runtime.ts +18 -34
  30. package/lib/runs-process-windows-mi.ts +167 -3
  31. package/lib/tools-inspect.ts +109 -59
  32. package/lib/tools-local.ts +26 -15
  33. package/lib/tools-message.ts +85 -58
  34. package/lib/tools-response.ts +97 -20
  35. package/lib/tools-spawn.ts +19 -12
  36. package/package.json +1 -1
  37. package/skills/actors/SKILL.md +3 -3
  38. package/skills/actors/references/diagnostics.md +3 -0
@@ -17,7 +17,54 @@ function asRecord(value: unknown): Record<string, unknown> {
17
17
  : {};
18
18
  }
19
19
 
20
- const controlField = ToolsResponse.formatMarkdownField;
20
+ const CONTROL_BUDGET = Limits.RUN_DELIVERY_MODEL_MAX_BYTES - 1;
21
+
22
+ type ControlDocument = Record<string, unknown>;
23
+ type ControlPart = { full: ControlDocument; selected: ControlDocument };
24
+
25
+ function controlMetadata(value: ControlDocument): string {
26
+ return (ToolsResponse.renderYamlMetadata(value, CONTROL_BUDGET) as
27
+ Extract<ToolsResponse.YamlMetadataRendering, { state: "available" }>).text;
28
+ }
29
+
30
+ /** One opaque selected scalar as a whole value or an explicit whole-field omission, never a clip. */
31
+ function controlSelected(key: string, value: string): ControlPart {
32
+ return ToolsResponse.selectYamlField(key, value, CONTROL_BUDGET);
33
+ }
34
+
35
+ /** Compiler-selected option strings stay a native sequence; malformed selections stay unavailable. */
36
+ function controlChoices(options: { key: "supported_actions" | "available_actions"; values: readonly string[] }): ControlPart {
37
+ const key = options.key, values = options.values;
38
+ // Values were selected by the original string filter, not a new action grammar.
39
+ const wellFormed = values.every((value) => typeof value === "string" && value.isWellFormed());
40
+ const omitted: ControlDocument = { [key]: wellFormed ? "[omitted]" : "[unavailable]",
41
+ [key + "_omission"]: { reason: wellFormed ? "response_too_large" : "malformed_selected_text",
42
+ shown: 0, omitted: values.length, total: values.length } };
43
+ const full: ControlDocument = !wellFormed ? omitted : values.length ? { [key]: [...values] } : { [key]: "none" };
44
+ const complete = ToolsResponse.renderYamlMetadata(full, CONTROL_BUDGET);
45
+ return { full, selected: complete.state === "available" && Buffer.byteLength(complete.text) <= Buffer.byteLength(controlMetadata(omitted)) ? full : omitted };
46
+ }
47
+
48
+ /** Grow whole selected fields in priority order while the complete document still fits. */
49
+ function controlDocument(render: () => ControlDocument, order: readonly ControlPart[]): string {
50
+ return ToolsResponse.growYamlDocument(render, order, CONTROL_BUDGET);
51
+ }
52
+
53
+ /** Only producer-known read-only inspect literals become typed next; other advice stays opaque content. */
54
+ function controlNextPart(action: string): ControlPart {
55
+ const inspect = /^inspect target=([^\s]+) view=([^\s]+)$/u.exec(action);
56
+ if (inspect) {
57
+ return ToolsResponse.selectYamlNode("next", { tool: "inspect", target: inspect[1], view: inspect[2] }, CONTROL_BUDGET);
58
+ }
59
+ const wellFormed = action.isWellFormed();
60
+ const omitted: ControlDocument = { next: { instruction: wellFormed ? "[omitted]" : "[unavailable]",
61
+ instruction_omission: { reason: wellFormed ? "response_too_large" : "malformed_selected_text", shown: 0, omitted: 1, total: 1 } } };
62
+ const full: ControlDocument = wellFormed ? { next: { instruction: action } } : omitted;
63
+ const complete = wellFormed ? ToolsResponse.renderYamlMetadata(full, CONTROL_BUDGET) : undefined;
64
+ const selected = complete?.state === "available" && Buffer.byteLength(complete.text) <= Buffer.byteLength(controlMetadata(omitted))
65
+ ? full : omitted;
66
+ return { full, selected };
67
+ }
21
68
 
22
69
  /** Only the four compiler-owned guard sites below; never wraps dispatch/dependency errors. */
23
70
  function controlGuardText(
@@ -26,37 +73,12 @@ function controlGuardText(
26
73
  options?: { key: "supported_actions" | "available_actions"; values: readonly string[] },
27
74
  ): string {
28
75
  const key = request.target === "runtime" ? "target" : "run";
29
- const identity = controlField(key, request.target === "runtime" ? request.target : request.target.slice(4));
30
- const omittedIdentity = `- ${key}: [omitted]\n- ${key}_omission:\n - reason: response_too_large\n - shown: 0\n - omitted: 1\n - total: 1`;
31
- const fixed = [controlField("action", request.action), controlField("result", result)];
32
- let fullOptions = "", omittedOptions = "";
33
- if (options) {
34
- const rows: string[] = [];
35
- let malformed = false;
36
- // Values were selected by the original string filter, not by a new action grammar.
37
- for (let index = 0; index < options.values.length; index++) {
38
- const value = options.values[index];
39
- if (!value.isWellFormed()) { malformed = true; break; }
40
- rows.push(" - " + value.replaceAll(/\r\n|\r|\n/gu, newline => newline + " "));
41
- }
42
- omittedOptions = `- ${options.key}: [${malformed ? "unavailable" : "omitted"}]\n- ${options.key}_omission:\n - reason: ${malformed ? "malformed_selected_text" : "response_too_large"}\n - shown: 0\n - omitted: ${options.values.length}\n - total: ${options.values.length}`;
43
- fullOptions = malformed ? omittedOptions : rows.length ? `- ${options.key}:\n${rows.join("\n")}` : controlField(options.key, "none");
44
- }
45
- // Start with the shorter complete-or-omitted rendering: adding an omission scaffold must
46
- // never manufacture overflow when the complete small field would fit. Then grow only.
47
- let selectedIdentity = Buffer.byteLength(identity) <= Buffer.byteLength(omittedIdentity) ? identity : omittedIdentity;
48
- let selectedOptions = Buffer.byteLength(fullOptions) <= Buffer.byteLength(omittedOptions) ? fullOptions : omittedOptions;
49
- const render = (target: string, choices: string) => ToolsResponse.withLeadingLineBreak(
50
- [target, ...fixed, ...(options ? [choices] : [])].join("\n"));
51
- if (Buffer.byteLength(render(identity, selectedOptions)) <= Limits.RUN_DELIVERY_MODEL_MAX_BYTES) selectedIdentity = identity;
52
- if (options && Buffer.byteLength(render(selectedIdentity, fullOptions)) <= Limits.RUN_DELIVERY_MODEL_MAX_BYTES) selectedOptions = fullOptions;
53
- return render(selectedIdentity, selectedOptions);
54
- }
55
-
56
- function controlNext(action: string): string {
57
- const inspect = /^inspect target=([^\s]+) view=([^\s]+)$/u.exec(action);
58
- return inspect ? ToolsResponse.formatInspectNextAction(inspect[1], inspect[2])
59
- : "- next:\n" + controlField("instruction", action, 1);
76
+ const identity = controlSelected(key, request.target === "runtime" ? request.target : request.target.slice(4));
77
+ const choices = options ? controlChoices(options) : undefined;
78
+ const render = (): ControlDocument => ({ ...identity.selected, action: request.action, result,
79
+ ...(choices ? choices.selected : {}) });
80
+ // Whole identity first, then the complete option sequence: the shorter seed must never manufacture overflow.
81
+ return ToolsResponse.withLeadingLineBreak(controlDocument(render, choices ? [identity, choices] : [identity]));
60
82
  }
61
83
 
62
84
  /** Pure first-layer copy; legacy stopped/killed flags are not scope-closure proof. */
@@ -64,32 +86,40 @@ export function compactControlResult(
64
86
  request: Control.ControlRequest,
65
87
  result: Record<string, unknown>,
66
88
  ): string {
67
- const fields = [request.target.startsWith("run:") ? controlField("run", request.target.slice(4)) : controlField("target", request.target),
68
- controlField("action", request.action)];
89
+ const key = request.target.startsWith("run:") ? "run" : "target";
90
+ const identity = controlSelected(key, key === "run" ? request.target.slice(4) : request.target);
91
+ const facts: ControlDocument = {};
69
92
  if (result.stopped === false || result.archived === false || result.pruned === false) {
70
- fields.push(controlField("result", "not applied"));
71
- if (typeof result.reason === "string") fields.push(controlField("reason", result.reason));
72
- const observed = asRecord(result.status);
73
- if (result.reason === "not running" && typeof observed.status === "string") fields.push(controlField("state", observed.status));
93
+ facts.result = "not applied";
74
94
  } else if (request.action === "kill" && result.stopped === true) {
75
- fields.push(controlField("result", "kill attempted"), controlField("termination", "unconfirmed"));
95
+ facts.result = "kill attempted";
96
+ facts.termination = "unconfirmed";
76
97
  } else if (result.archived === true) {
77
- fields.push(controlField("result", "archived"));
98
+ facts.result = "archived";
78
99
  } else if (result.pruned === true) {
79
- fields.push(controlField("result", "pruned"));
100
+ facts.result = "pruned";
80
101
  } else if (result.delivery === "queued" || result.delivery === "delivered") {
81
- fields.push(controlField("delivery", result.delivery), controlField("handling", "unconfirmed"));
102
+ facts.delivery = result.delivery;
103
+ facts.handling = "unconfirmed";
82
104
  } else {
83
- fields.push(controlField(typeof result.delivery === "string" ? "delivery" : "result",
84
- typeof result.delivery === "string" ? result.delivery : typeof result.status === "string" ? result.status : "accepted"));
85
- if (typeof result.reason === "string") fields.push(controlField("reason", result.reason));
105
+ facts[typeof result.delivery === "string" ? "delivery" : "result"] =
106
+ typeof result.delivery === "string" ? result.delivery : typeof result.status === "string" ? result.status : "accepted";
86
107
  }
87
- if (typeof result.control_id === "string") fields.push(controlField("control_id", result.control_id));
108
+ const notApplied = result.stopped === false || result.archived === false || result.pruned === false;
109
+ const reason = typeof result.reason === "string" ? controlSelected("reason", result.reason) : undefined;
110
+ const observed = asRecord(result.status);
111
+ const state = notApplied && result.reason === "not running" && typeof observed.status === "string"
112
+ ? controlSelected("state", observed.status) : undefined;
113
+ const controlId = typeof result.control_id === "string" ? controlSelected("control_id", result.control_id) : undefined;
88
114
  const actions = Array.isArray(result.next_actions) ? result.next_actions
89
115
  : request.target.startsWith("run:") ? runNextActions(request.target.slice(4), result) : [];
90
116
  const next = actions.find((action): action is string => typeof action === "string" && action.length > 0);
91
- if (next) fields.push(controlNext(next));
92
- return ToolsResponse.withLeadingLineBreak(fields.join("\n"));
117
+ const nextPart = next === undefined ? undefined : controlNextPart(next);
118
+ const parts = [identity, ...(reason ? [reason] : []), ...(state ? [state] : []), ...(controlId ? [controlId] : []), ...(nextPart ? [nextPart] : [])];
119
+ const render = (): ControlDocument => ({ ...identity.selected, action: request.action, ...facts,
120
+ ...(reason ? reason.selected : {}), ...(state ? state.selected : {}), ...(controlId ? controlId.selected : {}),
121
+ ...(nextPart ? nextPart.selected : {}) });
122
+ return ToolsResponse.withLeadingLineBreak(controlDocument(render, parts));
93
123
  }
94
124
 
95
125
  function runNextActions(run: string, result: Record<string, unknown>): string[] {
@@ -228,16 +258,13 @@ export function createControlToolDefinition<TContext = unknown>(
228
258
  );
229
259
  } catch (cause) {
230
260
  // Admission-era correlation is not current reply-scope proof after an awaited dispatch.
231
- const summary = [controlField("action", request.action), controlField("result", "request failed"),
232
- controlField("delivery", "unconfirmed"), controlField("handling", "unconfirmed"),
233
- controlField("warning", "Effects may already exist.\nDo not repeat automatically."),
234
- controlNext("inspect target=runtime view=runs")].join("\n");
235
- // Reserve both line breaks; display one whole caller-owned id or explicit whole-id omission.
236
- const identity = controlField("run", run);
237
- const target = Buffer.byteLength(identity) <= Limits.RUN_DELIVERY_MODEL_MAX_BYTES - Buffer.byteLength(summary) - 2
238
- ? identity
239
- : "- run: [omitted]\n- run_omission:\n - reason: response_too_large\n - shown: 0\n - omitted: 1\n - total: 1";
240
- throw new Error(ToolsResponse.withLeadingLineBreak(target + "\n" + summary), { cause });
261
+ // Display one whole caller-owned id or an explicit whole-id omission; the warning stays literal.
262
+ const identity = controlSelected("run", run);
263
+ const next = controlNextPart("inspect target=runtime view=runs");
264
+ const render = (): ControlDocument => ({ ...identity.selected, action: request.action,
265
+ result: "request failed", delivery: "unconfirmed", handling: "unconfirmed",
266
+ warning: "Effects may already exist.\nDo not repeat automatically.", ...next.selected });
267
+ throw new Error(ToolsResponse.withLeadingLineBreak(controlDocument(render, [identity])), { cause });
241
268
  }
242
269
  }
243
270
  // A refused identity change must not publish the replacement's status, even in verbose/details.
@@ -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.0",
3
+ "version": "0.57.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -78,11 +78,11 @@ Four locally authored Control guard errors use caller target/action/result recor
78
78
 
79
79
  Local spawn guards identify unsupported state_dir or invalid as with field/result and the selected caller value, not an admitted Run. Whole values fit16KiB or show field omission; malformed text is unavailable, not repaired. The host may reject unsupported state_dir by its own schema before the facade, so not every SDK validation error has AX text. This adds no launch, no-effects receipt, retry permission, generated identity or change to Recipe/session/post-launch failures.
80
80
 
81
- Successful normal register_tool receipts now use flat tool/result/source/callable_now fields and actual argument/warning sequences. A non-template source stays one exact literal; complete warnings take priority under16KiB, and whole-field/list omissions count only selected metadata, not hidden chars. Call advice remains opaque instruction, not automatic invocation; an unverified registration keeps its existing read-only tool-status next. Persistence is not callability: rely on the existing activation evidence, never substitute spawn or infer fresh host readiness from this layout. Transaction/errors/list/delete/draft and saved messages are not restyled by this slice.
81
+ Fresh source normal register_tool receipts use bare block-YAML tool/result/source/callable_now fields and native argument/warning sequences, including empty argument lists. Normal callability remains Boolean and a non-template source stays one exact safely quoted scalar; complete warnings take priority under16KiB, and whole-field/list omissions count only selected metadata, not hidden chars. Call advice remains opaque instruction, not automatic invocation; an unverified registration keeps its existing read-only tool-status next. Persistence is not callability: rely on the existing activation evidence, never substitute spawn or infer fresh host readiness from this layout. Transaction/errors/list/delete/draft and saved messages are not restyled by this slice.
82
82
 
83
- Registry name listings now show the same selected sorted names as one list with coverage; an overlarge whole group is omitted rather than mislabeled empty, and malformed text is unavailable without repair. Counts describe selected names, not active/callable or the complete loaded-host registry. Deletion returns tool/result and preserves the manual reload note as opaque instruction: neither a current-map miss nor returned deletion proves full host removal or authorizes automatic reload/retry. Existing partial-effect exceptions and machine details remain unchanged.
83
+ Fresh source registry listings and deletion receipts use bare block YAML. Listings preserve the same selected sorted names as a native sequence with coverage; empty selection is `[]`, an overlarge whole group is explicitly omitted rather than mislabeled empty, and malformed text is unavailable without repair. Counts describe selected names, not active/callable or the complete loaded-host registry. Deletion returns tool/result and preserves the manual reload note as opaque instruction: neither a current-map miss nor returned deletion proves full host removal or authorizes automatic reload/retry. Existing partial-effect exceptions and machine details remain unchanged. Combined compiled/package acceptance and loaded-host observation remain separate; an older runtime can still return the prior flat-list text.
84
84
 
85
- Draft-promotion success uses the same field grammar only for its original selected name/new-update/source=draft/callable display and one prior hint. It adds no default/body/argument/path or activation facts and does not become an ordinary registration transaction. A zero, opaque legacy display or unavailable field is not permission to infer readiness or repair callability. Whole scalar/action omissions stay explicit; invocation advice is content and only the existing read-only status hint may be typed. Promotion callbacks/display can still fail after writes with original errors; no automatic rollback/retry/reload follows.
85
+ Fresh source draft-promotion success uses bare block YAML only for its original selected name/new-update/source=draft/callable display and one prior hint. The selected callable display remains a string, including quoted "false", "true" or "0", not reconstructed Boolean readiness. It adds no default/body/argument/path or activation facts and does not become an ordinary registration transaction. A zero, opaque legacy display or unavailable field is not permission to infer readiness or repair callability. Whole scalar/action omissions stay explicit; invocation advice is content and only the existing read-only status hint may be typed. Promotion callbacks/display can still fail after writes with original errors; no automatic rollback/retry/reload follows.
86
86
 
87
87
  Local register_tool source-mode refusals now identify field/result and actual replacement/reference/source-mode/inherited-field choices. Existing correction advice is instruction content, not automatic retry/update or a no-effects receipt. Supplied values are not echoed. The host owns clone/coercion/schema behavior before the facade, so use the actual outcome rather than assuming every wrong-typed request reaches a local AX guard. Other registry errors and all existing activation/transaction contracts remain unchanged.
88
88
 
@@ -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.