@llblab/pi-actors 0.43.0 → 0.44.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.
- package/AGENTS.md +16 -10
- package/CHANGELOG.md +425 -536
- package/README.md +13 -11
- package/dist/index.js +1 -1
- package/dist/lib/async-runs.d.ts +2 -1
- package/dist/lib/async-runs.js +24 -34
- package/dist/lib/automatic-review-runtime.d.ts +1 -1
- package/dist/lib/automatic-review-runtime.js +5 -5
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/command-templates.js +38 -4
- package/dist/lib/control-projection.d.ts +20 -0
- package/dist/lib/control-projection.js +66 -0
- package/dist/lib/control.d.ts +3 -0
- package/dist/lib/control.js +27 -14
- package/dist/lib/draft-sleep.js +3 -3
- package/dist/lib/file-state.d.ts +4 -1
- package/dist/lib/file-state.js +118 -44
- package/dist/lib/inspector-overlay.d.ts +2 -0
- package/dist/lib/inspector-overlay.js +124 -69
- package/dist/lib/limits.d.ts +15 -3
- package/dist/lib/limits.js +15 -3
- package/dist/lib/observability.d.ts +4 -2
- package/dist/lib/observability.js +43 -36
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-control.js +6 -2
- package/dist/lib/review-control.d.ts +1 -1
- package/dist/lib/review-control.js +4 -5
- package/dist/lib/run-evidence-policy.d.ts +95 -0
- package/dist/lib/run-evidence-policy.js +177 -0
- package/dist/lib/run-ui-runtime.js +2 -0
- package/dist/lib/runs-control-delivery.d.ts +8 -1
- package/dist/lib/runs-control-delivery.js +38 -15
- package/dist/lib/runs-controls.d.ts +8 -4
- package/dist/lib/runs-controls.js +189 -50
- package/dist/lib/runs-retention.js +27 -14
- package/dist/lib/runs-trace.d.ts +26 -2
- package/dist/lib/runs-trace.js +411 -18
- package/dist/lib/runtime-identity.d.ts +7 -0
- package/dist/lib/runtime-identity.js +35 -0
- package/dist/lib/runtime-triage.d.ts +29 -0
- package/dist/lib/runtime-triage.js +60 -0
- package/dist/lib/tool-review-scheduler.js +7 -7
- package/dist/lib/tools-inspect.js +91 -18
- package/dist/lib/tools-message.d.ts +1 -2
- package/dist/lib/tools-message.js +6 -6
- package/dist/lib/tools-response.d.ts +0 -1
- package/dist/lib/tools-response.js +0 -9
- package/dist/lib/tools.d.ts +1 -1
- package/dist/lib/tools.js +1 -1
- package/dist/lib/trace-projection.js +107 -41
- package/dist/scripts/conformance.mjs +5 -0
- package/dist/scripts/locker.mjs +40 -90
- package/dist/scripts/music-player.mjs +48 -142
- package/dist/scripts/release-gates.mjs +56 -3
- package/dist/scripts/validate-recipe.mjs +5 -4
- package/dist/skills/actors/SKILL.md +17 -11
- package/dist/skills/swarm/SKILL.md +2 -4
- package/docs/README.md +1 -4
- package/docs/actor-inspector.md +6 -5
- package/docs/async-runs.md +11 -9
- package/docs/command-templates.md +6 -116
- package/docs/recipe-library.md +4 -6
- package/docs/releasing.md +28 -0
- package/docs/template-recipes.md +1 -1
- package/docs/tool-registry.md +2 -2
- package/index.ts +1 -1
- package/lib/async-runs.ts +26 -50
- package/lib/automatic-review-runtime.ts +7 -7
- package/lib/command-templates.ts +44 -4
- package/lib/control-projection.ts +105 -0
- package/lib/control.ts +33 -18
- package/lib/draft-sleep.ts +3 -3
- package/lib/file-state.ts +91 -63
- package/lib/inspector-overlay.ts +108 -61
- package/lib/limits.ts +15 -3
- package/lib/observability.ts +55 -57
- package/lib/prompts.ts +1 -1
- package/lib/recipe-control.ts +9 -2
- package/lib/review-control.ts +4 -5
- package/lib/run-evidence-policy.ts +242 -0
- package/lib/run-ui-runtime.ts +2 -0
- package/lib/runs-control-delivery.ts +45 -17
- package/lib/runs-controls.ts +180 -102
- package/lib/runs-retention.ts +28 -20
- package/lib/runs-trace.ts +499 -20
- package/lib/runtime-identity.ts +39 -0
- package/lib/runtime-triage.ts +106 -0
- package/lib/tool-review-scheduler.ts +7 -7
- package/lib/tools-inspect.ts +94 -20
- package/lib/tools-message.ts +7 -8
- package/lib/tools-response.ts +0 -12
- package/lib/tools.ts +4 -4
- package/lib/trace-projection.ts +156 -71
- package/package.json +1 -1
- package/scripts/conformance.mjs +5 -0
- package/scripts/locker.mjs +40 -90
- package/scripts/music-player.mjs +48 -142
- package/scripts/release-gates.mjs +56 -3
- package/scripts/validate-recipe.mjs +5 -4
- package/skills/actors/SKILL.md +17 -11
- package/skills/swarm/SKILL.md +2 -4
- package/dist/lib/runtime-notifier.d.ts +0 -48
- package/dist/lib/runtime-notifier.js +0 -138
- package/docs/0.43-baseline.md +0 -44
- package/docs/actors-deep-reference.md +0 -108
- package/docs/component-recipes.md +0 -45
- package/docs/task-first-recipes.md +0 -261
- package/lib/runtime-notifier.ts +0 -211
package/dist/lib/limits.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared output and
|
|
3
|
-
* Zones: output governance, Trace previews,
|
|
2
|
+
* Shared output, preview, and Control envelope limits.
|
|
3
|
+
* Zones: output governance, Trace previews, Inspect defaults, Control portability
|
|
4
4
|
*/
|
|
5
5
|
export const DEFAULT_INSPECT_LINES = 40;
|
|
6
6
|
export const TOOL_OUTPUT_MAX_BYTES = 50 * 1024;
|
|
7
7
|
export const TOOL_OUTPUT_MAX_LINES = 2_000;
|
|
8
8
|
export const COMPACT_PREVIEW_CHARS = 160;
|
|
9
|
-
export const
|
|
9
|
+
export const CONTROL_ACTION_MAX_LENGTH = 64;
|
|
10
|
+
export const CONTROL_INPUT_MAX_BYTES = 380;
|
|
11
|
+
export const CONTROL_WIRE_MAX_BYTES = 512;
|
|
10
12
|
export const INSPECTOR_BODY_PREVIEW_CHARS = 320;
|
|
11
13
|
export const DOCTOR_ACTION_PREVIEW_CHARS = 72;
|
|
12
14
|
export const SESSION_EVIDENCE_MAX_TURNS = 100;
|
|
@@ -14,3 +16,13 @@ export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
|
|
|
14
16
|
export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
|
|
15
17
|
export const TRACE_EVENT_MAX_BYTES = 64 * 1024;
|
|
16
18
|
export const TRACE_EVENT_MAX_READ = 200;
|
|
19
|
+
export const TRACE_JOURNAL_MAX_EVENTS = 2_048;
|
|
20
|
+
export const TRACE_JOURNAL_TARGET_EVENTS = 1_536;
|
|
21
|
+
export const TRACE_JOURNAL_MAX_BYTES = 4 * 1024 * 1024;
|
|
22
|
+
export const TRACE_JOURNAL_TARGET_BYTES = 3 * 1024 * 1024;
|
|
23
|
+
export const RUN_CONTROL_PENDING_LIMIT = 64;
|
|
24
|
+
export const RUN_CONTROL_TERMINAL_LIMIT = 128;
|
|
25
|
+
export const RUN_CONTROL_ERROR_MAX_BYTES = 4 * 1024;
|
|
26
|
+
export const RUN_CONTROL_JOURNAL_MAX_BYTES = 1024 * 1024;
|
|
27
|
+
export const RUN_RETENTION_MAX_RECORDS = 256;
|
|
28
|
+
export const RUN_RETENTION_MAX_BYTES = 1024 * 1024;
|
|
@@ -42,7 +42,7 @@ export interface RunSummary {
|
|
|
42
42
|
}
|
|
43
43
|
export interface RunUiObservationState {
|
|
44
44
|
attentionEventIds: Map<string, Set<string>>;
|
|
45
|
-
|
|
45
|
+
legacyEventLines: Map<string, number>;
|
|
46
46
|
frame: number;
|
|
47
47
|
observed: Map<string, RunObservedStatus>;
|
|
48
48
|
}
|
|
@@ -62,6 +62,7 @@ export interface RunUiNotificationSink {
|
|
|
62
62
|
}): void;
|
|
63
63
|
}
|
|
64
64
|
export declare function createRunUiObservationState(): RunUiObservationState;
|
|
65
|
+
export declare function primeRunAttentionState(state: RunUiObservationState, ownerId: string, stateRoot?: string): void;
|
|
65
66
|
export declare function readRunUiSnapshot(state: RunUiObservationState, ownerId: string, options?: {
|
|
66
67
|
includeAttention?: boolean;
|
|
67
68
|
stateRoot?: string;
|
|
@@ -74,6 +75,7 @@ export declare function reconcileRunTerminalNotifications(input: {
|
|
|
74
75
|
sink: RunUiNotificationSink;
|
|
75
76
|
state: RunUiObservationState;
|
|
76
77
|
stateRoot?: string;
|
|
78
|
+
includeAttention?: boolean;
|
|
77
79
|
}): RunUiSnapshot;
|
|
78
80
|
export declare function deliverRunAttentionNotifications(events: RunAttentionEvent[], sink: RunUiNotificationSink): void;
|
|
79
81
|
export interface RunRetirementCandidate {
|
|
@@ -173,7 +175,7 @@ export declare function findRunRetirementCandidates(summary: RunSummary): RunRet
|
|
|
173
175
|
export declare function executeRunRetirements(summary: RunSummary, options: RunRetirementExecutorOptions): Promise<RunRetirementExecution[]>;
|
|
174
176
|
export declare function detectRunTransitions(previous: Map<string, RunObservedStatus>, summary: RunSummary): RunTransition[];
|
|
175
177
|
export declare function pruneRunObservationState(previousStatuses: Map<string, RunObservedStatus>, previousLineCounts: Map<string, number>, summary: RunSummary, terminalRuns?: Iterable<string>, seenEventIds?: Map<string, Set<string>>): void;
|
|
176
|
-
export declare function detectRunAttentionEvents(
|
|
178
|
+
export declare function detectRunAttentionEvents(legacyLineCounts: Map<string, number>, summary: RunSummary, seenEventIds?: Map<string, Set<string>>, prime?: boolean): RunAttentionEvent[];
|
|
177
179
|
export declare function getRunAttentionNotificationType(event: RunAttentionEvent): RunTransitionNotificationType;
|
|
178
180
|
export declare function shouldNotifyRunAttentionEvent(event: RunAttentionEvent): boolean;
|
|
179
181
|
export declare function shouldSendRunAttentionFollowUp(event: RunAttentionEvent): boolean;
|
|
@@ -7,29 +7,33 @@ import { closeSync, existsSync, fstatSync, openSync, readdirSync, readFileSync,
|
|
|
7
7
|
import { basename, dirname, isAbsolute, join, relative, } from "node:path";
|
|
8
8
|
import * as AsyncRuns from "./async-runs.js";
|
|
9
9
|
import * as Paths from "./paths.js";
|
|
10
|
+
import * as RunsTrace from "./runs-trace.js";
|
|
10
11
|
import { readJsonlFileResilient } from "./state-readers.js";
|
|
11
12
|
export function createRunUiObservationState() {
|
|
12
13
|
return {
|
|
13
14
|
attentionEventIds: new Map(),
|
|
14
|
-
|
|
15
|
+
legacyEventLines: new Map(),
|
|
15
16
|
frame: 0,
|
|
16
17
|
observed: new Map(),
|
|
17
18
|
};
|
|
18
19
|
}
|
|
20
|
+
export function primeRunAttentionState(state, ownerId, stateRoot) {
|
|
21
|
+
detectRunAttentionEvents(state.legacyEventLines, summarizeRuns(stateRoot, ownerId), state.attentionEventIds, true);
|
|
22
|
+
}
|
|
19
23
|
export function readRunUiSnapshot(state, ownerId, options = {}) {
|
|
20
24
|
const summary = summarizeRuns(options.stateRoot, ownerId);
|
|
21
25
|
const status = renderRunStatus(summary, state.frame++);
|
|
22
26
|
return {
|
|
23
27
|
attentionEvents: options.includeAttention === false
|
|
24
28
|
? []
|
|
25
|
-
: detectRunAttentionEvents(state.
|
|
29
|
+
: detectRunAttentionEvents(state.legacyEventLines, summary, state.attentionEventIds),
|
|
26
30
|
status,
|
|
27
31
|
summary,
|
|
28
32
|
transitions: detectRunTransitions(state.observed, summary),
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
export function pruneRunUiObservationState(state, snapshot) {
|
|
32
|
-
pruneRunObservationState(state.observed, state.
|
|
36
|
+
pruneRunObservationState(state.observed, state.legacyEventLines, snapshot.summary, snapshot.transitions.map((transition) => transition.stateDir ?? transition.run), state.attentionEventIds);
|
|
33
37
|
}
|
|
34
38
|
export function deliverRunTransitionNotifications(transitions, sink, inFlight = new Set()) {
|
|
35
39
|
for (const transition of transitions) {
|
|
@@ -68,9 +72,11 @@ export function deliverRunTransitionNotifications(transitions, sink, inFlight =
|
|
|
68
72
|
}
|
|
69
73
|
export function reconcileRunTerminalNotifications(input) {
|
|
70
74
|
const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
|
|
71
|
-
includeAttention:
|
|
75
|
+
includeAttention: input.includeAttention,
|
|
72
76
|
stateRoot: input.stateRoot,
|
|
73
77
|
});
|
|
78
|
+
if (input.includeAttention)
|
|
79
|
+
deliverRunAttentionNotifications(snapshot.attentionEvents, input.sink);
|
|
74
80
|
deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
|
|
75
81
|
pruneRunUiObservationState(input.state, snapshot);
|
|
76
82
|
return snapshot;
|
|
@@ -769,12 +775,13 @@ function parseAttentionRecord(raw, run, index) {
|
|
|
769
775
|
}
|
|
770
776
|
function readTraceAttentionRecords(run) {
|
|
771
777
|
if (!run.stateDir)
|
|
772
|
-
return [];
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
+
return { canonical: true, records: [] };
|
|
779
|
+
if (existsSync(join(run.stateDir, "trace.jsonl")))
|
|
780
|
+
return {
|
|
781
|
+
canonical: true,
|
|
782
|
+
records: RunsTrace.readRunTraceJournal(run.stateDir).events.map(({ event }) => event),
|
|
783
|
+
};
|
|
784
|
+
return { canonical: false, records: readJsonlFileResilient(join(run.stateDir, "outbox.jsonl")).records };
|
|
778
785
|
}
|
|
779
786
|
export function pruneRunObservationState(previousStatuses, previousLineCounts, summary, terminalRuns = [], seenEventIds = new Map()) {
|
|
780
787
|
const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
|
|
@@ -785,43 +792,43 @@ export function pruneRunObservationState(previousStatuses, previousLineCounts, s
|
|
|
785
792
|
const activeLineKeys = new Set(summary.runs.map((run) => run.stateDir ?? run.run));
|
|
786
793
|
for (const run of terminalRunSet)
|
|
787
794
|
previousStatuses.delete(run);
|
|
788
|
-
for (const run of previousStatuses.keys())
|
|
795
|
+
for (const run of previousStatuses.keys())
|
|
789
796
|
if (!activeRuns.has(run))
|
|
790
797
|
previousStatuses.delete(run);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) {
|
|
798
|
+
for (const key of previousLineCounts.keys())
|
|
799
|
+
if (terminalLineKeys.has(key) || !activeLineKeys.has(key))
|
|
794
800
|
previousLineCounts.delete(key);
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
for (const key of seenEventIds.keys()) {
|
|
798
|
-
if (terminalLineKeys.has(key) || !activeLineKeys.has(key)) {
|
|
801
|
+
for (const key of seenEventIds.keys())
|
|
802
|
+
if (terminalLineKeys.has(key) || !activeLineKeys.has(key))
|
|
799
803
|
seenEventIds.delete(key);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
804
|
}
|
|
803
|
-
export function detectRunAttentionEvents(
|
|
805
|
+
export function detectRunAttentionEvents(legacyLineCounts, summary, seenEventIds = new Map(), prime = false) {
|
|
804
806
|
const events = [];
|
|
805
807
|
for (const run of summary.runs) {
|
|
806
808
|
const key = run.stateDir ?? run.run;
|
|
807
|
-
const
|
|
808
|
-
const
|
|
809
|
-
if (run.notificationPolicy === "silent") {
|
|
810
|
-
previousLineCounts.set(key, records.length);
|
|
811
|
-
seenEventIds.set(key, seenEventIds.get(key) ?? new Set());
|
|
812
|
-
continue;
|
|
813
|
-
}
|
|
814
|
-
const start = Math.min(previousCount, records.length);
|
|
809
|
+
const read = readTraceAttentionRecords(run);
|
|
810
|
+
const retained = new Set();
|
|
815
811
|
const seen = seenEventIds.get(key) ?? new Set();
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
812
|
+
const start = read.canonical ? 0
|
|
813
|
+
: Math.min(legacyLineCounts.get(key) ?? 0, read.records.length);
|
|
814
|
+
for (const [index, record] of read.records.entries()) {
|
|
815
|
+
const event = parseAttentionRecord(record, run, index);
|
|
816
|
+
if (!event || !shouldNotifyRunAttentionEvent(event) ||
|
|
817
|
+
event.kind === "runtime.trace_compacted")
|
|
819
818
|
continue;
|
|
820
|
-
|
|
821
|
-
|
|
819
|
+
retained.add(event.id);
|
|
820
|
+
if (prime || run.notificationPolicy === "silent")
|
|
821
|
+
seen.add(event.id);
|
|
822
|
+
else if (index >= start && !seen.has(event.id)) {
|
|
823
|
+
events.push(event);
|
|
824
|
+
seen.add(event.id);
|
|
825
|
+
}
|
|
822
826
|
}
|
|
823
|
-
|
|
824
|
-
|
|
827
|
+
seenEventIds.set(key, new Set([...retained].filter((id) => seen.has(id))));
|
|
828
|
+
if (read.canonical)
|
|
829
|
+
legacyLineCounts.delete(key);
|
|
830
|
+
else
|
|
831
|
+
legacyLineCounts.set(key, read.records.length);
|
|
825
832
|
}
|
|
826
833
|
return events;
|
|
827
834
|
}
|
package/dist/lib/prompts.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export declare const REGISTER_TOOL_DESCRIPTION: string;
|
|
7
7
|
export declare const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
|
|
8
8
|
export declare const REGISTER_TOOL_GUIDELINES: string[];
|
|
9
|
-
export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync and shell-free: string leaves split into executable + argv,
|
|
9
|
+
export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync and shell-free: string leaves split into executable + argv, infer .js/.mjs through node\u2192bun\u2192deno run and .sh through bash, and treat operators such as && as literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.\n- Recipes own template directly and may declare metadata/defaults/imports/control/artifacts; files >1 MiB or import depth >32 fail closed.\n- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.\n- Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.\n- Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid internal transport vocabulary in public guidance.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs. Inspect intentionally and avoid busy-polling. Terminal and coordinator-bound notifications queue as Pi follow-ups so concurrently completed actors can reach the coordinator after current work instead of steering between tool calls. Terminal follow-up content stays minimal: run, status, one base path, and relative artifact names only; semantic output stays in non-LLM details and run state. When a deferred actor result gates the next step, wait for its terminal follow-up; do not schedule continuation loops, repeatedly inspect, or mutate its reviewed scope while it runs. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue/stuck run.\n- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.\n- Prefer maintained packaged recipes/pipelines with spawn file=<recipe> before ad hoc scripts/wrappers; review swarms inherit current model/thinking, preflight before fanout, and expose quorum/concurrency/TTL knobs unless explicit args are passed.\n- For any non-trivial actor use or pi-actors change, read the bundled actors skill first. Before launching multiple actors/subagents for parallel implementation, independent artifact generation, delegated audit, or review, also read the bundled swarm skill; the coordinator owns decomposition, disjoint scopes, launch correctness, integration, and final validation. For deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.";
|
|
10
10
|
export declare const REGISTER_TOOL_PARAM_DESCRIPTIONS: {
|
|
11
11
|
readonly name: "Tool name in snake_case (e.g., 'transcribe')";
|
|
12
12
|
readonly description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.";
|
package/dist/lib/prompts.js
CHANGED
|
@@ -16,7 +16,7 @@ export const REGISTER_TOOL_GUIDELINES = [
|
|
|
16
16
|
export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
17
17
|
- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
|
|
18
18
|
- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
|
|
19
|
-
- Command templates stay sync and shell-free: string leaves split into executable + argv,
|
|
19
|
+
- Command templates stay sync and shell-free: string leaves split into executable + argv, infer .js/.mjs through node→bun→deno run and .sh through bash, and treat operators such as && as literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.
|
|
20
20
|
- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
|
|
21
21
|
- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
|
|
22
22
|
- Recipes own template directly and may declare metadata/defaults/imports/control/artifacts; files >1 MiB or import depth >32 fail closed.
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Zones: actor-local action declarations, normalization, reserved-action fencing
|
|
4
4
|
* Owns pure Recipe control validation; Recipe loading and Run capture stay in recipe/run domains.
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
import * as Control from "./control.js";
|
|
7
|
+
import * as Limits from "./limits.js";
|
|
7
8
|
const RUNTIME_RUN_ACTIONS = new Set(["archive", "kill", "prune"]);
|
|
8
9
|
export function normalizeRecipeControl(value) {
|
|
9
10
|
if (value === undefined)
|
|
@@ -18,9 +19,12 @@ export function normalizeRecipeControl(value) {
|
|
|
18
19
|
throw new Error("recipe.control actions must be non-empty strings");
|
|
19
20
|
}
|
|
20
21
|
const action = raw.trim();
|
|
21
|
-
if (!
|
|
22
|
+
if (!Control.isControlAction(action)) {
|
|
22
23
|
throw new Error(`invalid recipe.control action: ${action}`);
|
|
23
24
|
}
|
|
25
|
+
if (action.length > Limits.CONTROL_ACTION_MAX_LENGTH) {
|
|
26
|
+
throw new Error(`recipe.control action exceeds ${Limits.CONTROL_ACTION_MAX_LENGTH} ASCII characters: ${action}`);
|
|
27
|
+
}
|
|
24
28
|
if (RUNTIME_RUN_ACTIONS.has(action)) {
|
|
25
29
|
throw new Error(`recipe.control action is runtime-reserved and must not be declared: ${action}`);
|
|
26
30
|
}
|
|
@@ -11,4 +11,4 @@ export interface AutomaticReviewControlOptions {
|
|
|
11
11
|
toolStatePath?: string;
|
|
12
12
|
}
|
|
13
13
|
export declare function controlAutomaticReview(action: AutomaticReviewControlAction, scope: AutomaticReviewScope, options?: AutomaticReviewControlOptions): Record<string, unknown>;
|
|
14
|
-
export declare function parseAutomaticReviewScope(
|
|
14
|
+
export declare function parseAutomaticReviewScope(input: unknown): AutomaticReviewScope;
|
|
@@ -98,14 +98,13 @@ export function controlAutomaticReview(action, scope, options = {}) {
|
|
|
98
98
|
return {
|
|
99
99
|
...result,
|
|
100
100
|
next_actions: nextActions,
|
|
101
|
-
sent: result.changed === true,
|
|
102
101
|
};
|
|
103
102
|
}
|
|
104
|
-
export function parseAutomaticReviewScope(
|
|
105
|
-
const record =
|
|
106
|
-
?
|
|
103
|
+
export function parseAutomaticReviewScope(input) {
|
|
104
|
+
const record = input && typeof input === "object" && !Array.isArray(input)
|
|
105
|
+
? input
|
|
107
106
|
: {};
|
|
108
107
|
if (record.scope === "draft" || record.scope === "tool")
|
|
109
108
|
return record.scope;
|
|
110
|
-
throw new Error("review
|
|
109
|
+
throw new Error("review Control requires input.scope=draft or input.scope=tool.");
|
|
111
110
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure bounded Run-evidence policy.
|
|
3
|
+
* Zones: Trace retention, compaction accounting, Control capacity and admission
|
|
4
|
+
* Owns deterministic limits policy; journal I/O, locks, lifecycle authorization, and projection stay in adapters.
|
|
5
|
+
* Trace keeps the newest physical suffix because semantic priority would silently
|
|
6
|
+
* redefine causal history. Admitted Controls never expire: only a real terminal
|
|
7
|
+
* transition or generation replacement proves that durable work is finished.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TRACE_COMPACTION_KIND = "runtime.trace_compacted";
|
|
10
|
+
export declare const TRACE_COMPACTION_VERSION = 1;
|
|
11
|
+
export type TraceJournalUsage = {
|
|
12
|
+
bytes: number;
|
|
13
|
+
events: number;
|
|
14
|
+
};
|
|
15
|
+
export type TraceRetentionCandidate<T> = {
|
|
16
|
+
encodedBytes: number;
|
|
17
|
+
value: T;
|
|
18
|
+
};
|
|
19
|
+
export type TraceRetentionSelection<T> = {
|
|
20
|
+
droppedEvents: number;
|
|
21
|
+
retained: TraceRetentionCandidate<T>[];
|
|
22
|
+
retainedBytes: number;
|
|
23
|
+
retainedEvents: number;
|
|
24
|
+
};
|
|
25
|
+
export interface TraceCompactionData {
|
|
26
|
+
version: typeof TRACE_COMPACTION_VERSION;
|
|
27
|
+
compactions_total: number;
|
|
28
|
+
dropped_valid_events_total: number;
|
|
29
|
+
dropped_malformed_lines_total: number;
|
|
30
|
+
dropped_bytes_total: number;
|
|
31
|
+
dropped_event_count_exact: boolean;
|
|
32
|
+
retained_events: number;
|
|
33
|
+
retained_bytes: number;
|
|
34
|
+
history_complete: false;
|
|
35
|
+
}
|
|
36
|
+
export interface TraceCompactionDelta {
|
|
37
|
+
dropped_valid_events: number;
|
|
38
|
+
dropped_malformed_lines: number;
|
|
39
|
+
dropped_bytes: number;
|
|
40
|
+
dropped_event_count_exact: boolean;
|
|
41
|
+
retained_events: number;
|
|
42
|
+
retained_bytes: number;
|
|
43
|
+
}
|
|
44
|
+
export type RunControlStatus = "queued" | "delivered" | "claimed" | "handled" | "failed";
|
|
45
|
+
export type RunControlCapacity = {
|
|
46
|
+
available: number;
|
|
47
|
+
backpressured: boolean;
|
|
48
|
+
limit: number;
|
|
49
|
+
oldest_pending_at?: string;
|
|
50
|
+
pending: number;
|
|
51
|
+
};
|
|
52
|
+
export type RunControlAdmissionIntegrity = "valid" | "unreadable" | "malformed" | "noncanonical" | "invalid_record" | "invalid_status_timestamp" | "generation_mismatch" | "journal_bytes";
|
|
53
|
+
export interface RunControlAdmissionErrorDetails {
|
|
54
|
+
reason: "control_backpressure" | "control_journal_integrity";
|
|
55
|
+
pending: number;
|
|
56
|
+
limit: number;
|
|
57
|
+
journal_bytes: number;
|
|
58
|
+
journal_limit: number;
|
|
59
|
+
oldest_pending_at?: string;
|
|
60
|
+
run_instance_id: string;
|
|
61
|
+
next_actions: string[];
|
|
62
|
+
integrity_reason?: string;
|
|
63
|
+
}
|
|
64
|
+
export type RunControlAdmissionDecision = {
|
|
65
|
+
admitted: true;
|
|
66
|
+
capacity: RunControlCapacity;
|
|
67
|
+
} | {
|
|
68
|
+
admitted: false;
|
|
69
|
+
capacity: RunControlCapacity;
|
|
70
|
+
error: RunControlAdmissionErrorDetails;
|
|
71
|
+
};
|
|
72
|
+
export type RunControlAdmissionInput = {
|
|
73
|
+
integrity: RunControlAdmissionIntegrity;
|
|
74
|
+
journalBytes: number;
|
|
75
|
+
newRecordBytes: number;
|
|
76
|
+
records: readonly unknown[];
|
|
77
|
+
retainedJournalBytes: number;
|
|
78
|
+
runInstanceId: string;
|
|
79
|
+
capacity?: RunControlCapacity;
|
|
80
|
+
};
|
|
81
|
+
export declare function traceAppendFits(usage: TraceJournalUsage, encodedEventBytes: number): boolean;
|
|
82
|
+
export declare function selectNewestTraceSuffix<T>(candidates: readonly TraceRetentionCandidate<T>[], reserve?: {
|
|
83
|
+
bytes?: number;
|
|
84
|
+
events?: number;
|
|
85
|
+
}): TraceRetentionSelection<T>;
|
|
86
|
+
export declare function accumulateTraceCompactionStatistics(previous: TraceCompactionData | undefined, delta: TraceCompactionDelta): TraceCompactionData;
|
|
87
|
+
export declare function traceCompactionMarkerInput(data: TraceCompactionData): {
|
|
88
|
+
data: TraceCompactionData;
|
|
89
|
+
kind: typeof TRACE_COMPACTION_KIND;
|
|
90
|
+
level: "warning";
|
|
91
|
+
};
|
|
92
|
+
export declare function classifyRunControlRecord(value: unknown): "pending" | "terminal" | undefined;
|
|
93
|
+
export declare function runControlStatusTimestamp(record: Record<string, unknown>, status: RunControlStatus): unknown;
|
|
94
|
+
export declare function computeRunControlCapacity(records: readonly unknown[]): RunControlCapacity;
|
|
95
|
+
export declare function decideRunControlAdmission(input: RunControlAdmissionInput): RunControlAdmissionDecision;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure bounded Run-evidence policy.
|
|
3
|
+
* Zones: Trace retention, compaction accounting, Control capacity and admission
|
|
4
|
+
* Owns deterministic limits policy; journal I/O, locks, lifecycle authorization, and projection stay in adapters.
|
|
5
|
+
* Trace keeps the newest physical suffix because semantic priority would silently
|
|
6
|
+
* redefine causal history. Admitted Controls never expire: only a real terminal
|
|
7
|
+
* transition or generation replacement proves that durable work is finished.
|
|
8
|
+
*/
|
|
9
|
+
import * as Limits from "./limits.js";
|
|
10
|
+
export const TRACE_COMPACTION_KIND = "runtime.trace_compacted";
|
|
11
|
+
export const TRACE_COMPACTION_VERSION = 1;
|
|
12
|
+
const PENDING_CONTROL_STATUSES = new Set(["queued", "delivered", "claimed"]);
|
|
13
|
+
const TERMINAL_CONTROL_STATUSES = new Set(["handled", "failed"]);
|
|
14
|
+
const CONTROL_ADMISSION_NEXT_ACTIONS = [
|
|
15
|
+
"inspect target=run:<id> view=control",
|
|
16
|
+
"inspect target=runtime view=triage",
|
|
17
|
+
];
|
|
18
|
+
function isCount(value) {
|
|
19
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
20
|
+
}
|
|
21
|
+
function assertCount(value, name) {
|
|
22
|
+
if (!isCount(value))
|
|
23
|
+
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
24
|
+
}
|
|
25
|
+
function boundedAdd(left, right) {
|
|
26
|
+
return Math.min(Number.MAX_SAFE_INTEGER, left + right);
|
|
27
|
+
}
|
|
28
|
+
export function traceAppendFits(usage, encodedEventBytes) {
|
|
29
|
+
if (!isCount(usage.events) || !isCount(usage.bytes) || !isCount(encodedEventBytes))
|
|
30
|
+
return false;
|
|
31
|
+
return usage.events + 1 <= Limits.TRACE_JOURNAL_MAX_EVENTS &&
|
|
32
|
+
usage.bytes + encodedEventBytes <= Limits.TRACE_JOURNAL_MAX_BYTES;
|
|
33
|
+
}
|
|
34
|
+
export function selectNewestTraceSuffix(candidates, reserve = {}) {
|
|
35
|
+
const reservedBytes = reserve.bytes ?? 0;
|
|
36
|
+
const reservedEvents = reserve.events ?? 0;
|
|
37
|
+
assertCount(reservedBytes, "reserved Trace bytes");
|
|
38
|
+
assertCount(reservedEvents, "reserved Trace events");
|
|
39
|
+
const byteBudget = Math.max(0, Limits.TRACE_JOURNAL_TARGET_BYTES - reservedBytes);
|
|
40
|
+
const eventBudget = Math.max(0, Limits.TRACE_JOURNAL_TARGET_EVENTS - reservedEvents);
|
|
41
|
+
let retainedBytes = 0;
|
|
42
|
+
let start = candidates.length;
|
|
43
|
+
while (start > 0 && candidates.length - start < eventBudget) {
|
|
44
|
+
const candidate = candidates[start - 1];
|
|
45
|
+
assertCount(candidate.encodedBytes, "encoded Trace bytes");
|
|
46
|
+
if (retainedBytes + candidate.encodedBytes > byteBudget)
|
|
47
|
+
break;
|
|
48
|
+
retainedBytes += candidate.encodedBytes;
|
|
49
|
+
start -= 1;
|
|
50
|
+
}
|
|
51
|
+
const retained = candidates.slice(start);
|
|
52
|
+
return {
|
|
53
|
+
droppedEvents: candidates.length - retained.length,
|
|
54
|
+
retained,
|
|
55
|
+
retainedBytes,
|
|
56
|
+
retainedEvents: retained.length,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function accumulateTraceCompactionStatistics(previous, delta) {
|
|
60
|
+
for (const [name, value] of Object.entries(delta)) {
|
|
61
|
+
if (name !== "dropped_event_count_exact")
|
|
62
|
+
assertCount(value, name);
|
|
63
|
+
}
|
|
64
|
+
const prior = previous ?? {
|
|
65
|
+
compactions_total: 0,
|
|
66
|
+
dropped_bytes_total: 0,
|
|
67
|
+
dropped_event_count_exact: true,
|
|
68
|
+
dropped_malformed_lines_total: 0,
|
|
69
|
+
dropped_valid_events_total: 0,
|
|
70
|
+
};
|
|
71
|
+
const eventCountOverflow = prior.dropped_valid_events_total +
|
|
72
|
+
delta.dropped_valid_events > Number.MAX_SAFE_INTEGER;
|
|
73
|
+
return {
|
|
74
|
+
version: TRACE_COMPACTION_VERSION,
|
|
75
|
+
compactions_total: boundedAdd(prior.compactions_total, 1),
|
|
76
|
+
dropped_valid_events_total: boundedAdd(prior.dropped_valid_events_total, delta.dropped_valid_events),
|
|
77
|
+
dropped_malformed_lines_total: boundedAdd(prior.dropped_malformed_lines_total, delta.dropped_malformed_lines),
|
|
78
|
+
dropped_bytes_total: boundedAdd(prior.dropped_bytes_total, delta.dropped_bytes),
|
|
79
|
+
dropped_event_count_exact: prior.dropped_event_count_exact &&
|
|
80
|
+
delta.dropped_event_count_exact && !eventCountOverflow,
|
|
81
|
+
retained_events: delta.retained_events,
|
|
82
|
+
retained_bytes: delta.retained_bytes,
|
|
83
|
+
history_complete: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function traceCompactionMarkerInput(data) {
|
|
87
|
+
return { data, kind: TRACE_COMPACTION_KIND, level: "warning" };
|
|
88
|
+
}
|
|
89
|
+
export function classifyRunControlRecord(value) {
|
|
90
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
91
|
+
return undefined;
|
|
92
|
+
const status = value.status;
|
|
93
|
+
if (PENDING_CONTROL_STATUSES.has(status))
|
|
94
|
+
return "pending";
|
|
95
|
+
if (TERMINAL_CONTROL_STATUSES.has(status))
|
|
96
|
+
return "terminal";
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
export function runControlStatusTimestamp(record, status) {
|
|
100
|
+
if (status === "failed")
|
|
101
|
+
return record.failed_at;
|
|
102
|
+
if (status === "handled")
|
|
103
|
+
return record.handled_at;
|
|
104
|
+
if (status === "claimed")
|
|
105
|
+
return record.claimed_at;
|
|
106
|
+
if (status === "delivered")
|
|
107
|
+
return record.delivered_at;
|
|
108
|
+
return record.queued_at;
|
|
109
|
+
}
|
|
110
|
+
export function computeRunControlCapacity(records) {
|
|
111
|
+
let oldest;
|
|
112
|
+
let pending = 0;
|
|
113
|
+
for (const value of records) {
|
|
114
|
+
if (classifyRunControlRecord(value) !== "pending")
|
|
115
|
+
continue;
|
|
116
|
+
pending += 1;
|
|
117
|
+
const record = value;
|
|
118
|
+
const timestamp = runControlStatusTimestamp(record, record.status);
|
|
119
|
+
const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
|
|
120
|
+
if (typeof timestamp === "string" && Number.isFinite(timestampMs) &&
|
|
121
|
+
(!oldest || timestampMs < oldest.timestampMs)) {
|
|
122
|
+
oldest = { timestamp, timestampMs };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
available: Math.max(0, Limits.RUN_CONTROL_PENDING_LIMIT - pending),
|
|
127
|
+
backpressured: pending >= Limits.RUN_CONTROL_PENDING_LIMIT,
|
|
128
|
+
limit: Limits.RUN_CONTROL_PENDING_LIMIT,
|
|
129
|
+
...(oldest ? { oldest_pending_at: oldest.timestamp } : {}),
|
|
130
|
+
pending,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function decideRunControlAdmission(input) {
|
|
134
|
+
const capacity = input.capacity ?? computeRunControlCapacity(input.records);
|
|
135
|
+
const reject = (reason, integrityReason) => ({
|
|
136
|
+
admitted: false,
|
|
137
|
+
capacity,
|
|
138
|
+
error: {
|
|
139
|
+
reason,
|
|
140
|
+
pending: capacity.pending,
|
|
141
|
+
limit: capacity.limit,
|
|
142
|
+
journal_bytes: input.journalBytes,
|
|
143
|
+
journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
|
|
144
|
+
...(capacity.oldest_pending_at ? { oldest_pending_at: capacity.oldest_pending_at } : {}),
|
|
145
|
+
run_instance_id: input.runInstanceId,
|
|
146
|
+
next_actions: [...CONTROL_ADMISSION_NEXT_ACTIONS],
|
|
147
|
+
...(integrityReason ? { integrity_reason: integrityReason } : {}),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
if (input.integrity !== "valid")
|
|
151
|
+
return reject("control_journal_integrity", input.integrity);
|
|
152
|
+
if (!isCount(input.journalBytes) || !isCount(input.retainedJournalBytes) ||
|
|
153
|
+
!isCount(input.newRecordBytes) ||
|
|
154
|
+
input.journalBytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
|
|
155
|
+
return reject("control_journal_integrity", "journal_bytes");
|
|
156
|
+
}
|
|
157
|
+
for (const value of input.records) {
|
|
158
|
+
const classification = classifyRunControlRecord(value);
|
|
159
|
+
if (!classification)
|
|
160
|
+
return reject("control_journal_integrity", "invalid_record");
|
|
161
|
+
if (classification !== "pending")
|
|
162
|
+
continue;
|
|
163
|
+
const record = value;
|
|
164
|
+
const timestamp = runControlStatusTimestamp(record, record.status);
|
|
165
|
+
if (typeof timestamp !== "string" || !Number.isFinite(Date.parse(timestamp))) {
|
|
166
|
+
return reject("control_journal_integrity", "invalid_status_timestamp");
|
|
167
|
+
}
|
|
168
|
+
if (record.run_instance_id !== input.runInstanceId) {
|
|
169
|
+
return reject("control_journal_integrity", "generation_mismatch");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (capacity.backpressured ||
|
|
173
|
+
input.retainedJournalBytes + input.newRecordBytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
|
|
174
|
+
return reject("control_backpressure");
|
|
175
|
+
}
|
|
176
|
+
return { admitted: true, capacity };
|
|
177
|
+
}
|
|
@@ -79,6 +79,7 @@ export function createRunUiRuntime(deps) {
|
|
|
79
79
|
ownerId: deps.getRunOwnerId(ctx),
|
|
80
80
|
sink: Pi.createNotificationSink(deps.pi, ctx),
|
|
81
81
|
state: observation,
|
|
82
|
+
includeAttention: true,
|
|
82
83
|
});
|
|
83
84
|
reportDiagnostics(ctx);
|
|
84
85
|
},
|
|
@@ -110,6 +111,7 @@ export function createRunUiRuntime(deps) {
|
|
|
110
111
|
},
|
|
111
112
|
start(ctx) {
|
|
112
113
|
close();
|
|
114
|
+
Observability.primeRunAttentionState(observation, deps.getRunOwnerId(ctx));
|
|
113
115
|
update(ctx, true, true);
|
|
114
116
|
watcher.refresh();
|
|
115
117
|
reconciliation.start();
|
|
@@ -16,6 +16,13 @@ export interface DeliverRunControlRequest {
|
|
|
16
16
|
input?: unknown;
|
|
17
17
|
run_instance_id: string;
|
|
18
18
|
}
|
|
19
|
-
export declare
|
|
19
|
+
export declare function encodeRunControlWire(wire: {
|
|
20
|
+
action: string;
|
|
21
|
+
id: string;
|
|
22
|
+
input?: unknown;
|
|
23
|
+
}): {
|
|
24
|
+
bytes: number;
|
|
25
|
+
payload: string;
|
|
26
|
+
};
|
|
20
27
|
export declare function readRunControlEndpoint(stateDir: string, runInstanceId: string): RunControlEndpoint | undefined;
|
|
21
28
|
export declare function deliverRunControl(run: string, stateDir: string, request: DeliverRunControlRequest, options?: DeliverRunControlOptions): Promise<Record<string, unknown>>;
|