@naiba/pi-durable-background-tasks 0.2.0 → 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 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
 
@@ -10,10 +10,27 @@ bun add @naiba/pi-durable-background-tasks
10
10
 
11
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
12
 
13
- This is an extension for **Pi Durable**, not an auto-discovered Pi coding-agent CLI plugin. The host must install the extension in its registry and own its lifecycle. It does not manage Claude Code or Codex background tasks.
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
14
 
15
15
  ## Usage
16
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
+
17
34
  ```ts
18
35
  import {BackgroundTasks} from "@naiba/pi-durable-background-tasks";
19
36
  import {backgroundTasksExtension} from "@naiba/pi-durable-background-tasks/pi";
@@ -34,6 +51,7 @@ unsubscribe();
34
51
  | `background_status` | `{}` | List commands and their recorded state |
35
52
  | `background_output` | `id`, optional `offset`, `limit` | Read up to 64 KiB using byte offsets |
36
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 |
37
55
  | `background_stop` | `id` | Stop a command and await supervisor cleanup |
38
56
 
39
57
  Wait requires `timeout_ms` between 1 and 600000. Observation timeout/cancellation does not stop the command. Commands have no implicit execution deadline.
@@ -54,7 +72,7 @@ Output reads are bounded to 64 KiB, with byte offsets. Logs are retained without
54
72
 
55
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.
56
74
 
57
- The package itself has no Web UI and does not automatically resume a model when a command completes. CloudCode's Claude Code/Codex ACP background activity adapters are not implemented by this package; their execution must remain native.
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.
58
76
 
59
77
  ## Development
60
78
 
@@ -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
@@ -1,3 +1,4 @@
1
+ export { waitForBackgroundTasks } from "./wait-all.js";
1
2
  export type BackgroundTask = {
2
3
  id: string;
3
4
  command: string;
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 "@earendil-works/pi-ai";
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,5 +1,6 @@
1
1
  import { defineExtension, defineTool } from "@earendil-works/pi-durable";
2
- import { Type } from "@earendil-works/pi-ai";
2
+ import { Type } from "typebox";
3
+ import { waitForBackgroundTasks } from "./index.js";
3
4
  // Pi 1.2 consumes output; content silently hides task IDs and status from the model.
4
5
  const result = (value) => ({ output: [{ type: "text", text: JSON.stringify(value) }] });
5
6
  export function backgroundTasksExtension(service, cwd) {
@@ -14,6 +15,9 @@ export function backgroundTasksExtension(service, cwd) {
14
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
  ] });
@@ -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
+ }[]>;
@@ -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.2.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",
@@ -37,13 +42,31 @@
37
42
  },
38
43
  "peerDependencies": {
39
44
  "@earendil-works/pi-durable": "^1.2.0",
40
- "@earendil-works/pi-ai": "^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
- "@types/node": "^26.6.5",
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",
44
67
  "@types/bun": "^1.4.3",
45
- "typescript": "~7.0.2",
46
- "@earendil-works/pi-durable": "1.2.0",
47
- "@earendil-works/pi-ai": "1.2.0"
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,6 +1,6 @@
1
1
  import {defineExtension,defineTool} from "@earendil-works/pi-durable";
2
- import {Type} from "@earendil-works/pi-ai";
3
- import {BackgroundTasks} from "./index.js";
2
+ import {Type} from "typebox";
3
+ import {BackgroundTasks,waitForBackgroundTasks} from "./index.js";
4
4
  // Pi 1.2 consumes output; content silently hides task IDs and status from the model.
5
5
  const result=(value:unknown)=>({output:[{type:"text" as const,text:JSON.stringify(value)}]});
6
6
  export function backgroundTasksExtension(service:BackgroundTasks,cwd:string){
@@ -15,6 +15,9 @@ export function backgroundTasksExtension(service:BackgroundTasks,cwd:string){
15
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",
16
16
  parameters:Type.Object({id:Type.String(),timeout_ms:Type.Integer({minimum:1,maximum:600000})}),
17
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))}),
18
21
  defineTool({name:"background_stop",description:"Stop a session-owned command and reap its descendants. Returns after supervisor cleanup.",replay:"unsafe",
19
22
  parameters:Type.Object({id:Type.String()}),execute:async(args)=>result(await service.stop(args.id))}),
20
23
  ]});
@@ -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
+ }