@naiba/pi-durable-background-tasks 0.1.1 → 0.3.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/README.md +39 -3
- package/dist/coding-agent.d.ts +7 -0
- package/dist/coding-agent.js +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pi.d.ts +1 -1
- package/dist/pi.js +9 -4
- package/dist/wait-all.d.ts +13 -0
- package/dist/wait-all.js +10 -0
- package/package.json +32 -9
- package/src/coding-agent.ts +24 -0
- package/src/index.ts +1 -0
- package/src/pi.ts +9 -5
- package/src/wait-all.ts +9 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Pi Durable Background Tasks
|
|
2
2
|
|
|
3
|
-
Supervised background shell commands for Pi Durable. No delegated agents, model configuration, or implicit model calls.
|
|
3
|
+
Supervised background shell commands for Pi Coding Agent and Pi Durable. Both adapters share one independent process core. No delegated agents, model configuration, or implicit model calls.
|
|
4
4
|
|
|
5
5
|
**Initial development version. Linux only; requires Python 3.9+ and Linux pidfd support.**
|
|
6
6
|
|
|
@@ -8,12 +8,34 @@ Supervised background shell commands for Pi Durable. No delegated agents, model
|
|
|
8
8
|
bun add @naiba/pi-durable-background-tasks
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Available on [npm](https://www.npmjs.com/package/@naiba/pi-durable-background-tasks), with GitHub Actions Trusted Publishing and provenance. Node.js 22+ is required; Bun loads the TypeScript source exports, while Node loads the compiled JavaScript.
|
|
12
|
+
|
|
13
|
+
The host installs the appropriate adapter and owns its lifecycle. This is not an auto-discovered CLI plugin. It does not manage Claude Code or Codex background tasks. All host peer dependencies are optional: importing the core loads neither Pi runtime; importing `./coding-agent` does not load Durable.
|
|
14
|
+
|
|
11
15
|
## Usage
|
|
12
16
|
|
|
17
|
+
For native Pi, install `@earendil-works/pi-coding-agent` (1.5+) and `typebox` (1.4.4+), then add the factory to your `DefaultResourceLoader`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import {BackgroundTasks} from "@naiba/pi-durable-background-tasks";
|
|
21
|
+
import {backgroundTasksExtension} from "@naiba/pi-durable-background-tasks/coding-agent";
|
|
22
|
+
|
|
23
|
+
const tasks = new BackgroundTasks("/persistent/session/background");
|
|
24
|
+
const extension = backgroundTasksExtension(tasks, "/workspace", {
|
|
25
|
+
onStart: task => trackOwnedTask(task.id), // optional host delivery bookkeeping
|
|
26
|
+
});
|
|
27
|
+
// Pass extensionFactories: [extension] to DefaultResourceLoader, reload it,
|
|
28
|
+
// createAgentSession with that loader, then await session.bindExtensions(...).
|
|
29
|
+
// Keep normal tool_call approval hooks; await tasks.close() at host shutdown.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For Durable, install `@earendil-works/pi-durable`, `@earendil-works/pi-ai` (1.2+) and `typebox` (1.4.4+). The existing `./pi` entry point remains unchanged:
|
|
33
|
+
|
|
13
34
|
```ts
|
|
14
35
|
import {BackgroundTasks} from "@naiba/pi-durable-background-tasks";
|
|
15
36
|
import {backgroundTasksExtension} from "@naiba/pi-durable-background-tasks/pi";
|
|
16
37
|
|
|
38
|
+
// `registry` is your host's Pi Durable registry.
|
|
17
39
|
const tasks = new BackgroundTasks("/persistent/session/background");
|
|
18
40
|
registry.install(backgroundTasksExtension(tasks, "/workspace"));
|
|
19
41
|
// Register your normal Pi beforeTool approval policy. This package does not bypass it.
|
|
@@ -23,7 +45,15 @@ await tasks.close();
|
|
|
23
45
|
unsubscribe();
|
|
24
46
|
```
|
|
25
47
|
|
|
26
|
-
|
|
48
|
+
| Tool | Input | Purpose |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `background_start` | `command`, optional `name` | Start a session-owned shell command |
|
|
51
|
+
| `background_status` | `{}` | List commands and their recorded state |
|
|
52
|
+
| `background_output` | `id`, optional `offset`, `limit` | Read up to 64 KiB using byte offsets |
|
|
53
|
+
| `background_wait` | `id`, `timeout_ms` | Observe completion for a bounded interval |
|
|
54
|
+
| `background_wait_all` | `ids`, `timeout_ms` | Observe 1–100 distinct commands concurrently |
|
|
55
|
+
| `background_stop` | `id` | Stop a command and await supervisor cleanup |
|
|
56
|
+
|
|
27
57
|
Wait requires `timeout_ms` between 1 and 600000. Observation timeout/cancellation does not stop the command. Commands have no implicit execution deadline.
|
|
28
58
|
|
|
29
59
|
The core API works without creating a model session. `start` requires a stable `requestId`; repeats return the original record and mismatching arguments are rejected. Recovered records never automatically relaunch a command. Use one service instance per directory and one writer process; concurrent writers are not supported.
|
|
@@ -38,6 +68,12 @@ Records and logs survive reopening; live process continuity across host restart
|
|
|
38
68
|
|
|
39
69
|
Output reads are bounded to 64 KiB, with byte offsets. Logs are retained without automatic deletion; configure filesystem quotas at the host level. Commands run with inherited environment and host permissions. Do not expose this service directly to untrusted clients; isolate task directories by session and authorize every tool/HTTP action.
|
|
40
70
|
|
|
71
|
+
## CloudCode integration
|
|
72
|
+
|
|
73
|
+
[CloudCode](https://github.com/naiba/cloudcode) consumes this npm package for Pi sessions. It provides a collapsed background-activity panel, on-demand logs, explicit stop controls, native Pi approval, and protection against releasing/deleting sessions with active commands. Closing a browser is not host shutdown and does not stop commands.
|
|
74
|
+
|
|
75
|
+
CloudCode uses the native `./coding-agent` adapter, retaining its host-specific approval, completion delivery and strict log-evidence checks. The package itself has no Web UI and does not automatically resume a model when a command completes. Claude Code and Codex continue to use their own native executors.
|
|
76
|
+
|
|
41
77
|
## Development
|
|
42
78
|
|
|
43
79
|
```sh
|
|
@@ -57,4 +93,4 @@ Releases use `.github/workflows/publish.yml` on pushed `v*` tags, with npm Trust
|
|
|
57
93
|
- Workflow filename: `publish.yml`
|
|
58
94
|
- Environment: leave empty (the workflow does not declare an environment)
|
|
59
95
|
|
|
60
|
-
Update `package.json` to a new stable version, refresh `bun.lock` if needed, commit and push, then push the matching tag (for example, `v0.1.
|
|
96
|
+
Update `package.json` to a new stable version, refresh `bun.lock` if needed, commit and push, then push the matching tag (for example, `v0.1.2` for version `0.1.2`). The tag-triggered publish workflow first calls the reusable `test.yml` workflow on the same tag. Its publish job uses `needs: test`, so failed or cancelled tests prevent publication. Branch pushes and pull requests also run `test.yml`; tag pushes avoid a redundant standalone test run. After tests succeed, the publish job rejects mismatching or prerelease versions, rebuilds the package, then publishes publicly with provenance. Published versions are immutable: `0.1.0` and `0.1.1` already exist and must not be republished. Never move a published release tag. There is no manual-dispatch publish path.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { BackgroundTasks, type BackgroundTask } from "./index.js";
|
|
3
|
+
export interface BackgroundTasksExtensionOptions {
|
|
4
|
+
/** Called after start, including a deduplicated start. Host owns delivery policy. */
|
|
5
|
+
onStart?: (task: BackgroundTask) => void | Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function backgroundTasksExtension(service: BackgroundTasks, cwd: string, options?: BackgroundTasksExtensionOptions): ExtensionFactory;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { waitForBackgroundTasks } from "./index.js";
|
|
4
|
+
// Coding Agent consumes content; Durable consumes output. Separate entry points
|
|
5
|
+
// prevent a native Pi installation from loading the Durable runtime.
|
|
6
|
+
const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }], details: {} });
|
|
7
|
+
export function backgroundTasksExtension(service, cwd, options = {}) {
|
|
8
|
+
return pi => {
|
|
9
|
+
pi.registerTool(defineTool({ name: "background_start", label: "Start background command", description: "Start a supervised command and continue independent work. Commands do not survive host loss. Approval belongs to the host tool policy.", parameters: Type.Object({ command: Type.String(), name: Type.Optional(Type.String()) }), execute: async (id, args) => {
|
|
10
|
+
const task = service.start({ ...args, cwd, requestId: id });
|
|
11
|
+
await options.onStart?.(task);
|
|
12
|
+
return result(task);
|
|
13
|
+
} }));
|
|
14
|
+
pi.registerTool(defineTool({ name: "background_status", label: "Background status", description: "List session-owned background commands.", parameters: Type.Object({}), execute: async () => result(service.list()) }));
|
|
15
|
+
pi.registerTool(defineTool({ name: "background_output", label: "Background output", description: "Read bounded background output using byte offsets.", parameters: Type.Object({ id: Type.String(), offset: Type.Optional(Type.Integer({ minimum: 0 })), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 65536 })) }), execute: async (_id, args) => result(await service.output(args.id, args.offset, args.limit)) }));
|
|
16
|
+
pi.registerTool(defineTool({ name: "background_wait", label: "Wait for command", description: "Observe a command only when no independent work remains. Timeout ends observation, not execution.", parameters: Type.Object({ id: Type.String(), timeout_ms: Type.Integer({ minimum: 1, maximum: 600000 }) }), execute: async (_id, args, signal) => result(await service.wait(args.id, args.timeout_ms, signal)) }));
|
|
17
|
+
pi.registerTool(defineTool({ name: "background_wait_all", label: "Wait for commands", description: "Observe selected commands concurrently after independent work is finished. Timeout does not stop commands.", parameters: Type.Object({ ids: Type.Array(Type.String(), { minItems: 1, maxItems: 100, uniqueItems: true }), timeout_ms: Type.Integer({ minimum: 1, maximum: 600000 }) }), execute: async (_id, args, signal) => result(await waitForBackgroundTasks(service, args.ids, args.timeout_ms, signal)) }));
|
|
18
|
+
pi.registerTool(defineTool({ name: "background_stop", label: "Stop command", description: "Stop a session-owned command and reap its descendants.", parameters: Type.Object({ id: Type.String() }), execute: async (_id, args) => result(await service.stop(args.id)) }));
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { open } from "node:fs/promises";
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
export { waitForBackgroundTasks } from "./wait-all.js";
|
|
7
8
|
export class BackgroundTasks {
|
|
8
9
|
python;
|
|
9
10
|
records = new Map();
|
package/dist/pi.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { Type } from "
|
|
1
|
+
import { Type } from "typebox";
|
|
2
2
|
import { BackgroundTasks } from "./index.js";
|
|
3
3
|
export declare function backgroundTasksExtension(service: BackgroundTasks, cwd: string): import("@earendil-works/pi-durable").Extension<import("@earendil-works/pi-durable").ToolRegistration<Type.TObject<{}>, import("@earendil-works/chord").JsonValue>>;
|
package/dist/pi.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { defineExtension, defineTool } from "@earendil-works/pi-durable";
|
|
2
|
-
import { Type } from "
|
|
3
|
-
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { waitForBackgroundTasks } from "./index.js";
|
|
4
|
+
// Pi 1.2 consumes output; content silently hides task IDs and status from the model.
|
|
5
|
+
const result = (value) => ({ output: [{ type: "text", text: JSON.stringify(value) }] });
|
|
4
6
|
export function backgroundTasksExtension(service, cwd) {
|
|
5
7
|
return defineExtension({ name: "background-tasks", tools: [
|
|
6
|
-
defineTool({ name: "background_start", description: "Start a supervised background shell command. Returns immediately; no model delegation. Commands do not survive host loss. Approval belongs to the host tool policy.", replay: "unsafe",
|
|
8
|
+
defineTool({ name: "background_start", description: "Start a supervised background shell command. Returns immediately; continue independent work before checking results. Do not immediately wait when other work is available; no model delegation. Commands do not survive host loss. Approval belongs to the host tool policy.", replay: "unsafe",
|
|
7
9
|
parameters: Type.Object({ command: Type.String(), name: Type.Optional(Type.String()) }),
|
|
8
10
|
execute: async (args, api) => result(service.start({ ...args, cwd, requestId: String(api.taskId) })) }),
|
|
9
11
|
defineTool({ name: "background_status", description: "List background commands owned by this session.", replay: "safe", parameters: Type.Object({}), execute: async () => result(service.list()) }),
|
|
10
12
|
defineTool({ name: "background_output", description: "Read bounded background output using byte offsets.", replay: "safe",
|
|
11
13
|
parameters: Type.Object({ id: Type.String(), offset: Type.Optional(Type.Integer({ minimum: 0 })), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 65536 })) }),
|
|
12
14
|
execute: async (args) => result(await service.output(args.id, args.offset, args.limit)) }),
|
|
13
|
-
defineTool({ name: "background_wait", description: "
|
|
15
|
+
defineTool({ name: "background_wait", description: "Blocking observation of a background command. Use only when the next step requires its result and no independent work remains; otherwise continue working and check status later. Explicit timeout ends observation only, not execution.", replay: "safe",
|
|
14
16
|
parameters: Type.Object({ id: Type.String(), timeout_ms: Type.Integer({ minimum: 1, maximum: 600000 }) }),
|
|
15
17
|
execute: async (args, api, ctx) => result(await service.wait(args.id, args.timeout_ms, ctx.abortSignal)) }),
|
|
18
|
+
defineTool({ name: "background_wait_all", description: "Observe selected commands concurrently after independent work is finished. Timeout does not stop commands.", replay: "safe",
|
|
19
|
+
parameters: Type.Object({ ids: Type.Array(Type.String(), { minItems: 1, maxItems: 100, uniqueItems: true }), timeout_ms: Type.Integer({ minimum: 1, maximum: 600000 }) }),
|
|
20
|
+
execute: async (args, api, ctx) => result(await waitForBackgroundTasks(service, args.ids, args.timeout_ms, ctx.abortSignal)) }),
|
|
16
21
|
defineTool({ name: "background_stop", description: "Stop a session-owned command and reap its descendants. Returns after supervisor cleanup.", replay: "unsafe",
|
|
17
22
|
parameters: Type.Object({ id: Type.String() }), execute: async (args) => result(await service.stop(args.id)) }),
|
|
18
23
|
] });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BackgroundTasks } from "./index.js";
|
|
2
|
+
/** Validate the entire selection before observing; one timeout applies concurrently. */
|
|
3
|
+
export declare function waitForBackgroundTasks(service: Pick<BackgroundTasks, "get" | "wait">, ids: string[], timeout: number, signal?: AbortSignal): Promise<{
|
|
4
|
+
id: string;
|
|
5
|
+
command: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
name: string;
|
|
8
|
+
status: "starting" | "running" | "stopping" | "completed" | "failed" | "stopped" | "unknown";
|
|
9
|
+
createdAt: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
exitCode?: number | null;
|
|
12
|
+
error?: string;
|
|
13
|
+
}[]>;
|
package/dist/wait-all.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Validate the entire selection before observing; one timeout applies concurrently. */
|
|
2
|
+
export async function waitForBackgroundTasks(service, ids, timeout, signal) {
|
|
3
|
+
if (!ids.length || ids.length > 100 || new Set(ids).size !== ids.length)
|
|
4
|
+
throw new Error("Select 1..100 distinct background task IDs");
|
|
5
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 600000)
|
|
6
|
+
throw new Error("timeout_ms must be 1..600000");
|
|
7
|
+
for (const id of ids)
|
|
8
|
+
service.get(id);
|
|
9
|
+
return Promise.all(ids.map(id => service.wait(id, timeout, signal)));
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naiba/pi-durable-background-tasks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Background shell tools for Pi Durable; no model delegation",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Background shell tools for Pi Coding Agent and Pi Durable; no model delegation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"types": "./src/index.ts",
|
|
15
15
|
"default": "./dist/index.js"
|
|
16
16
|
},
|
|
17
|
+
"./coding-agent": {
|
|
18
|
+
"bun": "./src/coding-agent.ts",
|
|
19
|
+
"types": "./src/coding-agent.ts",
|
|
20
|
+
"default": "./dist/coding-agent.js"
|
|
21
|
+
},
|
|
17
22
|
"./pi": {
|
|
18
23
|
"bun": "./src/pi.ts",
|
|
19
24
|
"types": "./src/pi.ts",
|
|
@@ -36,14 +41,32 @@
|
|
|
36
41
|
"check": "tsc --noEmit"
|
|
37
42
|
},
|
|
38
43
|
"peerDependencies": {
|
|
39
|
-
"@earendil-works/pi-durable": "^1.0
|
|
40
|
-
"@earendil-works/pi-ai": "^1.0
|
|
44
|
+
"@earendil-works/pi-durable": "^1.2.0",
|
|
45
|
+
"@earendil-works/pi-ai": "^1.2.0",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "^1.5.0",
|
|
47
|
+
"typebox": "^1.4.4"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@earendil-works/pi-durable": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@earendil-works/pi-ai": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"@earendil-works/pi-coding-agent": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"typebox": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
41
62
|
},
|
|
42
63
|
"devDependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
45
|
-
"
|
|
46
|
-
"@
|
|
47
|
-
"@
|
|
64
|
+
"@earendil-works/pi-ai": "1.5.0",
|
|
65
|
+
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
66
|
+
"@earendil-works/pi-durable": "1.5.0",
|
|
67
|
+
"@types/bun": "^1.4.3",
|
|
68
|
+
"@types/node": "^26.6.5",
|
|
69
|
+
"typebox": "1.4.4",
|
|
70
|
+
"typescript": "~7.0.2"
|
|
48
71
|
}
|
|
49
72
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {defineTool,type ExtensionFactory} from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {Type} from "typebox";
|
|
3
|
+
import {BackgroundTasks,type BackgroundTask,waitForBackgroundTasks} from "./index.js";
|
|
4
|
+
|
|
5
|
+
export interface BackgroundTasksExtensionOptions {
|
|
6
|
+
/** Called after start, including a deduplicated start. Host owns delivery policy. */
|
|
7
|
+
onStart?:(task:BackgroundTask)=>void|Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Coding Agent consumes content; Durable consumes output. Separate entry points
|
|
11
|
+
// prevent a native Pi installation from loading the Durable runtime.
|
|
12
|
+
const result=(value:unknown)=>({content:[{type:"text" as const,text:JSON.stringify(value)}],details:{}});
|
|
13
|
+
export function backgroundTasksExtension(service:BackgroundTasks,cwd:string,options:BackgroundTasksExtensionOptions={}):ExtensionFactory{
|
|
14
|
+
return pi=>{
|
|
15
|
+
pi.registerTool(defineTool({name:"background_start",label:"Start background command",description:"Start a supervised command and continue independent work. Commands do not survive host loss. Approval belongs to the host tool policy.",parameters:Type.Object({command:Type.String(),name:Type.Optional(Type.String())}),execute:async(id,args)=>{
|
|
16
|
+
const task=service.start({...args,cwd,requestId:id});await options.onStart?.(task);return result(task);
|
|
17
|
+
}}));
|
|
18
|
+
pi.registerTool(defineTool({name:"background_status",label:"Background status",description:"List session-owned background commands.",parameters:Type.Object({}),execute:async()=>result(service.list())}));
|
|
19
|
+
pi.registerTool(defineTool({name:"background_output",label:"Background output",description:"Read bounded background output using byte offsets.",parameters:Type.Object({id:Type.String(),offset:Type.Optional(Type.Integer({minimum:0})),limit:Type.Optional(Type.Integer({minimum:1,maximum:65536}))}),execute:async(_id,args)=>result(await service.output(args.id,args.offset,args.limit))}));
|
|
20
|
+
pi.registerTool(defineTool({name:"background_wait",label:"Wait for command",description:"Observe a command only when no independent work remains. Timeout ends observation, not execution.",parameters:Type.Object({id:Type.String(),timeout_ms:Type.Integer({minimum:1,maximum:600000})}),execute:async(_id,args,signal)=>result(await service.wait(args.id,args.timeout_ms,signal))}));
|
|
21
|
+
pi.registerTool(defineTool({name:"background_wait_all",label:"Wait for commands",description:"Observe selected commands concurrently after independent work is finished. Timeout does not stop commands.",parameters:Type.Object({ids:Type.Array(Type.String(),{minItems:1,maxItems:100,uniqueItems:true}),timeout_ms:Type.Integer({minimum:1,maximum:600000})}),execute:async(_id,args,signal)=>result(await waitForBackgroundTasks(service,args.ids,args.timeout_ms,signal))}));
|
|
22
|
+
pi.registerTool(defineTool({name:"background_stop",label:"Stop command",description:"Stop a session-owned command and reap its descendants.",parameters:Type.Object({id:Type.String()}),execute:async(_id,args)=>result(await service.stop(args.id))}));
|
|
23
|
+
};
|
|
24
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {open} from "node:fs/promises";
|
|
|
4
4
|
import {join, resolve} from "node:path";
|
|
5
5
|
import {fileURLToPath} from "node:url";
|
|
6
6
|
import {createHash} from "node:crypto";
|
|
7
|
+
export {waitForBackgroundTasks} from "./wait-all.js";
|
|
7
8
|
|
|
8
9
|
export type BackgroundTask = {
|
|
9
10
|
id:string; command:string; cwd:string; name:string;
|
package/src/pi.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import {defineExtension,defineTool} from "@earendil-works/pi-durable";
|
|
2
|
-
import {Type} from "
|
|
3
|
-
import {BackgroundTasks} from "./index.js";
|
|
4
|
-
|
|
2
|
+
import {Type} from "typebox";
|
|
3
|
+
import {BackgroundTasks,waitForBackgroundTasks} from "./index.js";
|
|
4
|
+
// Pi 1.2 consumes output; content silently hides task IDs and status from the model.
|
|
5
|
+
const result=(value:unknown)=>({output:[{type:"text" as const,text:JSON.stringify(value)}]});
|
|
5
6
|
export function backgroundTasksExtension(service:BackgroundTasks,cwd:string){
|
|
6
7
|
return defineExtension({name:"background-tasks",tools:[
|
|
7
|
-
defineTool({name:"background_start",description:"Start a supervised background shell command. Returns immediately; no model delegation. Commands do not survive host loss. Approval belongs to the host tool policy.",replay:"unsafe",
|
|
8
|
+
defineTool({name:"background_start",description:"Start a supervised background shell command. Returns immediately; continue independent work before checking results. Do not immediately wait when other work is available; no model delegation. Commands do not survive host loss. Approval belongs to the host tool policy.",replay:"unsafe",
|
|
8
9
|
parameters:Type.Object({command:Type.String(),name:Type.Optional(Type.String())}),
|
|
9
10
|
execute:async(args,api)=>result(service.start({...args,cwd,requestId:String(api.taskId)}))}),
|
|
10
11
|
defineTool({name:"background_status",description:"List background commands owned by this session.",replay:"safe",parameters:Type.Object({}),execute:async()=>result(service.list())}),
|
|
11
12
|
defineTool({name:"background_output",description:"Read bounded background output using byte offsets.",replay:"safe",
|
|
12
13
|
parameters:Type.Object({id:Type.String(),offset:Type.Optional(Type.Integer({minimum:0})),limit:Type.Optional(Type.Integer({minimum:1,maximum:65536}))}),
|
|
13
14
|
execute:async(args)=>result(await service.output(args.id,args.offset,args.limit))}),
|
|
14
|
-
defineTool({name:"background_wait",description:"
|
|
15
|
+
defineTool({name:"background_wait",description:"Blocking observation of a background command. Use only when the next step requires its result and no independent work remains; otherwise continue working and check status later. Explicit timeout ends observation only, not execution.",replay:"safe",
|
|
15
16
|
parameters:Type.Object({id:Type.String(),timeout_ms:Type.Integer({minimum:1,maximum:600000})}),
|
|
16
17
|
execute:async(args,api,ctx)=>result(await service.wait(args.id,args.timeout_ms,ctx.abortSignal))}),
|
|
18
|
+
defineTool({name:"background_wait_all",description:"Observe selected commands concurrently after independent work is finished. Timeout does not stop commands.",replay:"safe",
|
|
19
|
+
parameters:Type.Object({ids:Type.Array(Type.String(),{minItems:1,maxItems:100,uniqueItems:true}),timeout_ms:Type.Integer({minimum:1,maximum:600000})}),
|
|
20
|
+
execute:async(args,api,ctx)=>result(await waitForBackgroundTasks(service,args.ids,args.timeout_ms,ctx.abortSignal))}),
|
|
17
21
|
defineTool({name:"background_stop",description:"Stop a session-owned command and reap its descendants. Returns after supervisor cleanup.",replay:"unsafe",
|
|
18
22
|
parameters:Type.Object({id:Type.String()}),execute:async(args)=>result(await service.stop(args.id))}),
|
|
19
23
|
]});
|
package/src/wait-all.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type {BackgroundTasks} from "./index.js";
|
|
2
|
+
|
|
3
|
+
/** Validate the entire selection before observing; one timeout applies concurrently. */
|
|
4
|
+
export async function waitForBackgroundTasks(service:Pick<BackgroundTasks,"get"|"wait">,ids:string[],timeout:number,signal?:AbortSignal){
|
|
5
|
+
if(!ids.length||ids.length>100||new Set(ids).size!==ids.length)throw new Error("Select 1..100 distinct background task IDs");
|
|
6
|
+
if(!Number.isInteger(timeout)||timeout<1||timeout>600000)throw new Error("timeout_ms must be 1..600000");
|
|
7
|
+
for(const id of ids)service.get(id);
|
|
8
|
+
return Promise.all(ids.map(id=>service.wait(id,timeout,signal)));
|
|
9
|
+
}
|