@oas-framework/pi 0.4.0 → 0.6.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/extension/core-loader.mjs +1 -3
- package/extension/index.ts +19 -346
- package/package.json +1 -1
|
@@ -55,9 +55,7 @@ if (!OAS_PKG_ROOT) {
|
|
|
55
55
|
const core = await import(pathToFileURL(join(OAS_PKG_ROOT, "lib", "core.mjs")).href);
|
|
56
56
|
|
|
57
57
|
export const {
|
|
58
|
-
appendLogEntry,
|
|
59
|
-
listInstances, retireInstance, spawnInstance, upsertTmpAgent, workspaceOf, writeSoul, PACKAGED_SKILLS_DIR,
|
|
60
|
-
workspaceConfigSkillDirs, isOasWorkspace, resolveOasConfig, runLifecycleHooks,
|
|
58
|
+
appendLogEntry, PACKAGED_SKILLS_DIR, workspaceConfigSkillDirs, isOasWorkspace,
|
|
61
59
|
} = core;
|
|
62
60
|
|
|
63
61
|
/** Kernel package version (for skew diagnostics against the adapter). */
|
package/extension/index.ts
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* oas
|
|
2
|
+
* oas pi adapter — pi-specific glue over the globally installed OAS kernel.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - imports single-file defs from .claude/agents/*.md and .agents/agents/*.md as tmp agents
|
|
8
|
-
* - work mode per soul: `worktree` (own git worktree + branch) or `checkout` (symlink to repo)
|
|
9
|
-
* - sessions run in tmux (session "pi-agents"), model selectable from pi's enabled models
|
|
4
|
+
* Everything agents DO goes through the universal `oas` CLI (status, spawn,
|
|
5
|
+
* retire, create, doctor, integration commands) from the bash tool. This
|
|
6
|
+
* extension only handles what a runtime must integrate natively:
|
|
10
7
|
*
|
|
11
|
-
*
|
|
8
|
+
* - skill discovery (resources_discover): the global getting-started skill,
|
|
9
|
+
* the OAS skill bundle inside OAS contexts, workspace skills above the
|
|
10
|
+
* repo boundary, and config-cascade skill dirs
|
|
11
|
+
* - instance memory automation: compaction journaling + STATE.md steers,
|
|
12
|
+
* and the resume nudge on session start
|
|
12
13
|
*/
|
|
13
|
-
import type { ExtensionAPI
|
|
14
|
-
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { Type } from "typebox";
|
|
16
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
15
|
import {
|
|
18
|
-
appendLogEntry,
|
|
19
|
-
listInstances, retireInstance, spawnInstance, upsertTmpAgent, workspaceOf, writeSoul, PACKAGED_SKILLS_DIR,
|
|
20
|
-
workspaceConfigSkillDirs, isOasWorkspace, resolveOasConfig, runLifecycleHooks,
|
|
16
|
+
appendLogEntry, isOasWorkspace, workspaceConfigSkillDirs, PACKAGED_SKILLS_DIR,
|
|
21
17
|
} from "./core-loader.mjs";
|
|
22
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
23
19
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -46,59 +42,20 @@ function workspaceSkillDirs(cwd: string): string[] {
|
|
|
46
42
|
return dirs;
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
function globToRegex(glob: string): RegExp {
|
|
50
|
-
const esc = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
51
|
-
return new RegExp(`^${esc}$`, "i");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Models enabled in pi: settings `enabledModels` patterns (scoped models) expanded
|
|
55
|
-
* against the registry; falls back to all auth-configured models if unscoped. */
|
|
56
|
-
function availableModels(ctx: ExtensionContext): string[] {
|
|
57
|
-
const all = ctx.modelRegistry.getAvailable().map((m) => ({ full: `${m.provider}/${m.id}`, id: m.id }));
|
|
58
|
-
let patterns: string[] | undefined;
|
|
59
|
-
try {
|
|
60
|
-
patterns = SettingsManager.create(ctx.cwd).getEnabledModels();
|
|
61
|
-
} catch { /* fall through to unscoped */ }
|
|
62
|
-
if (!patterns || patterns.length === 0) return all.map((m) => m.full);
|
|
63
|
-
|
|
64
|
-
const out: string[] = [];
|
|
65
|
-
for (const raw of patterns) {
|
|
66
|
-
// strip optional :thinking suffix (e.g. "provider/id:high")
|
|
67
|
-
const pattern = raw.replace(/:(off|minimal|low|medium|high|xhigh)$/i, "");
|
|
68
|
-
const isGlob = /[*?[]/.test(pattern);
|
|
69
|
-
const matches = isGlob
|
|
70
|
-
? all.filter((m) => globToRegex(pattern).test(m.full) || globToRegex(pattern).test(m.id))
|
|
71
|
-
: all.filter((m) => m.full.toLowerCase() === pattern.toLowerCase() || m.id.toLowerCase() === pattern.toLowerCase());
|
|
72
|
-
for (const m of matches) if (!out.includes(m.full)) out.push(m.full);
|
|
73
|
-
}
|
|
74
|
-
return out.length > 0 ? out : all.map((m) => m.full);
|
|
75
|
-
}
|
|
76
|
-
function validateModel(ctx: ExtensionContext, model?: string): string | undefined {
|
|
77
|
-
if (!model) return undefined;
|
|
78
|
-
const models = availableModels(ctx);
|
|
79
|
-
if (models.includes(model)) return model;
|
|
80
|
-
const matches = models.filter((m) => m.toLowerCase().includes(model.toLowerCase()));
|
|
81
|
-
if (matches.length === 1) return matches[0];
|
|
82
|
-
throw new Error(
|
|
83
|
-
matches.length === 0
|
|
84
|
-
? `model "${model}" is not enabled in pi — available: ${models.join(", ")}`
|
|
85
|
-
: `model "${model}" is ambiguous (${matches.join(", ")}) — be more specific`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
function text(s: string) {
|
|
89
|
-
return { content: [{ type: "text" as const, text: s }] };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
45
|
export default function (pi: ExtensionAPI) {
|
|
93
|
-
// (PI_AGENT_HOME is set by
|
|
46
|
+
// (PI_AGENT_HOME is set by the kernel's launch command)
|
|
94
47
|
const agentHome = process.env.PI_AGENT_HOME;
|
|
95
48
|
const isInstance = !!agentHome && existsSync(join(agentHome ?? "", "instance.json"));
|
|
96
49
|
|
|
97
|
-
// ----------
|
|
98
|
-
//
|
|
99
|
-
//
|
|
50
|
+
// ---------- OAS skills ----------
|
|
51
|
+
// getting-started is GLOBAL (it bootstraps — must be discoverable before any OAS
|
|
52
|
+
// workspace exists). The rest of the bundle is gated to OAS contexts (instance
|
|
53
|
+
// home, or a workspace/repo with an oas-config / agents root). Workspace and
|
|
54
|
+
// config skills stay trust-gated.
|
|
100
55
|
pi.on("resources_discover", async (event, ctx) => {
|
|
101
56
|
const paths: string[] = [];
|
|
57
|
+
const gettingStarted = join(PACKAGED_SKILLS_DIR, "oas-getting-started");
|
|
58
|
+
if (existsSync(gettingStarted)) paths.push(gettingStarted);
|
|
102
59
|
const inOas = isInstance || isOasWorkspace(event.cwd);
|
|
103
60
|
if (inOas && existsSync(PACKAGED_SKILLS_DIR)) paths.push(PACKAGED_SKILLS_DIR);
|
|
104
61
|
if (ctx.isProjectTrusted()) {
|
|
@@ -110,7 +67,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
110
67
|
});
|
|
111
68
|
|
|
112
69
|
// ---------- inside a spawned instance: memory automation ----------
|
|
113
|
-
|
|
114
70
|
if (isInstance) {
|
|
115
71
|
// Memory automation is gated on the knowledge integration having scaffolded
|
|
116
72
|
// instance memory (STATE.md/log.md are OKF conventions — kernel is agnostic;
|
|
@@ -135,26 +91,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
135
91
|
}, { deliverAs: "steer" });
|
|
136
92
|
});
|
|
137
93
|
|
|
138
|
-
// Post-commit lifecycle: after a successful `git commit` by this instance, fire
|
|
139
|
-
// the integrations' post-commit hooks (e.g. oas-okf spawns a knowledge-harvest
|
|
140
|
-
// agent attached to this instance's work tree). Kernel stays layer-agnostic.
|
|
141
|
-
pi.on("tool_result", async (event) => {
|
|
142
|
-
if (event.toolName !== "bash" || event.isError) return;
|
|
143
|
-
const cmd = String((event.input as { command?: string })?.command ?? "");
|
|
144
|
-
if (!/\bgit\b[^\n;&|]*\bcommit\b/.test(cmd)) return;
|
|
145
|
-
try {
|
|
146
|
-
const meta = JSON.parse(readFileSync(join(agentHome!, "instance.json"), "utf8"));
|
|
147
|
-
const root = findRoot(agentHome!);
|
|
148
|
-
if (!root || !meta.repo) return;
|
|
149
|
-
runLifecycleHooks("post-commit", {
|
|
150
|
-
home: agentHome!, instance: meta.instance, agentName: meta.agent,
|
|
151
|
-
soulDir: join(agentHome!, "soul"), contextDir: meta.repo,
|
|
152
|
-
workspaceDir: workspaceOf(root), rootDir: root,
|
|
153
|
-
resolved: resolveOasConfig(meta.repo), priorMeta: meta.providerMeta || {},
|
|
154
|
-
});
|
|
155
|
-
} catch { /* hooks never break the session */ }
|
|
156
|
-
});
|
|
157
|
-
|
|
158
94
|
// Resume nudge: on any later session in this home (restart, model switch, /new),
|
|
159
95
|
// point the model at its own memory before it does anything else.
|
|
160
96
|
pi.on("session_start", async (event) => {
|
|
@@ -171,267 +107,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
171
107
|
}, { deliverAs: "steer", triggerTurn: false });
|
|
172
108
|
});
|
|
173
109
|
}
|
|
174
|
-
|
|
175
|
-
// ---------- tools ----------
|
|
176
|
-
pi.registerTool({
|
|
177
|
-
name: "spawn_agent",
|
|
178
|
-
label: "Spawn Agent",
|
|
179
|
-
description: [
|
|
180
|
-
"Spawn an agent instance in tmux from the closest agents/ directory (walking up from cwd).",
|
|
181
|
-
"Modes: spawn an existing agent by name; create a tmp (adhoc) agent by passing `instructions`",
|
|
182
|
-
"(a full AGENTS.md defining behavior); or import a .claude/agents/.agents/agents def by passing `defFile`.",
|
|
183
|
-
"Each instance gets a home dir with the soul linked in, a TASK.md briefing, and a work tree:",
|
|
184
|
-
"`worktree` = own git worktree + branch, `checkout` = symlink to the repo's checked-out branch.",
|
|
185
|
-
"Returns instance metadata including the tmux window. Model must be one enabled in pi (see agents_status).",
|
|
186
|
-
].join(" "),
|
|
187
|
-
promptSnippet: "Spawn an agent (persistent, adhoc, or from an agent def file) in tmux",
|
|
188
|
-
promptGuidelines: [
|
|
189
|
-
"Use spawn_agent when the user asks to launch/spin up an agent, delegate a task to a new agent session, or create a subagent.",
|
|
190
|
-
"Before spawn_agent with a model the user named loosely, call agents_status to see enabled models.",
|
|
191
|
-
"For adhoc subagents, write complete standalone instructions — the tmp agent knows nothing about this conversation.",
|
|
192
|
-
],
|
|
193
|
-
parameters: Type.Object({
|
|
194
|
-
agent: Type.String({ description: "Agent name (existing agent, or name for a new tmp agent)" }),
|
|
195
|
-
instructions: Type.Optional(Type.String({
|
|
196
|
-
description: "Full AGENTS.md content for a local/adhoc agent. Creates/updates <root>/local-agents/<agent>/soul.",
|
|
197
|
-
})),
|
|
198
|
-
defFile: Type.Optional(Type.String({
|
|
199
|
-
description: "Path to a single-file agent def (.claude/agents/<x>.md style, frontmatter + body) to import as a tmp agent",
|
|
200
|
-
})),
|
|
201
|
-
task: Type.Optional(Type.String({ description: "Task briefing written to the instance's TASK.md and given as the first prompt" })),
|
|
202
|
-
purpose: Type.Optional(Type.String({ description: "Short kebab slug describing what this instance is for; names the instance <agent>-<purpose>" })),
|
|
203
|
-
model: Type.Optional(Type.String({ description: "Model as provider/id (must be enabled in pi); defaults to the soul's model or pi's default" })),
|
|
204
|
-
repo: Type.Optional(Type.String({ description: "Target repo (absolute, or relative to the agents root's parent). Defaults to the soul's repo, else the current git repo" })),
|
|
205
|
-
work: Type.Optional(StringEnum(["worktree", "checkout"] as const, {
|
|
206
|
-
description: "worktree = own git worktree + branch; checkout = work directly on the repo's checked-out branch. Defaults to the soul's setting",
|
|
207
|
-
})),
|
|
208
|
-
runtime: Type.Optional(StringEnum(["pi", "claude"] as const, { description: "Session runtime (default: soul's setting, else pi)" })),
|
|
209
|
-
branch: Type.Optional(Type.String({ description: "Branch name for worktree mode (default: agents/<instance>)" })),
|
|
210
|
-
launch: Type.Optional(Type.Boolean({ description: "Launch in tmux (default true). false = scaffold only and return the launch command" })),
|
|
211
|
-
}),
|
|
212
|
-
async execute(_id, p, _signal, _onUpdate, ctx) {
|
|
213
|
-
const root = ensureRoot(ctx.cwd);
|
|
214
|
-
const model = validateModel(ctx, p.model);
|
|
215
|
-
|
|
216
|
-
let agent = findAgent(root, p.agent);
|
|
217
|
-
if (p.instructions !== undefined || p.defFile !== undefined || (!agent && !p.instructions)) {
|
|
218
|
-
if (!agent && p.instructions === undefined && p.defFile === undefined) {
|
|
219
|
-
// Maybe it matches a .claude/agents / .agents/agents def by name.
|
|
220
|
-
const def = listAgentDefs(ctx.cwd).find((d) => d.name === p.agent);
|
|
221
|
-
if (!def) {
|
|
222
|
-
const known = [...listAgents(root).map((a) => a.name), ...listAgentDefs(ctx.cwd).map((d) => d.name)];
|
|
223
|
-
throw new Error(`unknown agent "${p.agent}" — known: ${known.join(", ") || "(none)"}. Pass instructions to create a tmp agent.`);
|
|
224
|
-
}
|
|
225
|
-
agent = upsertTmpAgent(root, { name: def.name, file: def.path, repo: p.repo, work: p.work, runtime: p.runtime, model: p.model });
|
|
226
|
-
} else if (!agent || agent.kind === "tmp") {
|
|
227
|
-
agent = upsertTmpAgent(root, {
|
|
228
|
-
name: p.agent, instructions: p.instructions, file: p.defFile,
|
|
229
|
-
repo: p.repo, work: p.work, runtime: p.runtime, model: p.model,
|
|
230
|
-
});
|
|
231
|
-
} else if (p.instructions !== undefined) {
|
|
232
|
-
throw new Error(`"${p.agent}" is a persistent agent — edit its soul instead of passing instructions`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const result = spawnInstance(root, agent!, {
|
|
237
|
-
instance: undefined, purpose: p.purpose, repo: p.repo || agent!.repo || defaultRepo(ctx.cwd),
|
|
238
|
-
work: p.work, runtime: p.runtime, model, task: p.task, branch: p.branch, launch: p.launch,
|
|
239
|
-
});
|
|
240
|
-
return { ...text(JSON.stringify(result, null, 2)), details: result };
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
pi.registerTool({
|
|
245
|
-
name: "agents_status",
|
|
246
|
-
label: "Agents Status",
|
|
247
|
-
description: [
|
|
248
|
-
"List the closest agents/ root: agents (persistent + tmp) with their souls and instances (running = has a live tmux window),",
|
|
249
|
-
"importable single-file defs from .claude/agents and .agents/agents, and the models currently enabled in pi.",
|
|
250
|
-
].join(" "),
|
|
251
|
-
promptSnippet: "List agents, their running instances, importable defs, and enabled models",
|
|
252
|
-
promptGuidelines: [
|
|
253
|
-
"Use agents_status to discover what agents exist and which models are enabled before spawning.",
|
|
254
|
-
],
|
|
255
|
-
parameters: Type.Object({}),
|
|
256
|
-
async execute(_id, _p, _signal, _onUpdate, ctx) {
|
|
257
|
-
const root = findRoot(ctx.cwd);
|
|
258
|
-
const details = {
|
|
259
|
-
root: root ?? null,
|
|
260
|
-
workspace: root ? workspaceOf(root) : null,
|
|
261
|
-
agents: root ? listInstances(root) : [],
|
|
262
|
-
agentDefs: listAgentDefs(ctx.cwd),
|
|
263
|
-
enabledModels: availableModels(ctx),
|
|
264
|
-
};
|
|
265
|
-
return { ...text(JSON.stringify(details, null, 2)), details };
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
pi.registerTool({
|
|
270
|
-
name: "retire_agent",
|
|
271
|
-
label: "Retire Agent",
|
|
272
|
-
description: "Retire an agent instance: kill its tmux window, remove its git worktree (if any), and delete its instance dir. Optionally delete its branch.",
|
|
273
|
-
promptSnippet: "Retire an agent instance (tmux window + worktree + instance dir)",
|
|
274
|
-
parameters: Type.Object({
|
|
275
|
-
instance: Type.String({ description: "Instance name (see agents_status)" }),
|
|
276
|
-
deleteBranch: Type.Optional(Type.Boolean({ description: "Also delete the instance's git branch (worktree mode)" })),
|
|
277
|
-
keepDir: Type.Optional(Type.Boolean({ description: "Keep the instance directory on disk" })),
|
|
278
|
-
}),
|
|
279
|
-
async execute(_id, p, _signal, _onUpdate, ctx) {
|
|
280
|
-
const root = ensureRoot(ctx.cwd);
|
|
281
|
-
if (isInstance && p.instance === process.env.PI_AGENT_INSTANCE) {
|
|
282
|
-
return { ...text(`"${p.instance}" is YOU — use the retire_self tool instead (it harvests your notes and schedules your window kill safely).`), isError: true };
|
|
283
|
-
}
|
|
284
|
-
const result = retireInstance(root, p.instance, { deleteBranch: p.deleteBranch, keepDir: p.keepDir });
|
|
285
|
-
return { ...text(JSON.stringify(result, null, 2)), details: result };
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// ---------- retire_self: only registered inside a spawned instance ----------
|
|
290
|
-
if (isInstance) {
|
|
291
|
-
pi.registerTool({
|
|
292
|
-
name: "retire_self",
|
|
293
|
-
label: "Retire Self",
|
|
294
|
-
description: "Retire THIS instance: run integration retire hooks (e.g. messaging identity self-delete, knowledge consolidation/harvest), remove the worktree and home dir, then kill this tmux window after a short delay. Irreversible. Use only when your task is complete (or your human told you to retire). Finish your memory FIRST per your knowledge integration's conventions (with okf: STATE.md, log.md, notes/) — retire hooks only take what is on disk when you call this.",
|
|
295
|
-
promptSnippet: "Retire this instance (harvest + cleanup + delayed window kill)",
|
|
296
|
-
parameters: Type.Object({
|
|
297
|
-
confirm: Type.Literal("retire", { description: 'Must be the string "retire" — confirms you mean to permanently retire this instance' }),
|
|
298
|
-
deleteBranch: Type.Optional(Type.Boolean({ description: "Also delete this instance's git branch (worktree mode)" })),
|
|
299
|
-
}),
|
|
300
|
-
async execute(_id, p, _signal, _onUpdate, ctx) {
|
|
301
|
-
const name = process.env.PI_AGENT_INSTANCE!;
|
|
302
|
-
const root = ensureRoot(ctx.cwd);
|
|
303
|
-
const result = retireInstance(root, name, { self: true, deleteBranch: p.deleteBranch });
|
|
304
|
-
return {
|
|
305
|
-
...text(`Retired. Harvested: ${JSON.stringify(result.harvested)}. This window dies in ~8s — say any goodbyes now.`),
|
|
306
|
-
details: result,
|
|
307
|
-
};
|
|
308
|
-
},
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ---------- /agents command ----------
|
|
313
|
-
pi.registerCommand("agents", {
|
|
314
|
-
description: "Spawn/list/retire agents from the closest agents/ directory",
|
|
315
|
-
handler: async (args, ctx) => {
|
|
316
|
-
if (!ctx.hasUI) { ctx.ui.notify("/agents needs an interactive UI", "error"); return; }
|
|
317
|
-
let root = findRoot(ctx.cwd);
|
|
318
|
-
const arg = (args || "").trim();
|
|
319
|
-
|
|
320
|
-
if (arg === "list" || (arg === "" && !root)) {
|
|
321
|
-
if (!root) { ctx.ui.notify(`No agents/ dir found above ${ctx.cwd} — mkdir agents, or set PI_AGENTS_ROOT`, "warning"); return; }
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (!root) return;
|
|
325
|
-
const agents = listInstances(root);
|
|
326
|
-
const defs = listAgentDefs(ctx.cwd).filter((d) => !agents.some((a) => a.name === d.name));
|
|
327
|
-
const running = agents.flatMap((a) => a.instances.filter((i: any) => i.running).map((i: any) => i.instance));
|
|
328
|
-
|
|
329
|
-
const items = [
|
|
330
|
-
...agents.map((a) => `spawn: ${a.name}${a.kind === "tmp" ? " (tmp)" : ""} — ${a.description || a.repo || ""}`),
|
|
331
|
-
...defs.map((d) => `import: ${d.name} — ${d.description || d.source}`),
|
|
332
|
-
"new adhoc agent…",
|
|
333
|
-
...(running.length ? ["retire an instance…"] : []),
|
|
334
|
-
"status",
|
|
335
|
-
];
|
|
336
|
-
const choice = await ctx.ui.select(`agents @ ${root}`, items);
|
|
337
|
-
if (!choice) return;
|
|
338
|
-
|
|
339
|
-
try {
|
|
340
|
-
if (choice === "status") {
|
|
341
|
-
const lines = agents.flatMap((a) => [
|
|
342
|
-
`${a.kind === "tmp" ? "[tmp] " : ""}${a.name} — repo=${a.repo || "?"} work=${a.work} runtime=${a.runtime}${a.model ? ` model=${a.model}` : ""}`,
|
|
343
|
-
...a.instances.map((i: any) => ` ${i.running ? "●" : "○"} ${i.instance}${i.branch ? ` [${i.branch}]` : ""}`),
|
|
344
|
-
]);
|
|
345
|
-
ctx.ui.notify(lines.join("\n") || "no agents", "info");
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (choice === "retire an instance…") {
|
|
350
|
-
const inst = await ctx.ui.select("Retire which instance?", running);
|
|
351
|
-
if (!inst) return;
|
|
352
|
-
const delBranch = await ctx.ui.confirm("Delete branch?", "Also delete the instance's git branch (if worktree)?");
|
|
353
|
-
const r = retireInstance(root, inst, { deleteBranch: delBranch });
|
|
354
|
-
ctx.ui.notify(`Retired ${r.retired}`, "info");
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Model picker shared by all spawn paths.
|
|
359
|
-
const pickModel = async (soulModel?: string): Promise<string | undefined> => {
|
|
360
|
-
const models = availableModels(ctx);
|
|
361
|
-
const def = soulModel ? `soul default (${soulModel})` : "pi default";
|
|
362
|
-
const picked = await ctx.ui.select("Model for this instance:", [def, ...models]);
|
|
363
|
-
if (!picked || picked === def) return soulModel;
|
|
364
|
-
return picked;
|
|
365
|
-
};
|
|
366
|
-
const askTask = async () => (await ctx.ui.editor("Task briefing (TASK.md):", "")) || undefined;
|
|
367
|
-
|
|
368
|
-
if (choice === "new adhoc agent…") {
|
|
369
|
-
const name = await ctx.ui.input("Adhoc agent name:", "e.g. api-doc-writer");
|
|
370
|
-
if (!name) return;
|
|
371
|
-
const instructions = await ctx.ui.editor("Agent instructions (its AGENTS.md):", "# Role\n\nYou are ");
|
|
372
|
-
if (!instructions?.trim()) return;
|
|
373
|
-
const repo = await ctx.ui.input("Repo (abs or relative to workspace):", defaultRepo(ctx.cwd) || "");
|
|
374
|
-
const work = await ctx.ui.select("Work mode:", ["checkout (work on the repo's current branch)", "worktree (own worktree + branch)"]);
|
|
375
|
-
if (!work) return;
|
|
376
|
-
const model = await pickModel();
|
|
377
|
-
const agent = upsertTmpAgent(root, {
|
|
378
|
-
name, instructions, repo: repo || defaultRepo(ctx.cwd) || undefined,
|
|
379
|
-
work: work.startsWith("worktree") ? "worktree" : "checkout", model,
|
|
380
|
-
});
|
|
381
|
-
const r = spawnInstance(root, agent, { task: await askTask(), model });
|
|
382
|
-
ctx.ui.notify(`Launched ${r.instance} — ${r.attach}`, "info");
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (choice.startsWith("import: ")) {
|
|
387
|
-
const name = choice.slice("import: ".length).split(" — ")[0];
|
|
388
|
-
const def = listAgentDefs(ctx.cwd).find((d) => d.name === name)!;
|
|
389
|
-
const repo = await ctx.ui.input("Repo (abs or relative to workspace):", defaultRepo(ctx.cwd) || "");
|
|
390
|
-
const agent = upsertTmpAgent(root, { name: def.name, file: def.path, repo: repo || defaultRepo(ctx.cwd) || undefined });
|
|
391
|
-
const model = await pickModel(agent.model);
|
|
392
|
-
const r = spawnInstance(root, agent, { task: await askTask(), model });
|
|
393
|
-
ctx.ui.notify(`Launched ${r.instance} — ${r.attach}`, "info");
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (choice.startsWith("spawn: ")) {
|
|
398
|
-
const name = choice.slice("spawn: ".length).split(" — ")[0].replace(" (tmp)", "");
|
|
399
|
-
const agent = findAgent(root, name)!;
|
|
400
|
-
const purpose = await ctx.ui.input("Purpose slug (names the instance):", "e.g. fix-auth");
|
|
401
|
-
const model = await pickModel(agent.model);
|
|
402
|
-
const r = spawnInstance(root, agent, { purpose: purpose || undefined, model, task: await askTask() });
|
|
403
|
-
ctx.ui.notify(`Launched ${r.instance} — ${r.attach}`, "info");
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
} catch (e: any) {
|
|
407
|
-
ctx.ui.notify(e.message, "error");
|
|
408
|
-
}
|
|
409
|
-
},
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
// ---------- convenience: create a persistent agent via tool too ----------
|
|
413
|
-
pi.registerTool({
|
|
414
|
-
name: "create_agent",
|
|
415
|
-
label: "Create Agent",
|
|
416
|
-
description: "Create a persistent agent soul under the closest agents/ directory (<root>/<name>/soul with soul.yaml + AGENTS.md). Does not spawn an instance.",
|
|
417
|
-
promptSnippet: "Create a persistent agent soul (soul.yaml + AGENTS.md) under agents/",
|
|
418
|
-
parameters: Type.Object({
|
|
419
|
-
name: Type.String(),
|
|
420
|
-
repo: Type.Optional(Type.String({ description: "Target repo (absolute, or relative to the agents root's parent)" })),
|
|
421
|
-
work: Type.Optional(StringEnum(["worktree", "checkout"] as const)),
|
|
422
|
-
runtime: Type.Optional(StringEnum(["pi", "claude"] as const)),
|
|
423
|
-
model: Type.Optional(Type.String({ description: "Default model as provider/id (must be enabled in pi)" })),
|
|
424
|
-
description: Type.Optional(Type.String()),
|
|
425
|
-
instructions: Type.Optional(Type.String({ description: "AGENTS.md content for the soul (default: a starter template)" })),
|
|
426
|
-
}),
|
|
427
|
-
async execute(_id, p, _signal, _onUpdate, ctx) {
|
|
428
|
-
const root = ensureRoot(ctx.cwd);
|
|
429
|
-
const model = validateModel(ctx, p.model);
|
|
430
|
-
const r = createAgent(root, { ...p, model });
|
|
431
|
-
if (p.instructions) {
|
|
432
|
-
writeSoul(root, { name: r.agent, kind: "persistent", repo: p.repo, work: p.work, runtime: p.runtime, model, description: p.description, instructions: p.instructions });
|
|
433
|
-
}
|
|
434
|
-
return { ...text(`Created soul: ${r.soul}\nEdit ${r.soul}/AGENTS.md to shape the agent, then spawn_agent.`), details: r };
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
110
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oas-framework/pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "OAS pi adapter — agent tools (spawn_agent, agents_status, retire_agent) and skill discovery for pi, over the globally installed @oas-framework/oas kernel",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|