@oai404iao/pi-subagent 0.3.0 → 0.4.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 +294 -89
- package/agents/worker.md +1 -1
- package/config.example.json +3 -4
- package/config.schema.json +26 -20
- package/index.ts +2 -0
- package/package.json +13 -12
- package/src/agent-state.ts +125 -0
- package/src/agent-sync.ts +171 -88
- package/src/agents.ts +3 -22
- package/src/catalog.ts +47 -0
- package/src/completion-mailbox.ts +656 -0
- package/src/config.ts +43 -35
- package/src/coordinator.ts +2172 -326
- package/src/descriptor.ts +96 -33
- package/src/index.ts +176 -58
- package/src/mailbox.ts +451 -0
- package/src/providers.ts +221 -28
- package/src/render.ts +23 -16
- package/src/scheduler.ts +173 -0
- package/src/schemas.ts +76 -38
- package/src/task-path.ts +146 -0
- package/src/types.ts +53 -15
package/src/task-path.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SubagentDescriptor,
|
|
3
|
+
SubagentTask,
|
|
4
|
+
} from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export const ROOT_TASK_PATH = "/root";
|
|
7
|
+
export const MAX_TASK_NAME_LENGTH = 64;
|
|
8
|
+
export const MAX_TASK_PATH_LENGTH = 4096;
|
|
9
|
+
|
|
10
|
+
const TASK_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
11
|
+
const AGENT_ID_PATTERN =
|
|
12
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
13
|
+
|
|
14
|
+
function pathSegments(path: string): string[] {
|
|
15
|
+
if (path.length > MAX_TASK_PATH_LENGTH) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`task path exceeds ${MAX_TASK_PATH_LENGTH} characters`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
if (!path.startsWith("/") || path.endsWith("/") || path.includes("//")) {
|
|
21
|
+
throw new Error(`task path must be a canonical absolute path under ${ROOT_TASK_PATH}`);
|
|
22
|
+
}
|
|
23
|
+
const segments = path.slice(1).split("/");
|
|
24
|
+
if (segments[0] !== "root") {
|
|
25
|
+
throw new Error(`task path must be rooted at ${ROOT_TASK_PATH}`);
|
|
26
|
+
}
|
|
27
|
+
return segments;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateTaskName(value: string): string {
|
|
31
|
+
if (!TASK_NAME_PATTERN.test(value)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"task_name must contain 1-64 lowercase ASCII letters, digits, hyphens, or underscores and start with a letter or digit",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (value === "root") {
|
|
37
|
+
throw new Error(`task_name "${value}" is reserved`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateTaskPath(path: string): string {
|
|
43
|
+
if (path === ROOT_TASK_PATH) return path;
|
|
44
|
+
const segments = pathSegments(path);
|
|
45
|
+
for (let index = 1; index < segments.length; index++) {
|
|
46
|
+
validateTaskName(segments[index]!);
|
|
47
|
+
}
|
|
48
|
+
return path;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function taskPath(parentPath: string, name: string): string {
|
|
52
|
+
validateTaskPath(parentPath);
|
|
53
|
+
validateTaskName(name);
|
|
54
|
+
const path = `${parentPath}/${name}`;
|
|
55
|
+
return validateTaskPath(path);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function descriptorTaskPath(
|
|
59
|
+
descriptor: SubagentDescriptor,
|
|
60
|
+
): string {
|
|
61
|
+
return descriptor.task.path;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function descriptorTask(
|
|
65
|
+
descriptor: SubagentDescriptor,
|
|
66
|
+
): SubagentTask {
|
|
67
|
+
return { ...descriptor.task };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function validateDescriptorTask(task: SubagentTask): SubagentTask {
|
|
71
|
+
const name = validateTaskName(task.name);
|
|
72
|
+
const path = validateTaskPath(task.path);
|
|
73
|
+
if (path === ROOT_TASK_PATH || path.split("/").at(-1) !== name) {
|
|
74
|
+
throw new Error("task.path must end with task.name");
|
|
75
|
+
}
|
|
76
|
+
return { name, path };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function slugTaskName(value: string): string {
|
|
80
|
+
const normalized = value
|
|
81
|
+
.normalize("NFKD")
|
|
82
|
+
.replace(/\p{Mark}/gu, "")
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
85
|
+
.replace(/[-_]{2,}/g, "-")
|
|
86
|
+
.replace(/^[-_]+|[-_]+$/g, "")
|
|
87
|
+
.slice(0, MAX_TASK_NAME_LENGTH)
|
|
88
|
+
.replace(/[-_]+$/g, "");
|
|
89
|
+
const candidate = !normalized || normalized === "root" ? "task" : normalized;
|
|
90
|
+
return validateTaskName(candidate);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function numberedTaskName(base: string, ordinal: number): string {
|
|
94
|
+
validateTaskName(base);
|
|
95
|
+
if (!Number.isSafeInteger(ordinal) || ordinal < 2) {
|
|
96
|
+
throw new Error("task name ordinal must be an integer of at least 2");
|
|
97
|
+
}
|
|
98
|
+
const suffix = `-${ordinal}`;
|
|
99
|
+
const prefix = base
|
|
100
|
+
.slice(0, MAX_TASK_NAME_LENGTH - suffix.length)
|
|
101
|
+
.replace(/[-_]+$/g, "");
|
|
102
|
+
return validateTaskName(`${prefix || "task"}${suffix}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveTaskPath(
|
|
106
|
+
basePath: string,
|
|
107
|
+
reference: string,
|
|
108
|
+
): string {
|
|
109
|
+
validateTaskPath(basePath);
|
|
110
|
+
if (!reference.trim() || reference !== reference.trim()) {
|
|
111
|
+
throw new Error("task path reference must be a non-empty trimmed string");
|
|
112
|
+
}
|
|
113
|
+
if (reference.length > MAX_TASK_PATH_LENGTH) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`task path reference exceeds ${MAX_TASK_PATH_LENGTH} characters`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (reference.includes("//") || (reference.length > 1 && reference.endsWith("/"))) {
|
|
119
|
+
throw new Error("task path reference contains an empty segment");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const absolute = reference.startsWith("/");
|
|
123
|
+
const segments = absolute ? ["root"] : pathSegments(basePath);
|
|
124
|
+
const input = absolute ? reference.slice(1).split("/") : reference.split("/");
|
|
125
|
+
if (absolute && input.shift() !== "root") {
|
|
126
|
+
throw new Error(`absolute task paths must be rooted at ${ROOT_TASK_PATH}`);
|
|
127
|
+
}
|
|
128
|
+
for (const segment of input) {
|
|
129
|
+
if (!segment || segment === ".") continue;
|
|
130
|
+
if (segment === "..") {
|
|
131
|
+
if (segments.length === 1) {
|
|
132
|
+
throw new Error(`task path reference cannot escape ${ROOT_TASK_PATH}`);
|
|
133
|
+
}
|
|
134
|
+
segments.pop();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
validateTaskName(segment);
|
|
138
|
+
segments.push(segment);
|
|
139
|
+
}
|
|
140
|
+
const resolved = `/${segments.join("/")}`;
|
|
141
|
+
return validateTaskPath(resolved);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function isAgentId(value: string): boolean {
|
|
145
|
+
return AGENT_ID_PATTERN.test(value);
|
|
146
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
|
|
4
|
+
/** Current on-disk subagent descriptor version. */
|
|
5
|
+
export const DESCRIPTOR_VERSION = 4;
|
|
6
|
+
|
|
4
7
|
export type AgentScope = "user" | "project" | "both";
|
|
5
|
-
|
|
6
|
-
export type
|
|
8
|
+
/** Sources that runtime discovery is allowed to activate. */
|
|
9
|
+
export type AgentSource = "user" | "project";
|
|
10
|
+
/**
|
|
11
|
+
* Single execution mode. `foreground` waits for every child's final answer;
|
|
12
|
+
* `background` starts durable mailbox children.
|
|
13
|
+
*/
|
|
14
|
+
export type RuntimeMode = "foreground" | "background";
|
|
7
15
|
export type SubagentMode = "one-shot" | "continuable";
|
|
8
16
|
export type SubagentProviderName = "spawn" | "fork";
|
|
9
17
|
export type SubagentStopReason = "completed" | "aborted" | "error" | "max-tokens";
|
|
18
|
+
export type ContextInheritance =
|
|
19
|
+
| { mode: "fresh" }
|
|
20
|
+
| { mode: "all_completed" }
|
|
21
|
+
| { mode: "last_n_completed"; completedTurns: number };
|
|
22
|
+
|
|
23
|
+
export interface SubagentTask {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
}
|
|
10
27
|
|
|
11
28
|
export interface SubagentSettings {
|
|
12
29
|
agentScope: AgentScope;
|
|
13
|
-
syncBundledAgents: boolean;
|
|
14
30
|
maxDepth: number;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
31
|
+
runtimeMode: RuntimeMode;
|
|
32
|
+
maxConcurrentBackgroundRuns: number;
|
|
33
|
+
maxIdleRuntimes: number;
|
|
18
34
|
inheritExtensions: boolean;
|
|
19
35
|
openAIIdentity: boolean;
|
|
20
36
|
maxOutputBytes: number;
|
|
@@ -48,18 +64,16 @@ export interface ResolvedModel {
|
|
|
48
64
|
|
|
49
65
|
export interface SubagentRuntimeSnapshot {
|
|
50
66
|
agentScope: AgentScope;
|
|
51
|
-
syncBundledAgents: boolean;
|
|
52
67
|
maxDepth: number;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
runtimeMode: RuntimeMode;
|
|
69
|
+
maxConcurrentBackgroundRuns: number;
|
|
70
|
+
maxIdleRuntimes: number;
|
|
56
71
|
inheritExtensions: boolean;
|
|
57
72
|
openAIIdentity: boolean;
|
|
58
73
|
maxOutputBytes: number;
|
|
59
74
|
}
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
version: 2;
|
|
76
|
+
interface SubagentDescriptorBase {
|
|
63
77
|
mode: SubagentMode;
|
|
64
78
|
provider: SubagentProviderName;
|
|
65
79
|
label: string;
|
|
@@ -80,15 +94,24 @@ export interface SubagentDescriptor {
|
|
|
80
94
|
runtime: SubagentRuntimeSnapshot;
|
|
81
95
|
}
|
|
82
96
|
|
|
97
|
+
export interface SubagentDescriptor extends SubagentDescriptorBase {
|
|
98
|
+
version: typeof DESCRIPTOR_VERSION;
|
|
99
|
+
task: SubagentTask;
|
|
100
|
+
context: ContextInheritance;
|
|
101
|
+
}
|
|
102
|
+
|
|
83
103
|
export interface SubagentUsage extends Usage {
|
|
84
104
|
turns: number;
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
export interface SubagentRunResult {
|
|
88
108
|
agentId: string;
|
|
109
|
+
turnId: string;
|
|
89
110
|
piSessionId?: string;
|
|
90
111
|
sessionFile?: string;
|
|
91
112
|
output: string;
|
|
113
|
+
outputTruncated?: boolean;
|
|
114
|
+
omittedBytes?: number;
|
|
92
115
|
stopReason: SubagentStopReason;
|
|
93
116
|
usage: SubagentUsage;
|
|
94
117
|
}
|
|
@@ -102,9 +125,12 @@ export interface TraceItem {
|
|
|
102
125
|
export interface DelegationDetails {
|
|
103
126
|
kind: "delegation";
|
|
104
127
|
agentId: string;
|
|
128
|
+
taskPath: string;
|
|
129
|
+
turnId?: string;
|
|
105
130
|
piSessionId?: string;
|
|
106
131
|
provider: SubagentProviderName;
|
|
107
132
|
mode: SubagentMode;
|
|
133
|
+
context: ContextInheritance;
|
|
108
134
|
agent: string;
|
|
109
135
|
label: string;
|
|
110
136
|
depth: number;
|
|
@@ -118,18 +144,30 @@ export interface DelegationDetails {
|
|
|
118
144
|
|
|
119
145
|
export interface ControlDetails {
|
|
120
146
|
kind: "control";
|
|
121
|
-
action: "send" | "interrupt" | "list" | "report";
|
|
147
|
+
action: "send" | "followup" | "wait" | "interrupt" | "list" | "report";
|
|
122
148
|
agentId?: string;
|
|
149
|
+
taskPath?: string;
|
|
150
|
+
messageId?: string;
|
|
151
|
+
turnId?: string;
|
|
152
|
+
pendingMessages?: number;
|
|
153
|
+
claimedMessages?: number;
|
|
154
|
+
completionIds?: string[];
|
|
155
|
+
timedOut?: boolean;
|
|
156
|
+
unreadUpdates?: number;
|
|
123
157
|
}
|
|
124
158
|
|
|
125
159
|
export interface CatalogChild {
|
|
126
160
|
kind: "child";
|
|
127
161
|
agentId: string;
|
|
128
162
|
parentAgentId: string;
|
|
163
|
+
taskPath: string;
|
|
164
|
+
parentTaskPath: string;
|
|
129
165
|
depth: number;
|
|
130
166
|
descriptor: SubagentDescriptor;
|
|
131
167
|
sessionFile?: string;
|
|
132
168
|
status: "running" | "idle" | "ready";
|
|
169
|
+
pendingMessages: number;
|
|
170
|
+
unreadUpdates: number;
|
|
133
171
|
}
|
|
134
172
|
|
|
135
173
|
export interface CatalogDiagnostic {
|
|
@@ -144,10 +182,10 @@ export interface CatalogDiagnostic {
|
|
|
144
182
|
export type CatalogEntry = CatalogChild | CatalogDiagnostic;
|
|
145
183
|
|
|
146
184
|
export interface ParentMessageDetails {
|
|
147
|
-
kind: "report"
|
|
185
|
+
kind: "report";
|
|
148
186
|
childAgentId: string;
|
|
187
|
+
taskPath?: string;
|
|
149
188
|
label: string;
|
|
150
|
-
stopReason?: SubagentStopReason;
|
|
151
189
|
truncated?: boolean;
|
|
152
190
|
}
|
|
153
191
|
|