@youngjurry/pi-agents 0.7.1

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/tools.ts ADDED
@@ -0,0 +1,303 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import {
3
+ defineTool,
4
+ type AgentToolResult,
5
+ type ToolDefinition,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import { Text } from "@earendil-works/pi-tui";
8
+ import { Type } from "typebox";
9
+ import { Value } from "typebox/value";
10
+ import type { AgentControl } from "./control.ts";
11
+ import type {
12
+ AgentToolCatalogEntry,
13
+ AgentView,
14
+ CollaborationDetails,
15
+ CollaborationToolName,
16
+ } from "./types.ts";
17
+
18
+ const ThinkingLevelSchema = StringEnum(
19
+ ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const,
20
+ { description: "Optional thinking override; otherwise Role then global defaults apply unless model is explicitly set" },
21
+ );
22
+ const AgentListViewSchema = StringEnum(["roles", "tools", "status", "results"] as const);
23
+ const SpawnAgentItemSchema = Type.Object({
24
+ message: Type.String({ description: "Task to assign to the child agent" }),
25
+ task_name: Type.String({ description: "Unique lowercase name using letters, digits, and underscores" }),
26
+ agent_type: Type.Optional(Type.String({ description: "Optional role name returned by list_agents(view=\"roles\"); omit for general-purpose work" })),
27
+ model: Type.Optional(Type.String({ description: "Optional provider/model override; when set without reasoning_effort, the model's highest supported thinking level is used" })),
28
+ reasoning_effort: Type.Optional(ThinkingLevelSchema),
29
+ fork_turns: Type.Optional(Type.String({ description: "none, all, or a positive integer string; defaults to all" })),
30
+ });
31
+
32
+ function details(tool: CollaborationToolName, sender: string, targets: AgentView[], message?: string, timedOut?: boolean): CollaborationDetails {
33
+ return { tool, sender, targets, message, timedOut };
34
+ }
35
+
36
+ function result<T extends CollaborationDetails>(text: string, value: T): AgentToolResult<T> {
37
+ return { content: [{ type: "text", text }], details: value };
38
+ }
39
+
40
+ function compactStatus(agent: AgentView): string {
41
+ const residency = agent.loaded ? "loaded" : "unloaded";
42
+ const nickname = agent.nickname ? ` (${agent.nickname})` : "";
43
+ const waiting = agent.status === "queued" ? `, waiting at queue position ${agent.queuePosition ?? "?"}` : "";
44
+ return `${agent.path}${nickname}: ${agent.status}${waiting}, ${agent.model}, thinking ${agent.thinkingLevel ?? "unknown"}, ${residency}`;
45
+ }
46
+
47
+ function catalogParameters(entry: AgentToolCatalogEntry): string {
48
+ const schema = entry.parameters as { properties?: Record<string, unknown>; required?: string[] };
49
+ const required = new Set(schema.required ?? []);
50
+ return Object.keys(schema.properties ?? {}).map((name) => required.has(name) ? `${name}*` : name).join(", ");
51
+ }
52
+
53
+ function renderCallHeader(name: string, summary: string, theme: any): Text {
54
+ return new Text(
55
+ `${theme.fg("toolTitle", theme.bold(`${name} `))}${theme.fg("accent", summary)}`,
56
+ 0,
57
+ 0,
58
+ );
59
+ }
60
+
61
+ function renderCollaborationResult(
62
+ toolResult: AgentToolResult<CollaborationDetails>,
63
+ options: { expanded: boolean; isPartial: boolean },
64
+ theme: any,
65
+ ): Text {
66
+ if (options.isPartial) return new Text(theme.fg("warning", "Waiting for agent activity…"), 0, 0);
67
+ const data = toolResult.details;
68
+ if (!data) {
69
+ const item = toolResult.content[0];
70
+ return new Text(item?.type === "text" ? item.text : "(no output)", 0, 0);
71
+ }
72
+ const icon = data.timedOut ? theme.fg("warning", "◷") : theme.fg("success", "✓");
73
+ const lines = [`${icon} ${theme.fg("toolTitle", data.tool)}`];
74
+ for (const agent of data.targets) lines.push(` ${theme.fg("accent", compactStatus(agent))}`);
75
+ if (data.roles?.length) {
76
+ lines.push(` ${theme.fg("muted", "Roles:")} ${data.roles.map((role) => role.name).join(", ")}`);
77
+ if (options.expanded) {
78
+ for (const role of data.roles) {
79
+ const configuration = [
80
+ role.source,
81
+ role.model ? `model: ${role.model}` : undefined,
82
+ role.thinkingLevel ? `thinking: ${role.thinkingLevel}` : undefined,
83
+ `tools: ${role.tools?.join(", ") || "default set"}`,
84
+ ].filter(Boolean).join(" · ");
85
+ lines.push(` ${theme.fg("accent", role.name)} — ${role.description}`);
86
+ lines.push(` ${theme.fg("dim", configuration)}`);
87
+ }
88
+ }
89
+ }
90
+ if (data.toolCatalog?.length) {
91
+ lines.push(` ${theme.fg("muted", "Tools:")} ${data.toolCatalog.map((tool) => tool.name).join(", ")}`);
92
+ if (options.expanded) {
93
+ for (const tool of data.toolCatalog) {
94
+ lines.push(` ${theme.fg("accent", tool.name)} — ${tool.description}`);
95
+ lines.push(` ${theme.fg("dim", catalogParameters(tool) || "no arguments")}`);
96
+ }
97
+ }
98
+ }
99
+ if (options.expanded && data.message) lines.push("", theme.fg("dim", data.message));
100
+ return new Text(lines.join("\n"), 0, 0);
101
+ }
102
+
103
+ export function createCollaborationTools(control: AgentControl): ToolDefinition[] {
104
+ const spawnAgents = defineTool({
105
+ name: "spawn_agents",
106
+ label: "Spawn Agents",
107
+ description: "Create one or more independent child agents in one call; available execution slots start immediately and overflow waits in a visible persistent queue.",
108
+ parameters: Type.Object({
109
+ agents: Type.Array(SpawnAgentItemSchema, { minItems: 1, description: "One or more independent child tasks" }),
110
+ }),
111
+ async execute(_id, params, _signal, onUpdate, ctx) {
112
+ const sender = control.callerPath(ctx);
113
+ onUpdate?.(result(`Preparing ${params.agents.length} child task${params.agents.length === 1 ? "" : "s"}…`, details("spawn_agents", sender, [])));
114
+ const agents = await control.spawnMany(ctx, params.agents.map((agent) => ({
115
+ message: agent.message,
116
+ taskName: agent.task_name,
117
+ agentType: agent.agent_type,
118
+ model: agent.model,
119
+ thinkingLevel: agent.reasoning_effort,
120
+ forkTurns: agent.fork_turns,
121
+ })));
122
+ const capacity = control.getCounts();
123
+ return result(
124
+ JSON.stringify({
125
+ agents: agents.map((agent) => ({
126
+ path: agent.path,
127
+ nickname: agent.nickname,
128
+ status: agent.status,
129
+ model: agent.model,
130
+ thinking_level: agent.thinkingLevel,
131
+ queue_position: agent.queuePosition,
132
+ })),
133
+ capacity,
134
+ }),
135
+ details("spawn_agents", sender, agents, `${agents.length} tasks accepted`),
136
+ );
137
+ },
138
+ renderCall(args, theme) {
139
+ return renderCallHeader("spawn_agents", `${args.agents.length} · ${args.agents.map((agent) => agent.task_name).join(", ")}`, theme);
140
+ },
141
+ renderResult: renderCollaborationResult,
142
+ });
143
+
144
+ const sendMessage = defineTool({
145
+ name: "send_message",
146
+ label: "Send Message",
147
+ description: "Message an agent without waking an idle target.",
148
+ parameters: Type.Object({
149
+ target: Type.String({ description: "Absolute agent path, child-relative name, or agent session ID" }),
150
+ message: Type.String({ description: "Message to deliver" }),
151
+ }),
152
+ async execute(_id, params, _signal, _onUpdate, ctx) {
153
+ const sender = control.callerPath(ctx);
154
+ const target = await control.message(ctx, { target: params.target, message: params.message, triggerTurn: false });
155
+ return result("", details("send_message", sender, [target], params.message));
156
+ },
157
+ renderCall(args, theme) {
158
+ return renderCallHeader("send_message", `${args.target} ← ${args.message}`, theme);
159
+ },
160
+ renderResult: renderCollaborationResult,
161
+ });
162
+
163
+ const followupTask = defineTool({
164
+ name: "followup_task",
165
+ label: "Follow-up Task",
166
+ description: "Assign follow-up work and start the target agent.",
167
+ parameters: Type.Object({
168
+ target: Type.String({ description: "Absolute agent path, child-relative name, or agent session ID" }),
169
+ message: Type.String({ description: "Follow-up task" }),
170
+ }),
171
+ async execute(_id, params, _signal, _onUpdate, ctx) {
172
+ const sender = control.callerPath(ctx);
173
+ const target = await control.message(ctx, { target: params.target, message: params.message, triggerTurn: true });
174
+ return result("", details("followup_task", sender, [target], params.message));
175
+ },
176
+ renderCall(args, theme) {
177
+ return renderCallHeader("followup_task", `${args.target} ← ${args.message}`, theme);
178
+ },
179
+ renderResult: renderCollaborationResult,
180
+ });
181
+
182
+ const waitAgent = defineTool({
183
+ name: "wait_agent",
184
+ label: "Wait Agent",
185
+ description: "Wait for agent activity and receive queued completion notices in the result; use this instead of polling.",
186
+ parameters: Type.Object({
187
+ timeout_ms: Type.Optional(Type.Integer({ minimum: 1, maximum: 3_600_000 })),
188
+ }),
189
+ async execute(_id, params, signal, onUpdate, ctx) {
190
+ const sender = control.callerPath(ctx);
191
+ onUpdate?.(result("Waiting for mailbox activity…", details("wait_agent", sender, [])));
192
+ const outcome = await control.waitForMailbox(ctx, params.timeout_ms, signal);
193
+ if (outcome.aborted) throw new Error("wait_agent was aborted");
194
+ const notices = control.drainPendingMail(sender);
195
+ const agents = control.list(ctx);
196
+ const text = notices.length > 0
197
+ ? `Mailbox activity received:\n\n${notices.join("\n\n")}`
198
+ : outcome.timedOut
199
+ ? "Wait timed out."
200
+ : "Mailbox activity received.";
201
+ return result(text, details("wait_agent", sender, agents, text, outcome.timedOut));
202
+ },
203
+ renderCall(args, theme) {
204
+ return renderCallHeader("wait_agent", `${args.timeout_ms ?? 30_000}ms`, theme);
205
+ },
206
+ renderResult: renderCollaborationResult,
207
+ });
208
+
209
+ const interruptAgent = defineTool({
210
+ name: "interrupt_agent",
211
+ label: "Interrupt Agent",
212
+ description: "Stop a running agent while keeping it available for follow-up.",
213
+ parameters: Type.Object({
214
+ target: Type.String({ description: "Absolute agent path, child-relative name, or agent session ID" }),
215
+ }),
216
+ async execute(_id, params, _signal, _onUpdate, ctx) {
217
+ const sender = control.callerPath(ctx);
218
+ const target = await control.interrupt(ctx, params.target);
219
+ return result(JSON.stringify({ previous_or_current_status: target.status }), details("interrupt_agent", sender, [target]));
220
+ },
221
+ renderCall(args, theme) {
222
+ return renderCallHeader("interrupt_agent", args.target, theme);
223
+ },
224
+ renderResult: renderCollaborationResult,
225
+ });
226
+
227
+ const directTools = [spawnAgents, sendMessage, followupTask, waitAgent, interruptAgent];
228
+ const toolCatalog: AgentToolCatalogEntry[] = directTools.map((tool) => ({
229
+ name: tool.name,
230
+ description: tool.description,
231
+ parameters: tool.parameters,
232
+ }));
233
+ const directToolsByName = new Map(directTools.map((tool) => [tool.name, tool]));
234
+
235
+ const agentAction = defineTool({
236
+ name: "agent_action",
237
+ label: "Agent Action",
238
+ description: "Execute a sub-agent action returned by list_agents(view=\"tools\").",
239
+ parameters: Type.Object({
240
+ action: Type.String({ description: "Action name from the tool catalog" }),
241
+ arguments: Type.Record(Type.String(), Type.Unknown(), { description: "Arguments matching the selected action schema" }),
242
+ }),
243
+ async execute(id, params, signal, onUpdate, ctx) {
244
+ const actionTool = directToolsByName.get(params.action);
245
+ if (!actionTool) throw new Error(`Unknown agent action '${params.action}'. Query list_agents(view="tools") for available actions.`);
246
+ if (!Value.Check(actionTool.parameters, params.arguments)) {
247
+ const first = Value.Errors(actionTool.parameters, params.arguments)[0];
248
+ const problem = first ? `${first.instancePath || "/"}: ${first.message}` : "arguments do not match the action schema";
249
+ throw new Error(`Invalid arguments for ${params.action}: ${problem}`);
250
+ }
251
+ return actionTool.execute(id, params.arguments, signal, onUpdate, ctx);
252
+ },
253
+ renderCall(args, theme) {
254
+ return renderCallHeader("agent_action", args.action, theme);
255
+ },
256
+ renderResult: renderCollaborationResult,
257
+ });
258
+
259
+ const listAgents = defineTool({
260
+ name: "list_agents",
261
+ label: "List Agents",
262
+ description: "Inspect sub-agent roles, action tools, status, or stored results.",
263
+ parameters: Type.Object({
264
+ view: AgentListViewSchema,
265
+ path_prefix: Type.Optional(Type.String({ description: "Optional absolute path or caller-relative subtree prefix for status and results" })),
266
+ }),
267
+ async execute(_id, params, _signal, _onUpdate, ctx) {
268
+ const sender = control.callerPath(ctx);
269
+ if (params.view === "roles") {
270
+ const roles = control.listRoles(ctx);
271
+ return result(JSON.stringify({ roles }), {
272
+ ...details("list_agents", sender, []),
273
+ roles,
274
+ });
275
+ }
276
+ if (params.view === "tools") {
277
+ return result(
278
+ JSON.stringify({
279
+ tools: toolCatalog,
280
+ invoke_with: {
281
+ tool: "agent_action",
282
+ arguments: { action: "<tool name>", arguments: "<matching action arguments>" },
283
+ },
284
+ }),
285
+ {
286
+ ...details("list_agents", sender, []),
287
+ toolCatalog,
288
+ },
289
+ );
290
+ }
291
+ const agents = control.list(ctx, params.path_prefix, params.view === "results");
292
+ const payload = params.view === "status" ? { agents, capacity: control.getCounts() } : { agents };
293
+ return result(JSON.stringify(payload), details("list_agents", sender, agents));
294
+ },
295
+ renderCall(args, theme) {
296
+ const scope = args.path_prefix ? `${args.view} · ${args.path_prefix}` : args.view;
297
+ return renderCallHeader("list_agents", scope, theme);
298
+ },
299
+ renderResult: renderCollaborationResult,
300
+ });
301
+
302
+ return [...directTools, listAgents, agentAction];
303
+ }
package/types.ts ADDED
@@ -0,0 +1,161 @@
1
+ import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type { Model } from "@earendil-works/pi-ai";
3
+ import type { AgentSession, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
+
5
+ export const EXTENSION_ID = "codex-agents";
6
+ export const STATE_ENTRY_TYPE = "codex-agents-state";
7
+ export const CHILD_META_ENTRY_TYPE = "codex-agents-child-meta";
8
+ export const FORK_CONTEXT_ENTRY_TYPE = "codex-agents-fork-context";
9
+ export const ROOT_PATH = "/root";
10
+ export const DIRECT_AGENT_TOOL_NAMES = [
11
+ "spawn_agents",
12
+ "send_message",
13
+ "followup_task",
14
+ "wait_agent",
15
+ "interrupt_agent",
16
+ ] as const;
17
+ export const AGENT_GATEWAY_TOOL_NAMES = ["list_agents", "agent_action"] as const;
18
+ export const COLLABORATION_TOOL_NAMES = [
19
+ ...DIRECT_AGENT_TOOL_NAMES,
20
+ ...AGENT_GATEWAY_TOOL_NAMES,
21
+ ] as const;
22
+
23
+ export type CollaborationToolName = (typeof COLLABORATION_TOOL_NAMES)[number];
24
+ export type AgentLifecycleStatus =
25
+ | "queued"
26
+ | "pending_init"
27
+ | "running"
28
+ | "interrupted"
29
+ | "completed"
30
+ | "errored"
31
+ | "shutdown";
32
+
33
+ export interface PersistedAgent {
34
+ id: string;
35
+ path: string;
36
+ parentPath: string;
37
+ parentId: string;
38
+ taskName: string;
39
+ nickname?: string;
40
+ role?: string;
41
+ modelProvider: string;
42
+ modelId: string;
43
+ thinkingLevel?: ThinkingLevel;
44
+ status: AgentLifecycleStatus;
45
+ statusMessage?: string;
46
+ finalAnswer?: string;
47
+ resultFile?: string;
48
+ sessionFile?: string;
49
+ createdAt: number;
50
+ updatedAt: number;
51
+ lastUsedAt: number;
52
+ lastAssignedAt?: number;
53
+ queuedMessage?: string;
54
+ queuedMail?: string[];
55
+ }
56
+
57
+ export interface PersistedTreeState {
58
+ version: 1;
59
+ rootSessionId: string;
60
+ agents: PersistedAgent[];
61
+ }
62
+
63
+ export interface AgentRecord extends PersistedAgent {
64
+ session?: AgentSession;
65
+ unsubscribe?: () => void;
66
+ loaded: boolean;
67
+ holdsExecutionSlot: boolean;
68
+ launchGeneration: number;
69
+ lastCompletionTimestamp?: number;
70
+ }
71
+
72
+ export interface RootBinding {
73
+ ctx: ExtensionContext;
74
+ sessionId: string;
75
+ cwd: string;
76
+ model?: Model<any>;
77
+ thinkingLevel?: ThinkingLevel;
78
+ systemPrompt: string;
79
+ }
80
+
81
+ export interface AgentView {
82
+ id: string;
83
+ path: string;
84
+ parentPath?: string;
85
+ nickname?: string;
86
+ role?: string;
87
+ model: string;
88
+ thinkingLevel?: ThinkingLevel;
89
+ status: AgentLifecycleStatus;
90
+ statusMessage?: string;
91
+ loaded: boolean;
92
+ resultFile?: string;
93
+ finalAnswer?: string;
94
+ queuePosition?: number;
95
+ lastAssignedAt?: number;
96
+ }
97
+
98
+ export interface AgentCounts {
99
+ running: number;
100
+ queued: number;
101
+ loaded: number;
102
+ total: number;
103
+ slots: number;
104
+ residentSlots: number;
105
+ }
106
+
107
+ export interface AgentTranscriptView {
108
+ agent: AgentView;
109
+ sessionFile: string;
110
+ cwd: string;
111
+ messages: AgentMessage[];
112
+ toolDefinitions: ToolDefinition[];
113
+ createdAt: number;
114
+ updatedAt: number;
115
+ }
116
+
117
+ export interface AgentToolCatalogEntry {
118
+ name: string;
119
+ description: string;
120
+ parameters: unknown;
121
+ }
122
+
123
+ export interface CollaborationDetails {
124
+ tool: CollaborationToolName;
125
+ sender: string;
126
+ targets: AgentView[];
127
+ message?: string;
128
+ timedOut?: boolean;
129
+ roles?: AgentRoleView[];
130
+ toolCatalog?: AgentToolCatalogEntry[];
131
+ }
132
+
133
+ export interface ForkContextPayload {
134
+ messages: AgentMessage[];
135
+ }
136
+
137
+ export interface ChildMetaPayload {
138
+ path: string;
139
+ parentPath: string;
140
+ role?: string;
141
+ }
142
+
143
+ export interface AgentRoleView {
144
+ name: string;
145
+ description: string;
146
+ model?: string;
147
+ thinkingLevel?: ThinkingLevel;
148
+ tools?: string[];
149
+ source: "builtin" | "user" | "project";
150
+ }
151
+
152
+ export interface AgentRole {
153
+ name: string;
154
+ description: string;
155
+ systemPrompt: string;
156
+ tools?: string[];
157
+ model?: string;
158
+ thinkingLevel?: ThinkingLevel;
159
+ nicknameCandidates?: string[];
160
+ source: "builtin" | "user" | "project";
161
+ }