@llblab/pi-actors 0.48.0 → 0.49.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 +1 -1
- package/BACKLOG.md +4 -1
- package/CHANGELOG.md +16 -0
- package/README.md +4 -4
- package/dist/lib/async-runs.d.ts +7 -0
- package/dist/lib/async-runs.js +63 -8
- package/dist/lib/extension-runtime.js +3 -0
- package/dist/lib/file-state.js +15 -1
- package/dist/lib/inspector.js +4 -0
- package/dist/lib/observability.js +2 -5
- package/dist/lib/recipes-discovery.js +3 -1
- package/dist/lib/recipes-references.d.ts +3 -0
- package/dist/lib/recipes-references.js +15 -0
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +8 -4
- package/dist/lib/runs-start.d.ts +2 -1
- package/dist/lib/runs-start.js +11 -5
- package/dist/lib/tools-local.js +12 -8
- package/dist/lib/tools.d.ts +1 -0
- package/dist/lib/tools.js +1 -0
- package/dist/skills/actors/SKILL.md +3 -3
- package/dist/skills/actors/references/persistent-tools.md +3 -1
- package/dist/skills/actors/references/recipes.md +2 -2
- package/dist/skills/actors/references/runs.md +2 -0
- package/dist/skills/actors/scripts/validate-recipe.mjs +12 -0
- package/dist/skills/music-player/SKILL.md +53 -0
- package/dist/skills/music-player/genapps/music-player.mjs +360 -0
- package/{skills/media/recipes/player.json → dist/skills/music-player/recipes/playback.json} +4 -3
- package/dist/skills/music-player/scripts/playback-client.mjs +143 -0
- package/dist/skills/{media/scripts/music-player.mjs → music-player/scripts/playback.mjs} +596 -53
- package/docs/async-runs.md +1 -1
- package/docs/recipe-library.md +2 -5
- package/docs/template-recipes.md +7 -0
- package/docs/tool-registry.md +2 -2
- package/lib/async-runs.ts +92 -15
- package/lib/extension-runtime.ts +4 -0
- package/lib/file-state.ts +14 -1
- package/lib/inspector.ts +4 -0
- package/lib/observability.ts +2 -5
- package/lib/recipes-discovery.ts +2 -1
- package/lib/recipes-references.ts +20 -0
- package/lib/registry.ts +8 -6
- package/lib/runs-start.ts +16 -6
- package/lib/tools-local.ts +10 -8
- package/lib/tools.ts +2 -0
- package/package.json +1 -1
- package/skills/actors/SKILL.md +3 -3
- package/skills/actors/references/persistent-tools.md +3 -1
- package/skills/actors/references/recipes.md +2 -2
- package/skills/actors/references/runs.md +2 -0
- package/skills/actors/scripts/validate-recipe.mjs +12 -0
- package/skills/music-player/SKILL.md +53 -0
- package/skills/music-player/genapps/music-player.mjs +360 -0
- package/{dist/skills/media/recipes/player.json → skills/music-player/recipes/playback.json} +4 -3
- package/skills/music-player/scripts/playback-client.mjs +143 -0
- package/skills/{media/scripts/music-player.mjs → music-player/scripts/playback.mjs} +596 -53
- package/dist/skills/media/SKILL.md +0 -44
- package/dist/skills/media/recipes/library.json +0 -45
- package/dist/skills/media/recipes/playlist-build.json +0 -15
- package/dist/skills/media/recipes/playlist-scan.json +0 -11
- package/dist/skills/media/scripts/media-utils.mjs +0 -47
- package/skills/media/SKILL.md +0 -44
- package/skills/media/recipes/library.json +0 -45
- package/skills/media/recipes/playlist-build.json +0 -15
- package/skills/media/recipes/playlist-scan.json +0 -11
- package/skills/media/scripts/media-utils.mjs +0 -47
package/AGENTS.md
CHANGED
|
@@ -60,7 +60,7 @@ Pi host
|
|
|
60
60
|
- `observability.ts`, `run-ui-runtime.ts`: Trace attention, terminal reconciliation, and Pi follow-up delivery.
|
|
61
61
|
- automatic draft/tool review domains: structurally redacted model review, journaled mutation, lineage, recovery, and explicit retry/reset safety.
|
|
62
62
|
|
|
63
|
-
The bundled Skill Recipe dependency DAG is `artifacts → actors, swarm
|
|
63
|
+
The bundled Skill Recipe dependency DAG is `artifacts → actors, swarm` and `project-work → actors, artifacts, swarm`; `actors`, `music-player`, `swarm`, and `recipe-memory` have no cross-Skill Recipe dependencies.
|
|
64
64
|
|
|
65
65
|
Scripts remain self-contained when no non-script consumer justifies a TypeScript domain. Command-template script leaves infer `.js`/`.mjs` in order through Node, Bun, or `deno run` and `.sh` through Bash without shell evaluation. Helper-backed Skill Recipes self-locate through runtime-owned `{skill_dir}`. Recipes stay optional, composable, policy-light, and caller-configurable.
|
|
66
66
|
|
package/BACKLOG.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
# Project Backlog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
- [ ] `Linux MPRIS media integration`: Expose the active `music-player/playback` singleton as one optional generation-fenced MPRIS2 player so GNOME and compatible desktop shells can show current media and native controls without making D-Bus a second playback authority.
|
|
4
|
+
- [ ] Publish `PlaybackStatus`, track metadata, duration, read-time position, volume, and supported capabilities under one stable session-scoped bus identity; disappear cleanly when the Run stops or its generation is replaced, and fail soft when the user D-Bus session is unavailable.
|
|
5
|
+
- [ ] Map `Play`, `Pause`, `PlayPause`, `Next`, `Previous`, `Stop`, `Seek`, `SetPosition`, and `Volume` back into the existing generation-fenced music-player Control/helper contract rather than signaling the backend or editing Run state directly.
|
|
6
|
+
- [ ] Validate deterministic D-Bus contract behavior plus a live GNOME smoke showing the media surface, metadata, progress, volume, and controls while preserving backend independence and exact Actor ownership.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.49.0: Skill-Scoped Music Player
|
|
8
|
+
|
|
9
|
+
- `Skill-Scoped Singletons`: Added one optional async singleton Recipe per active Skill with canonical `run:<skill>` and `<skill>/<recipe>` identities, idempotent compatible reuse, lifecycle/process fencing, terminal-generation replacement, delegation-safe identity inheritance, persistent actor-owned state directories, focused inspection, and fail-closed conflicts across Recipe, owner, startup values, and Control.
|
|
10
|
+
- `Terminal Attention Dedupe`: Retains seen Trace-attention identities while terminal Runs remain observable, preventing periodic reconciliation from replaying the same command follow-ups after the terminal transition.
|
|
11
|
+
- `Focused Music Player`: Replaces broad Media with one `music-player/playback` singleton for files, directories, URLs, and playlists. It checkpoints queue and paused intent, reports structured status/Trace, rejects Actor/standalone ownership collisions, and recovers malformed checkpoints explicitly. Live Generative App validation confirmed fresh status and exact terminal Controls across the complete action set.
|
|
12
|
+
- `Absolute Volume Control`: Adds generation-fenced `volume` with `{ "percent": 0..100 }`, direct helper parity, structured status/checkpoint/Trace evidence, and fail-before-admission invalid-input handling. Linux WirePlumber resolves the exact playback stream by process identity and changes it in place; unsupported environments retain the restart fallback. UIs resolve relative steps to one absolute percentage.
|
|
13
|
+
- `Percentage Seeking`: Adds generation-fenced `seek` with `{ "percent": 0..100 }`, bounded duration probing, read-time progress percentage, structured status/checkpoint/Trace evidence, and supported-backend restart at the resolved offset while preserving track and paused/playing intent; unavailable duration or backend support fails explicitly.
|
|
14
|
+
- `Portable Playback Protocol`: Adds actor-neutral foreground `serve` and a pure generation-fenced `playback-client.mjs` for structured status and bounded controls. The Recipe remains the sole managed lifecycle owner; standalone foreground ownership is explicit and mutually exclusive, with no hidden daemon start, adoption, or supervisor handoff.
|
|
15
|
+
- `Optional Generative App`: Ships a ready `genapps/music-player.mjs` adapter for a co-installed generic Generative App runtime. It reports Actor availability and waits for the exact terminal canonical Actor Control for every mutation, including failed-action propagation; Pi remains the lifecycle composition root. Progress and volume use symmetric seven-button `0..90` scales in steps of 15.
|
|
16
|
+
|
|
17
|
+
## 0.48.1: Persistent Skill Composition
|
|
18
|
+
|
|
19
|
+
- `Persistent Skill Composition`: Made `register_tool from=<skill>/<recipe>` use Pi's authoritative active-session Skill snapshot across every admitted Skill location and activate synchronous or asynchronous tools from the resolved effective contract. Compact user Recipes retain logical delegation without copied contracts, absolute helper paths, symlinks, or ambient runtime re-resolution.
|
|
20
|
+
|
|
5
21
|
## 0.48.0: Host-Coordinated Swarms
|
|
6
22
|
|
|
7
23
|
- `Coordinator And Swarm Methodology`: Defined gatewayless host coordination with companion transports as presence only; the coordinator accepts declarative outcomes, creates explicit Runs, stays available, and owns integration/final validation. Reasoning is role-allocated: bounded authors default off, independent reviewers/integrators use medium, and the coordinator selects evidence-worthy fanout. Swarm retains overhead admission, disjoint ownership, isolation, mutation freeze, and event/timer observation.
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ For local development:
|
|
|
29
29
|
pi install /path/to/pi-actors
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The package contributes the extension and six capability-owning Skills for actors, artifacts,
|
|
32
|
+
The package contributes the extension and six capability-owning Skills for actors, artifacts, music playback, project work, Recipe memory, and swarm orchestration.
|
|
33
33
|
|
|
34
34
|
## Public Tools
|
|
35
35
|
|
|
@@ -40,7 +40,7 @@ Create a Run from an active-Skill Recipe, an explicit Recipe file, or an inline
|
|
|
40
40
|
```text
|
|
41
41
|
spawn template="sleep 30" as=run:demo
|
|
42
42
|
spawn recipe=project-work/repo-health values={"repo":"/work/project","model":"provider/model"}
|
|
43
|
-
spawn recipe=
|
|
43
|
+
spawn recipe=music-player/playback values={"source":"/music"}
|
|
44
44
|
spawn template="make test" as=run:test
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -80,7 +80,7 @@ inspect target=run:test view=trace source=lifecycle lines=40
|
|
|
80
80
|
inspect target=run:test view=control
|
|
81
81
|
inspect target=runtime view=status
|
|
82
82
|
inspect target=recipes view=status
|
|
83
|
-
inspect target=recipes view=doctor identity=
|
|
83
|
+
inspect target=recipes view=doctor identity=music-player/playback
|
|
84
84
|
inspect target=tool:my_tool view=status
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -203,7 +203,7 @@ Useful entry points include:
|
|
|
203
203
|
- `project-work/release-readiness`
|
|
204
204
|
- `swarm/quorum-review`
|
|
205
205
|
- `artifacts/bundle`
|
|
206
|
-
- `
|
|
206
|
+
- `music-player/playback` — singleton controlled playback service
|
|
207
207
|
- `actors/resource-locker` — optional controlled resource-lock service
|
|
208
208
|
|
|
209
209
|
Validate Recipes with:
|
package/dist/lib/async-runs.d.ts
CHANGED
|
@@ -33,6 +33,9 @@ export interface AsyncRunStartParams {
|
|
|
33
33
|
name?: string;
|
|
34
34
|
ownerId?: string;
|
|
35
35
|
run_id?: string;
|
|
36
|
+
singleton?: boolean;
|
|
37
|
+
singleton_run_id?: string;
|
|
38
|
+
singleton_recipe_id?: string;
|
|
36
39
|
state_dir?: string;
|
|
37
40
|
tool?: string;
|
|
38
41
|
template?: CommandTemplateValue;
|
|
@@ -94,6 +97,10 @@ export interface AsyncRunMeta {
|
|
|
94
97
|
process_identity?: RunProcessIdentity;
|
|
95
98
|
recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
|
|
96
99
|
retire_when?: "children_terminal";
|
|
100
|
+
reused?: boolean;
|
|
101
|
+
singleton?: boolean;
|
|
102
|
+
singleton_recipe_id?: string;
|
|
103
|
+
singleton_values?: Record<string, unknown>;
|
|
97
104
|
transport_context?: Record<string, unknown>;
|
|
98
105
|
}
|
|
99
106
|
export { safeRunId } from "./runs-identity.ts";
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, } from "node:fs";
|
|
8
8
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { isDeepStrictEqual } from "node:util";
|
|
10
11
|
import { writeJsonAtomic } from "./file-state.js";
|
|
11
12
|
import { CURRENT_MODEL_VALUE_KEY, CURRENT_THINKING_VALUE_KEY, describeCurrentPolicyProvenance, } from "./model-context.js";
|
|
12
13
|
import * as Paths from "./paths.js";
|
|
@@ -105,6 +106,32 @@ function resolveStateDir(params, run) {
|
|
|
105
106
|
function assertNoActiveRunState(stateDir) {
|
|
106
107
|
RunsStart.assertNoActiveRunState(stateDir, readJson, RUNNER_PATH);
|
|
107
108
|
}
|
|
109
|
+
function reuseCompatibleSingletonRun(stateDir, startParams, singletonValues) {
|
|
110
|
+
const existing = RunsStart.readActiveOwnedRunState(stateDir, readJson, RUNNER_PATH);
|
|
111
|
+
if (!existing)
|
|
112
|
+
return undefined;
|
|
113
|
+
const compatible = existing.singleton === true &&
|
|
114
|
+
existing.singleton_recipe_id === startParams.singleton_recipe_id &&
|
|
115
|
+
existing.ownerId === startParams.ownerId &&
|
|
116
|
+
isDeepStrictEqual(existing.singleton_values ?? {}, singletonValues) &&
|
|
117
|
+
isDeepStrictEqual(existing.control ?? [], startParams.control ?? []);
|
|
118
|
+
if (!compatible) {
|
|
119
|
+
throw new Error(`Active singleton Run ${String(existing.run ?? stateDir)} has incompatible Recipe identity, owner, startup values, or Control contract. Stop it before changing singleton configuration.`);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
...existing,
|
|
123
|
+
...(startParams.launch_source
|
|
124
|
+
? {
|
|
125
|
+
launch_kind: startParams.launch_source,
|
|
126
|
+
launch_source: startParams.launch_source,
|
|
127
|
+
}
|
|
128
|
+
: {}),
|
|
129
|
+
...(startParams.launch_correlation
|
|
130
|
+
? { launch_correlation: startParams.launch_correlation }
|
|
131
|
+
: {}),
|
|
132
|
+
reused: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
108
135
|
function resolveRecipeFile(file, cwd, options) {
|
|
109
136
|
const path = RecipesReferences.resolveRecipeReferencePath(file, cwd, options.skillContext);
|
|
110
137
|
if (path)
|
|
@@ -147,10 +174,23 @@ function resolveStartParams(params, cwd, options) {
|
|
|
147
174
|
if (!params.file)
|
|
148
175
|
return params;
|
|
149
176
|
const fileParams = readRecipeFile(params.file, cwd, options);
|
|
177
|
+
const singleton = fileParams.singleton === true;
|
|
178
|
+
if (singleton && fileParams.async !== true) {
|
|
179
|
+
throw new Error("singleton Recipes must declare async: true");
|
|
180
|
+
}
|
|
181
|
+
if (singleton && (!fileParams.singleton_run_id || !fileParams.singleton_recipe_id)) {
|
|
182
|
+
throw new Error("singleton Recipes must resolve one active Skill-owned singleton identity");
|
|
183
|
+
}
|
|
184
|
+
const singletonRun = singleton ? fileParams.singleton_run_id : undefined;
|
|
185
|
+
if (singletonRun && params.run_id && params.run_id !== singletonRun) {
|
|
186
|
+
throw new Error(`singleton Recipe run identity is run:${singletonRun}; received run:${params.run_id}`);
|
|
187
|
+
}
|
|
150
188
|
return {
|
|
151
189
|
...fileParams,
|
|
152
190
|
...params,
|
|
153
|
-
|
|
191
|
+
...(singleton ? { singleton: true } : {}),
|
|
192
|
+
run_id: singletonRun ||
|
|
193
|
+
params.run_id ||
|
|
154
194
|
fileParams.run_id ||
|
|
155
195
|
fileParams.name ||
|
|
156
196
|
getRunIdFromFile(fileParams.file),
|
|
@@ -282,14 +322,27 @@ export function startRun(params, cwd, options = {}) {
|
|
|
282
322
|
values: startParams.policy_values ?? startParams.values ?? {},
|
|
283
323
|
});
|
|
284
324
|
assertCurrentPlaceholderReferencesResolved(resolved.template, startParams.defaults, values, modelPolicy);
|
|
285
|
-
|
|
325
|
+
const singletonValues = Object.fromEntries([...declaredArgs].map((key) => [key, values[key]]));
|
|
326
|
+
if (startParams.singleton !== true)
|
|
327
|
+
assertNoActiveRunState(stateDir);
|
|
286
328
|
mkdirSync(stateDir, { recursive: true });
|
|
287
329
|
const releaseStartLock = acquireStateStartLock(stateDir, {
|
|
288
330
|
onContention: startParams.lifecycleHooks?.onLockContention,
|
|
289
331
|
});
|
|
290
332
|
try {
|
|
291
333
|
claimRunStateDirectory(stateDir, run);
|
|
292
|
-
|
|
334
|
+
if (recipeFile &&
|
|
335
|
+
isMutableUsageRecipeFile(recipeFile) &&
|
|
336
|
+
!RecipesUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn")) {
|
|
337
|
+
throw new Error(`Recipe launch rejected because its source changed during activation: ${recipeFile}. Reload recipe tools and retry.`);
|
|
338
|
+
}
|
|
339
|
+
if (startParams.singleton === true) {
|
|
340
|
+
const existing = reuseCompatibleSingletonRun(stateDir, startParams, singletonValues);
|
|
341
|
+
if (existing)
|
|
342
|
+
return existing;
|
|
343
|
+
}
|
|
344
|
+
else
|
|
345
|
+
assertNoActiveRunState(stateDir);
|
|
293
346
|
prepareStateDirForStart(stateDir);
|
|
294
347
|
const stdout = join(stateDir, "stdout.log");
|
|
295
348
|
const stderr = join(stateDir, "stderr.log");
|
|
@@ -307,11 +360,6 @@ export function startRun(params, cwd, options = {}) {
|
|
|
307
360
|
}
|
|
308
361
|
: record)
|
|
309
362
|
: undefined;
|
|
310
|
-
if (recipeFile &&
|
|
311
|
-
isMutableUsageRecipeFile(recipeFile) &&
|
|
312
|
-
!RecipesUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn")) {
|
|
313
|
-
throw new Error(`Recipe launch rejected because its source changed during activation: ${recipeFile}. Reload recipe tools and retry.`);
|
|
314
|
-
}
|
|
315
363
|
const outFd = openSync(stdout, "a");
|
|
316
364
|
const errFd = openSync(stderr, "a");
|
|
317
365
|
const argv = asyncRunnerArgv(stateDir);
|
|
@@ -360,6 +408,13 @@ export function startRun(params, cwd, options = {}) {
|
|
|
360
408
|
...(startParams.retire_when === "children_terminal"
|
|
361
409
|
? { retire_when: "children_terminal" }
|
|
362
410
|
: {}),
|
|
411
|
+
...(startParams.singleton === true
|
|
412
|
+
? {
|
|
413
|
+
singleton: true,
|
|
414
|
+
singleton_recipe_id: startParams.singleton_recipe_id,
|
|
415
|
+
singleton_values: singletonValues,
|
|
416
|
+
}
|
|
417
|
+
: {}),
|
|
363
418
|
...(transportContext
|
|
364
419
|
? { transport_context: transportContext } : {}),
|
|
365
420
|
};
|
|
@@ -127,6 +127,9 @@ export function createActorExtensionRuntime(pi) {
|
|
|
127
127
|
Pi.registerToolDefinitions(pi, Tools.createCoreActorToolDefinitions({
|
|
128
128
|
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
129
129
|
getActiveTools: () => pi.getActiveTools(),
|
|
130
|
+
getRecipeResolutionContext: () => activeRunContext
|
|
131
|
+
? getRecipeResolutionContext(activeRunContext)
|
|
132
|
+
: undefined,
|
|
130
133
|
getRuntimeTool: (name) => Tools.resolveActiveRuntimeTool(name, runtime.getTools(), (activeName) => actorToolDefinitions.get(activeName)),
|
|
131
134
|
getRuntimeToolStatus: runtime.getToolStatus,
|
|
132
135
|
handleRuntimeControl: automaticReview.handleControl,
|
package/dist/lib/file-state.js
CHANGED
|
@@ -273,13 +273,27 @@ export function withFileMutationLock(path, mutate, options = {}) {
|
|
|
273
273
|
release();
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
|
+
function replaceFileWithRetry(source, target) {
|
|
277
|
+
for (let attempt = 0;; attempt += 1) {
|
|
278
|
+
try {
|
|
279
|
+
renameSync(source, target);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
const code = error.code;
|
|
284
|
+
if ((code !== "EPERM" && code !== "EBUSY") || attempt >= 19)
|
|
285
|
+
throw error;
|
|
286
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
276
290
|
export function writeTextAtomic(path, content, options = {}) {
|
|
277
291
|
mkdirSync(dirname(path), { recursive: true });
|
|
278
292
|
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
279
293
|
try {
|
|
280
294
|
writeFileSync(tempPath, content, "utf8");
|
|
281
295
|
options.onBeforeReplace?.();
|
|
282
|
-
|
|
296
|
+
replaceFileWithRetry(tempPath, path);
|
|
283
297
|
}
|
|
284
298
|
catch (error) {
|
|
285
299
|
try {
|
package/dist/lib/inspector.js
CHANGED
|
@@ -117,6 +117,9 @@ export function readActorInspectorRecipe(stateDir) {
|
|
|
117
117
|
source_kind: primarySourceKind,
|
|
118
118
|
launch_kind: meta.launch_kind ?? meta.launch_source,
|
|
119
119
|
launch_source: meta.launch_source,
|
|
120
|
+
...(meta.singleton === true
|
|
121
|
+
? { singleton: true, singleton_recipe_id: meta.singleton_recipe_id }
|
|
122
|
+
: {}),
|
|
120
123
|
}),
|
|
121
124
|
launch: redactedRecord({
|
|
122
125
|
cwd: meta.cwd,
|
|
@@ -127,6 +130,7 @@ export function readActorInspectorRecipe(stateDir) {
|
|
|
127
130
|
artifacts: meta.artifacts,
|
|
128
131
|
notification_policy: meta.notification_policy,
|
|
129
132
|
retire_when: meta.retire_when,
|
|
133
|
+
singleton_values: meta.singleton_values,
|
|
130
134
|
}),
|
|
131
135
|
};
|
|
132
136
|
}
|
|
@@ -786,9 +786,6 @@ function readTraceAttentionRecords(run) {
|
|
|
786
786
|
export function pruneRunObservationState(previousStatuses, previousLineCounts, summary, terminalRuns = [], seenEventIds = new Map()) {
|
|
787
787
|
const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
|
|
788
788
|
const terminalRunSet = new Set(terminalRuns);
|
|
789
|
-
const terminalLineKeys = new Set(summary.runs
|
|
790
|
-
.filter((run) => terminalRunSet.has(runObservationKey(run)))
|
|
791
|
-
.map((run) => runObservationKey(run)));
|
|
792
789
|
const activeLineKeys = new Set(summary.runs.map((run) => run.stateDir ?? run.run));
|
|
793
790
|
for (const run of terminalRunSet)
|
|
794
791
|
previousStatuses.delete(run);
|
|
@@ -796,10 +793,10 @@ export function pruneRunObservationState(previousStatuses, previousLineCounts, s
|
|
|
796
793
|
if (!activeRuns.has(run))
|
|
797
794
|
previousStatuses.delete(run);
|
|
798
795
|
for (const key of previousLineCounts.keys())
|
|
799
|
-
if (
|
|
796
|
+
if (!activeLineKeys.has(key))
|
|
800
797
|
previousLineCounts.delete(key);
|
|
801
798
|
for (const key of seenEventIds.keys())
|
|
802
|
-
if (
|
|
799
|
+
if (!activeLineKeys.has(key))
|
|
803
800
|
seenEventIds.delete(key);
|
|
804
801
|
}
|
|
805
802
|
export function detectRunAttentionEvents(legacyLineCounts, summary, seenEventIds = new Map(), prime = false) {
|
|
@@ -707,7 +707,9 @@ export function summarizeRegisteredToolArgs(tool) {
|
|
|
707
707
|
const requiredSet = new Set(required);
|
|
708
708
|
const optional = publicArgs.filter((arg) => !requiredSet.has(arg));
|
|
709
709
|
if (tool.recipe?.async === true) {
|
|
710
|
-
|
|
710
|
+
if (tool.recipe.singleton !== true)
|
|
711
|
+
optional.push("run_id");
|
|
712
|
+
optional.push("transport_context");
|
|
711
713
|
}
|
|
712
714
|
return { optional, required };
|
|
713
715
|
}
|
|
@@ -14,6 +14,7 @@ export type TemplateRecipeImport = string | TemplateRecipeImportBinding;
|
|
|
14
14
|
export interface TemplateRecipeDefinition {
|
|
15
15
|
description?: string;
|
|
16
16
|
disabled?: boolean;
|
|
17
|
+
singleton?: boolean;
|
|
17
18
|
imports?: Record<string, TemplateRecipeImport>;
|
|
18
19
|
template: CommandTemplateValue;
|
|
19
20
|
args?: string[];
|
|
@@ -42,6 +43,8 @@ export interface TemplateRecipeConfig extends TemplateRecipeDefinition {
|
|
|
42
43
|
async?: boolean;
|
|
43
44
|
recipe_dir?: string;
|
|
44
45
|
skill_dir?: string;
|
|
46
|
+
singleton_run_id?: string;
|
|
47
|
+
singleton_recipe_id?: string;
|
|
45
48
|
}
|
|
46
49
|
export interface TemplateRecipeContextRecord {
|
|
47
50
|
alias?: string;
|
|
@@ -970,6 +970,21 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
970
970
|
: delegated?.async === false
|
|
971
971
|
? { async: false }
|
|
972
972
|
: {}),
|
|
973
|
+
...(substituted.singleton === true || delegated?.singleton === true
|
|
974
|
+
? {
|
|
975
|
+
singleton: true,
|
|
976
|
+
singleton_run_id: delegated?.singleton === true
|
|
977
|
+
? delegated.singleton_run_id
|
|
978
|
+
: skillDir
|
|
979
|
+
? basename(skillDir)
|
|
980
|
+
: undefined,
|
|
981
|
+
singleton_recipe_id: delegated?.singleton === true
|
|
982
|
+
? delegated.singleton_recipe_id
|
|
983
|
+
: skillDir
|
|
984
|
+
? `${basename(skillDir)}/${recipeName}`
|
|
985
|
+
: undefined,
|
|
986
|
+
}
|
|
987
|
+
: {}),
|
|
973
988
|
...(Object.keys(imports).length > 0
|
|
974
989
|
? { imports: getRecipeImports(raw) }
|
|
975
990
|
: {}),
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as CommandTemplates from "./command-templates.ts";
|
|
7
7
|
import * as Config from "./config.ts";
|
|
8
|
+
import * as RecipesContext from "./recipes-context.ts";
|
|
8
9
|
export interface RegisterToolInput {
|
|
9
10
|
name?: string;
|
|
10
11
|
description?: string;
|
|
@@ -55,6 +56,7 @@ export interface RegisterToolRuntimeDeps<TContext> {
|
|
|
55
56
|
recipeRoot?: string;
|
|
56
57
|
getToolNameBlocker: (name: string) => string | undefined;
|
|
57
58
|
getTools: () => Map<string, Config.RegisteredTool>;
|
|
59
|
+
getRecipeResolutionContext?: () => RecipesContext.RecipeResolutionContext | undefined;
|
|
58
60
|
getActiveTools: () => string[];
|
|
59
61
|
notify: (ctx: TContext, message: string, type: "info" | "warning" | "error") => void;
|
|
60
62
|
registerRuntimeTool: (cfg: Config.RegisteredTool) => RuntimeActivation | void;
|
package/dist/lib/registry.js
CHANGED
|
@@ -233,9 +233,10 @@ function getInputTemplate(value) {
|
|
|
233
233
|
throw new Error(ExecutionOutput.formatToolText("Tool template must be a string, object, or sequence."));
|
|
234
234
|
}
|
|
235
235
|
function getRegistrationResolutionContext(ctx, deps) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
const runtimeContext = deps.getRecipeResolutionContext?.();
|
|
237
|
+
if (runtimeContext)
|
|
238
|
+
return runtimeContext;
|
|
239
|
+
if (ctx && typeof ctx === "object" && "recipeResolutionContext" in ctx) {
|
|
239
240
|
const resolutionContext = ctx.recipeResolutionContext;
|
|
240
241
|
if (resolutionContext)
|
|
241
242
|
return resolutionContext;
|
|
@@ -415,15 +416,18 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
|
415
416
|
let persisted = false;
|
|
416
417
|
let activation;
|
|
417
418
|
let cfg;
|
|
419
|
+
let transactionStage = "persist";
|
|
418
420
|
try {
|
|
419
421
|
persistToolRecipe(deps, name, authoredRecipe);
|
|
420
422
|
persisted = true;
|
|
423
|
+
transactionStage = "persisted_admission";
|
|
421
424
|
const admitted = RecipesDiscovery.admitUserRecipe(recipePath, resolutionContext);
|
|
422
425
|
if (!admitted.validated || !admitted.tool) {
|
|
423
426
|
throw new Error(`Persisted tool recipe admission failed: ${admitted.diagnostics.join("; ")}`);
|
|
424
427
|
}
|
|
425
428
|
cfg = admitted.tool;
|
|
426
429
|
tools.set(name, cfg);
|
|
430
|
+
transactionStage = "runtime_activation";
|
|
427
431
|
activation = deps.registerRuntimeTool(cfg) ?? undefined;
|
|
428
432
|
if (activation &&
|
|
429
433
|
(!activation.host_registered ||
|
|
@@ -447,7 +451,7 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
|
447
451
|
tools.delete(name);
|
|
448
452
|
}
|
|
449
453
|
deps.setActiveTools(activeBefore);
|
|
450
|
-
throw new Error(ExecutionOutput.formatToolText(`Tool registration transaction failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
454
|
+
throw new Error(ExecutionOutput.formatToolText(`Tool registration transaction failed at ${transactionStage} (${resolutionContext.generation}; active Skills: ${activeSkillSummary(resolutionContext)}): ${error instanceof Error ? error.message : String(error)}`));
|
|
451
455
|
}
|
|
452
456
|
deps.notify(ctx, `Tool activated: ${name}`, "info");
|
|
453
457
|
const templateWarnings = CommandTemplates.getCommandTemplateWarnings(typeof cfg.recipe?.template === "object" && !Array.isArray(cfg.recipe.template)
|
package/dist/lib/runs-start.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type FileMutationLockOptions } from "./file-state.ts";
|
|
7
7
|
type RunJsonReader = (path: string) => Record<string, unknown> | undefined;
|
|
8
|
-
export declare function
|
|
8
|
+
export declare function readActiveOwnedRunState(stateDir: string, readJson: RunJsonReader, _runnerPath: string): Record<string, unknown> | undefined;
|
|
9
|
+
export declare function assertNoActiveRunState(stateDir: string, readJson: RunJsonReader, runnerPath: string): void;
|
|
9
10
|
export declare function acquireStateStartLock(stateDir: string, options?: FileMutationLockOptions): () => void;
|
|
10
11
|
export declare function prepareStateDirForStart(stateDir: string, readJson: RunJsonReader, _runnerPath: string): void;
|
|
11
12
|
export {};
|
package/dist/lib/runs-start.js
CHANGED
|
@@ -7,14 +7,16 @@ import { rmSync } from "node:fs";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { acquireFileMutationLock, } from "./file-state.js";
|
|
9
9
|
import { isAlive, verifyRunProcessIdentity, } from "./runs-process.js";
|
|
10
|
-
export function
|
|
10
|
+
export function readActiveOwnedRunState(stateDir, readJson, _runnerPath) {
|
|
11
11
|
const meta = readJson(join(stateDir, "run.json"));
|
|
12
12
|
if (!meta)
|
|
13
|
-
return;
|
|
13
|
+
return undefined;
|
|
14
|
+
const result = readJson(join(stateDir, "result.json"));
|
|
15
|
+
if (typeof result?.completedAt === "string")
|
|
16
|
+
return undefined;
|
|
14
17
|
const pid = Number(meta.pid || 0);
|
|
15
|
-
const cwd = String(meta.cwd ?? "");
|
|
16
18
|
if (!pid || !isAlive(pid))
|
|
17
|
-
return;
|
|
19
|
+
return undefined;
|
|
18
20
|
const identity = verifyRunProcessIdentity(pid, meta.process_identity);
|
|
19
21
|
if (identity.status === "owner_mismatch") {
|
|
20
22
|
throw new Error(`Run state process identity does not match the live pid: ${String(meta.run ?? stateDir)}. Refusing to reuse the state directory while pid ${pid} is alive.`);
|
|
@@ -22,7 +24,11 @@ export function assertNoActiveRunState(stateDir, readJson, _runnerPath) {
|
|
|
22
24
|
if (identity.status === "unsupported_proof") {
|
|
23
25
|
throw new Error(`Run state process identity proof is unavailable: ${String(meta.run ?? stateDir)}. Refusing to reuse the state directory while pid ${pid} is alive.`);
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
return identity.valid ? meta : undefined;
|
|
28
|
+
}
|
|
29
|
+
export function assertNoActiveRunState(stateDir, readJson, runnerPath) {
|
|
30
|
+
const meta = readActiveOwnedRunState(stateDir, readJson, runnerPath);
|
|
31
|
+
if (meta) {
|
|
26
32
|
throw new Error(`Run state already has an active owned process: ${String(meta.run ?? stateDir)}. Stop it before reusing the same run_id or state_dir.`);
|
|
27
33
|
}
|
|
28
34
|
}
|
package/dist/lib/tools-local.js
CHANGED
|
@@ -93,8 +93,10 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
93
93
|
const paramSchema = {};
|
|
94
94
|
const required = [];
|
|
95
95
|
const isRecipe = RecipesReferences.isRecipeTool(cfg.template, cfg.recipe);
|
|
96
|
-
const isAsyncRecipe = cfg.recipe
|
|
97
|
-
|
|
96
|
+
const isAsyncRecipe = cfg.recipe
|
|
97
|
+
? cfg.recipe.async === true
|
|
98
|
+
: RecipesReferences.isAsyncRecipeReference(cfg.template);
|
|
99
|
+
const isSingletonRecipe = cfg.recipe?.singleton === true;
|
|
98
100
|
const recipeTemplate = cfg.recipe?.template ?? RecipesReferences.getRecipeTemplate(cfg.template);
|
|
99
101
|
const requiredTemplate = recipeTemplate ?? cfg.template;
|
|
100
102
|
const requiredTemplateConfig = typeof requiredTemplate === "object" && !Array.isArray(requiredTemplate)
|
|
@@ -112,7 +114,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
112
114
|
const requiredArgs = isRecipe && cfg.storedArgs !== undefined
|
|
113
115
|
? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg) &&
|
|
114
116
|
!Object.hasOwn(recipeInlineDefaults, arg)))
|
|
115
|
-
: RecipesReferences.isRecipeReference(cfg.template) && !recipeTemplate
|
|
117
|
+
: !cfg.recipe && RecipesReferences.isRecipeReference(cfg.template) && !recipeTemplate
|
|
116
118
|
? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
|
|
117
119
|
: Schema.getRequiredToolArgNames(requiredTemplateConfig);
|
|
118
120
|
for (const arg of cfg.args) {
|
|
@@ -122,7 +124,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
122
124
|
if (requiredArgs.has(arg))
|
|
123
125
|
required.push(arg);
|
|
124
126
|
}
|
|
125
|
-
if (isAsyncRecipe)
|
|
127
|
+
if (isAsyncRecipe && !isSingletonRecipe)
|
|
126
128
|
paramSchema.run_id = Schema.stringSchema("Optional run id override for this async template recipe invocation.");
|
|
127
129
|
if (isAsyncRecipe) {
|
|
128
130
|
paramSchema.transport_context = Schema.looseObjectSchema("Optional originating transport route preserved for detached terminal follow-up.");
|
|
@@ -145,9 +147,11 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
145
147
|
const input = params;
|
|
146
148
|
const { run_id, transport_context, ...values } = input;
|
|
147
149
|
const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
|
|
148
|
-
const runId =
|
|
149
|
-
?
|
|
150
|
-
:
|
|
150
|
+
const runId = isSingletonRecipe
|
|
151
|
+
? undefined
|
|
152
|
+
: typeof run_id === "string" && run_id.trim()
|
|
153
|
+
? run_id.trim()
|
|
154
|
+
: `${cfg.name}-${Date.now()}`;
|
|
151
155
|
const meta = AsyncRuns.startRun({
|
|
152
156
|
...base,
|
|
153
157
|
launch_source: "tool",
|
|
@@ -180,7 +184,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
|
|
|
180
184
|
return await Execution.executeRegisteredTool(cfg, Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues(params, ctx), cfg.argTypes), exec, ctx.cwd, signal);
|
|
181
185
|
}
|
|
182
186
|
catch (error) {
|
|
183
|
-
throw formatRuntimeToolArgumentError(cfg, error, required, isAsyncRecipe);
|
|
187
|
+
throw formatRuntimeToolArgumentError(cfg, error, required, isAsyncRecipe && !isSingletonRecipe);
|
|
184
188
|
}
|
|
185
189
|
},
|
|
186
190
|
};
|
package/dist/lib/tools.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ActorToolDefinition {
|
|
|
14
14
|
export interface CoreActorToolDefinitionDeps<TContext extends RuntimeToolContext> {
|
|
15
15
|
configPath: string;
|
|
16
16
|
getActiveTools: () => string[];
|
|
17
|
+
getRecipeResolutionContext: () => import("./recipes-context.ts").RecipeResolutionContext | undefined;
|
|
17
18
|
getRuntimeTool: (name: string) => unknown;
|
|
18
19
|
getRuntimeToolStatus: (name: string) => Record<string, unknown> | undefined;
|
|
19
20
|
handleRuntimeControl?: (action: string, input: unknown) => Record<string, unknown>;
|
package/dist/lib/tools.js
CHANGED
|
@@ -30,6 +30,7 @@ export function createCoreActorToolDefinitions(deps) {
|
|
|
30
30
|
getActiveTools: deps.getActiveTools,
|
|
31
31
|
getToolNameBlocker: deps.registryRuntime.getToolNameBlocker,
|
|
32
32
|
getTools: deps.registryRuntime.getTools,
|
|
33
|
+
getRecipeResolutionContext: deps.getRecipeResolutionContext,
|
|
33
34
|
notify: deps.registryRuntime.notify,
|
|
34
35
|
registerRuntimeTool: deps.registryRuntime.registerRuntimeTool,
|
|
35
36
|
reservedToolNames: RESERVED_TOOL_NAMES,
|
|
@@ -52,18 +52,18 @@ direct delegation ≠ named import composition
|
|
|
52
52
|
Run Control ≠ actor chat
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
A Skill Recipe is a maintained component addressed by `<skill>/<recipe>`. `spawn` creates a Run from a Recipe. `register_tool
|
|
55
|
+
A Skill Recipe is a maintained component addressed by `<skill>/<recipe>`. `spawn` creates a Run from a Recipe. `register_tool from=<skill>/<recipe>` persists compact logical delegation and activates from the resolved effective contract without copying, symlinking, or ambient re-resolution. A tool is callable in the current session only when activation evidence says `callable_now: true`.
|
|
56
56
|
|
|
57
57
|
`actors` owns generic Recipe/tool/Run mechanics. The owning capability Skill owns capability-specific selection and constraints. `swarm` owns multi-actor decomposition and integration methodology.
|
|
58
58
|
|
|
59
59
|
## Persistent capability workflow
|
|
60
60
|
|
|
61
|
-
To make `
|
|
61
|
+
To make `music-player/playback` callable as `music_player` with a default source:
|
|
62
62
|
|
|
63
63
|
```text
|
|
64
64
|
register_tool
|
|
65
65
|
name=music_player
|
|
66
|
-
from=
|
|
66
|
+
from=music-player/playback
|
|
67
67
|
defaults={"source":"~/Music/1MIX"}
|
|
68
68
|
```
|
|
69
69
|
|
|
@@ -22,12 +22,14 @@ Do not mix source modes.
|
|
|
22
22
|
```text
|
|
23
23
|
register_tool
|
|
24
24
|
name=music_player
|
|
25
|
-
from=
|
|
25
|
+
from=music-player/playback
|
|
26
26
|
defaults={"source":"~/Music/1MIX"}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
`from` means logical direct delegation. The source remains authoritative for async behavior, caller args and types, source defaults, artifacts, Control, and runtime-owned origins. The persistent user Recipe stores only the compact specialization; do not copy inherited fields.
|
|
30
30
|
|
|
31
|
+
Resolution uses Pi's authoritative active-session Skill snapshot across every Skill location Pi admits. Registration resolves and validates the maintained source, persists only its logical `<skill>/<recipe>` reference plus caller specialization, and projects the already-resolved effective contract into the runtime tool. Never replace this composition with a copied Recipe, absolute helper path, symlink, or ambient runtime re-resolution.
|
|
32
|
+
|
|
31
33
|
Use `description` to narrow agent-facing intent when useful. Every supplied default must name a caller-owned source arg and satisfy its type or enum. Never default runtime-owned origins.
|
|
32
34
|
|
|
33
35
|
## Prove registration
|
|
@@ -7,14 +7,14 @@ A Recipe is a reusable executable definition. Address a maintained component as
|
|
|
7
7
|
Use direct delegation when the root remains fundamentally the same capability under a different persistent name, description, or caller default:
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
|
|
10
|
+
music-player/playback
|
|
11
11
|
→ music_player with a default source
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
For agent-facing persistent specialization, use:
|
|
15
15
|
|
|
16
16
|
```text
|
|
17
|
-
register_tool from=
|
|
17
|
+
register_tool from=music-player/playback defaults={"source":"~/Music/1MIX"}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
The delegated root inherits async behavior, args and types, defaults, artifacts, Control, and runtime-owned origins. Do not copy those fields into the wrapper.
|
|
@@ -10,6 +10,8 @@ spawn recipe=<skill>/<recipe> values={...} as=run:<id>
|
|
|
10
10
|
|
|
11
11
|
Use the owning capability Skill to choose the Recipe and capability-specific values. Retain the returned Run id. A spawn result reports `launch_kind: "spawn"`; it is never evidence of registered-tool invocation.
|
|
12
12
|
|
|
13
|
+
A rare Skill Recipe may declare `singleton: true`. Do not pass `as`: the runtime derives `run:<skill>` plus the canonical `<skill>/<recipe>` identity, allows at most one singleton Recipe per Skill, and returns the same compatible healthy active Run instead of launching a duplicate. Contradictory Recipe identity, startup values, Control, or ownership fails closed. Delegation inherits both singleton identities instead of retargeting them. A terminal result is never reused; after its runner exits, restart keeps the logical Run id but creates a new fenced generation. Continuity still depends on actor-owned validated state, not the Run id alone.
|
|
14
|
+
|
|
13
15
|
## Observe
|
|
14
16
|
|
|
15
17
|
Normally wait for terminal follow-up. Inspect only when requested, when meaningful attention arrives, or when the Run is overdue or blocked:
|