@fradser/pi-agent-teams 0.5.3 → 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/.memory/MEMORY.md +3 -1
- package/.memory/node-terminal-followups.md +3 -2
- package/.memory/team-centric-design.md +21 -0
- package/README.md +99 -63
- package/agents/observer.md +1 -1
- package/agents/reviewer.md +4 -2
- package/agents/specialist.md +1 -1
- package/agents/worker.md +3 -3
- package/package.json +3 -3
- package/src/activity.ts +86 -0
- package/src/agents.ts +58 -21
- package/src/follow-up-queue.ts +93 -26
- package/src/guidance.ts +81 -60
- package/src/index.ts +92 -28
- package/src/spawner.ts +194 -350
- package/src/state.ts +252 -565
- package/src/statefile.ts +209 -46
- package/src/team-machine.ts +946 -0
- package/src/tools.ts +177 -173
- package/src/types.ts +153 -189
- package/src/ui.ts +306 -230
- package/src/worker.ts +172 -34
- package/src/worktree.ts +20 -8
- package/.memory/run-centric-design.md +0 -22
- package/src/run-machine.ts +0 -606
- package/src/terminal.ts +0 -34
package/src/tools.ts
CHANGED
|
@@ -1,213 +1,217 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { formatAgentMessagePrefix, formatAgentTaskName, formatToolEventLabel } from "@fradser/pi-kit";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
cancelNodeAndTerminate,
|
|
7
|
-
cancelRunAndTerminate,
|
|
8
|
-
ensureRunContext,
|
|
4
|
+
createBoardTask,
|
|
5
|
+
formatSilenceDuration,
|
|
9
6
|
publishStateSnapshot,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
7
|
+
sendLeaderMessage,
|
|
8
|
+
shutdownTeammate,
|
|
9
|
+
spawnTeammate,
|
|
10
|
+
} from "./team-machine.ts";
|
|
11
|
+
import { listTasks, livingTeammates } from "./state.ts";
|
|
12
|
+
import { LEADER_RECIPIENT, SendMessageParams, TeammateShutdownParams, TeammateSpawnParams, TaskCreateParams } from "./types.ts";
|
|
13
|
+
import { registerTaskListTool } from "./worker.ts";
|
|
14
|
+
import { openTeamConsole, refreshTeamUI } from "./ui.ts";
|
|
15
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
17
18
|
|
|
18
|
-
function
|
|
19
|
-
return
|
|
19
|
+
function isValidAgentName(name: string): boolean {
|
|
20
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && !name.includes("..") && name !== "con";
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async function gatherForeground(runId: string, signal: AbortSignal | undefined): Promise<string> {
|
|
27
|
-
const foregroundTimeoutMs = 5 * 60 * 1000;
|
|
28
|
-
const deadline = Date.now() + foregroundTimeoutMs;
|
|
29
|
-
while (true) {
|
|
30
|
-
applyWorkerEvents();
|
|
31
|
-
const run = getRun(runId);
|
|
32
|
-
if (!run || run.status !== "running") break;
|
|
33
|
-
if (signal?.aborted) {
|
|
34
|
-
run.background = true;
|
|
35
|
-
run.completionNotified = false;
|
|
36
|
-
publishStateSnapshot();
|
|
37
|
-
throw new Error(`Run [${runId}] continues in the background — workers will message team-leader upon completion.`);
|
|
38
|
-
}
|
|
39
|
-
if (Date.now() >= deadline) {
|
|
40
|
-
run.background = true;
|
|
41
|
-
run.completionNotified = false;
|
|
42
|
-
publishStateSnapshot();
|
|
43
|
-
return `Run [${runId}] is still running after ${Math.round(foregroundTimeoutMs / 1000)}s — detached to background. Workers will message team-leader upon completion.`;
|
|
44
|
-
}
|
|
45
|
-
await sleep(150);
|
|
46
|
-
}
|
|
47
|
-
markRunCompletionDelivered(runId);
|
|
48
|
-
publishStateSnapshot();
|
|
49
|
-
return buildRunSummary(runId);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function resolveTargetNode(target: string): { node: import("./types").Node } | undefined {
|
|
53
|
-
return listNodes().map((node) => ({ node })).find(({ node }) => node.id === target || node.workerKey === target);
|
|
23
|
+
function rosterSummary(): string {
|
|
24
|
+
const alive = livingTeammates();
|
|
25
|
+
if (alive.length === 0) return "No living teammates.";
|
|
26
|
+
return alive.map((t) => `@${t.name} (${t.agent}, ${t.status}${t.currentTaskId ? `, task ${t.currentTaskId}` : ""})`).join("\n");
|
|
54
27
|
}
|
|
55
28
|
|
|
56
29
|
export function registerLeaderTools(pi: ExtensionAPI): void {
|
|
57
30
|
pi.registerTool({
|
|
58
|
-
name: "
|
|
59
|
-
promptSnippet: "
|
|
60
|
-
label: "
|
|
61
|
-
description: "
|
|
62
|
-
parameters:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
throw new Error(`Agent "${task.agent}" not found in any scope. Available agents: ${available || "(none)"}.`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const created = createRun({
|
|
75
|
-
cwd,
|
|
76
|
-
concurrency: params.concurrency ?? 4,
|
|
77
|
-
worktree: params.worktree ?? false,
|
|
78
|
-
background: params.background ?? true,
|
|
79
|
-
summarize: params.summarize,
|
|
80
|
-
summaryAgent: params.summaryAgent,
|
|
81
|
-
nodes: params.tasks.map((task) => ({
|
|
82
|
-
id: task.id,
|
|
83
|
-
agent: task.agent,
|
|
84
|
-
prompt: task.prompt,
|
|
85
|
-
paths: task.paths,
|
|
86
|
-
access: task.access ?? "read",
|
|
87
|
-
model: task.model,
|
|
88
|
-
mode: task.mode,
|
|
89
|
-
turnBudget: task.turnBudget,
|
|
90
|
-
forkContext: task.forkContext,
|
|
91
|
-
inputBindings: task.inputBindings,
|
|
92
|
-
dependsOn: task.dependsOn ?? [],
|
|
93
|
-
})),
|
|
94
|
-
});
|
|
95
|
-
if (!created.ok) throw new Error(created.error ?? "Failed to create run.");
|
|
96
|
-
const run = created.run;
|
|
97
|
-
publishStateSnapshot();
|
|
98
|
-
refreshTeamUI(ctx);
|
|
99
|
-
scheduleRun(run.id, dctx);
|
|
100
|
-
|
|
101
|
-
if (run.background) {
|
|
102
|
-
return {
|
|
103
|
-
content: [{ type: "text", text: `Started run [${run.id}] "${Object.keys(run.nodes).length} node(s)" — background.\nConcurrency: ${run.concurrency} | Worktree: ${run.worktree ? "yes" : "no"}\n\nWorkers will report their deliverables through the automatic completion follow-up. Do not sleep to wait.` }],
|
|
104
|
-
details: {},
|
|
105
|
-
};
|
|
31
|
+
name: "teammate_spawn",
|
|
32
|
+
promptSnippet: "Spawn a named resident teammate",
|
|
33
|
+
label: "Spawn Teammate",
|
|
34
|
+
description: "Spawn one named resident teammate. Model and worktree behavior come from its declarative agent definition; the kickoff prompt is optional.",
|
|
35
|
+
parameters: TeammateSpawnParams,
|
|
36
|
+
// Canonical lifecycle rows (same as packages/monitor): empty renderCall,
|
|
37
|
+
// ONE startup row owned by renderResult.
|
|
38
|
+
renderShell: "self",
|
|
39
|
+
renderCall: () => new Text("", 0, 0),
|
|
40
|
+
renderResult(result, _options, theme, context) {
|
|
41
|
+
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
42
|
+
if ((result as { isError?: boolean }).isError) {
|
|
43
|
+
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to spawn teammate."), 0, 0);
|
|
106
44
|
}
|
|
107
|
-
|
|
45
|
+
const params = context.args as { name: string; agent: string; prompt?: string };
|
|
46
|
+
const line = `${theme.fg("toolTitle", theme.bold(formatToolEventLabel("started", "", "agent").trimEnd()))} ${theme.fg("accent", `@${params.name}`)} ${theme.fg("dim", "·")} ${theme.fg("customMessageText", theme.bold(formatAgentTaskName(params.prompt ?? "", params.name)))}`;
|
|
47
|
+
return {
|
|
48
|
+
render: (width: number) => width > 0 ? [truncateToWidth(line, width)] : [],
|
|
49
|
+
invalidate: () => {},
|
|
50
|
+
};
|
|
108
51
|
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
pi.registerTool({
|
|
112
|
-
name: "teammate_fanout",
|
|
113
|
-
promptSnippet: "Fan out a completed node's structured array output",
|
|
114
|
-
label: "Fan Out Tasks",
|
|
115
|
-
description: "Leader-only bounded fanout. Validates a completed structured array before creating a separate child run.",
|
|
116
|
-
parameters: TeammateFanoutParams,
|
|
117
52
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
118
|
-
const
|
|
119
|
-
if (!
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
prompt: `${params.prompt}\n\nFanout item:\n${JSON.stringify(item)}`,
|
|
129
|
-
paths: params.paths,
|
|
130
|
-
access: params.access ?? "read" as const,
|
|
131
|
-
model: params.model,
|
|
132
|
-
mode: "json" as const,
|
|
133
|
-
turnBudget: params.turnBudget,
|
|
134
|
-
dependsOn: [],
|
|
135
|
-
}));
|
|
136
|
-
if (!resolveAgent(params.agent, cwd)) throw new Error(`Agent "${params.agent}" not found.`);
|
|
137
|
-
const created = createRun({ cwd, concurrency: params.concurrency ?? 4, worktree: false, background: params.background ?? true, summarize: true, nodes: tasks });
|
|
138
|
-
if (!created.ok) throw new Error(created.error ?? "Failed to create fanout run.");
|
|
139
|
-
publishStateSnapshot();
|
|
140
|
-
scheduleRun(created.run.id, dctx);
|
|
141
|
-
return { content: [{ type: "text", text: `Started fanout run [${created.run.id}] with ${tasks.length} item(s).` }], details: {} };
|
|
53
|
+
const result = spawnTeammate(params);
|
|
54
|
+
if (!result.ok) throw new Error(result.error);
|
|
55
|
+
refreshTeamUI(ctx);
|
|
56
|
+
const kickoffNote = params.prompt?.trim()
|
|
57
|
+
? "It received your kickoff prompt and is working on it."
|
|
58
|
+
: "It is idle: it wakes for inbox messages and claimable board tasks.";
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: `@${params.name} is alive as ${params.agent}.\n${kickoffNote}\n\n${rosterSummary()}` }],
|
|
61
|
+
details: { started: true },
|
|
62
|
+
};
|
|
142
63
|
},
|
|
143
64
|
});
|
|
144
65
|
|
|
145
66
|
pi.registerTool({
|
|
146
|
-
name: "
|
|
147
|
-
promptSnippet: "
|
|
148
|
-
label: "
|
|
149
|
-
description: "
|
|
150
|
-
parameters:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
67
|
+
name: "teammate_shutdown",
|
|
68
|
+
promptSnippet: "Shut down a resident teammate",
|
|
69
|
+
label: "Shutdown Teammate",
|
|
70
|
+
description: "Gracefully stop one named teammate. Its claimed task returns to the board; its worktree diff is captured before teardown.",
|
|
71
|
+
parameters: TeammateShutdownParams,
|
|
72
|
+
renderShell: "self",
|
|
73
|
+
renderCall: () => new Text("", 0, 0),
|
|
74
|
+
renderResult(result, _options, theme, context) {
|
|
75
|
+
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
76
|
+
if ((result as { isError?: boolean }).isError) {
|
|
77
|
+
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to shut down teammate."), 0, 0);
|
|
156
78
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
return {
|
|
79
|
+
const name = String((context.args as { name?: string }).name ?? "");
|
|
80
|
+
const line = `${theme.fg("toolTitle", theme.bold(formatToolEventLabel("event", `@${name} shut down`).trimEnd()))}`;
|
|
81
|
+
return {
|
|
82
|
+
render: (width: number) => [truncateToWidth(line, Math.max(1, width))],
|
|
83
|
+
invalidate: () => {},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
87
|
+
const result = await shutdownTeammate(params.name);
|
|
88
|
+
if (!result.ok) throw new Error(result.error);
|
|
89
|
+
refreshTeamUI(ctx);
|
|
90
|
+
return { content: [{ type: "text", text: result.body }], details: {} };
|
|
160
91
|
},
|
|
161
92
|
});
|
|
162
93
|
|
|
163
94
|
pi.registerTool({
|
|
164
|
-
name: "
|
|
165
|
-
promptSnippet: "
|
|
166
|
-
label: "
|
|
167
|
-
description: "
|
|
168
|
-
parameters:
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
95
|
+
name: "send_message",
|
|
96
|
+
promptSnippet: "Send a message to a teammate",
|
|
97
|
+
label: "Send Message",
|
|
98
|
+
description: "The only messaging primitive. Address a living teammate by name; working teammates receive a steer immediately and idle teammates wake with the message. status is reserved for worker reports to leader.",
|
|
99
|
+
parameters: SendMessageParams,
|
|
100
|
+
renderCall(args, theme) {
|
|
101
|
+
const prefix = theme.fg("toolTitle", theme.bold(formatAgentMessagePrefix("to")));
|
|
102
|
+
const recipient = theme.fg("accent", `@${String(args.to ?? "")}`);
|
|
103
|
+
return new Text(`${prefix}${recipient}`, 0, 0);
|
|
104
|
+
},
|
|
105
|
+
async execute(_toolCallId, params) {
|
|
106
|
+
if (params.to === LEADER_RECIPIENT) throw new Error('The leader cannot send a message to itself.');
|
|
107
|
+
if (params.status) throw new Error('status is reserved for worker reports to="leader".');
|
|
108
|
+
const result = sendLeaderMessage(params.to, params.message);
|
|
175
109
|
if (!result.ok) throw new Error(result.error);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
110
|
+
const disposition = result.queued ? "Queued for its next wake-up." : "Delivered to its running turn.";
|
|
111
|
+
let text = `Message to @${params.to}: ${disposition}`;
|
|
112
|
+
if (!result.queued && result.stalledMs !== undefined) {
|
|
113
|
+
text += `\nWarning: @${params.to} has been stalled with no output for ${formatSilenceDuration(result.stalledMs)} — the control-stream write succeeded, but the teammate may be wedged. Consider teammate_shutdown if it stays silent.`;
|
|
114
|
+
}
|
|
115
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
179
116
|
},
|
|
180
117
|
});
|
|
181
118
|
|
|
182
119
|
pi.registerTool({
|
|
183
|
-
name: "
|
|
184
|
-
promptSnippet: "
|
|
185
|
-
label: "
|
|
186
|
-
description: "
|
|
187
|
-
parameters:
|
|
120
|
+
name: "task_create",
|
|
121
|
+
promptSnippet: "Create a shared board task",
|
|
122
|
+
label: "Create Task",
|
|
123
|
+
description: "Create one shared board task. Resident teammates self-claim it when dependencies are met; an optional verify command gates completion.",
|
|
124
|
+
parameters: TaskCreateParams,
|
|
188
125
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
126
|
+
const tasks = listTasks();
|
|
127
|
+
if (params.dependsOn?.some((dep) => !tasks.some((task) => task.id === dep))) {
|
|
128
|
+
throw new Error(`Unknown dependency id in [${params.dependsOn.join(", ")}].`);
|
|
129
|
+
}
|
|
130
|
+
const created = createBoardTask(params);
|
|
131
|
+
if (!created.ok) throw new Error(created.error);
|
|
195
132
|
refreshTeamUI(ctx);
|
|
196
|
-
return { content: [{ type: "text", text: `
|
|
133
|
+
return { content: [{ type: "text", text: `Created [${created.id}] "${params.subject}". Idle teammates are notified automatically.` }], details: {} };
|
|
197
134
|
},
|
|
198
135
|
});
|
|
136
|
+
|
|
137
|
+
registerTaskListTool(pi);
|
|
199
138
|
}
|
|
200
139
|
|
|
201
140
|
export function registerTeamCommand(pi: ExtensionAPI): void {
|
|
202
|
-
pi.registerCommand("
|
|
203
|
-
description: "
|
|
141
|
+
pi.registerCommand("agent-teams", {
|
|
142
|
+
description: "Agent Teams menu: console and create agents from session history",
|
|
204
143
|
handler: async (_args, ctx) => {
|
|
205
|
-
|
|
206
|
-
|
|
144
|
+
const choice = await ctx.ui.select("agent-teams", ["console", "project", "local"]);
|
|
145
|
+
if (!choice) return;
|
|
146
|
+
if (choice === "console") {
|
|
147
|
+
publishStateSnapshot();
|
|
148
|
+
if (ctx.mode !== "tui") { ctx.ui.notify(rosterSummary(), "info"); return; }
|
|
149
|
+
await openTeamConsole(ctx);
|
|
150
|
+
refreshTeamUI(ctx);
|
|
207
151
|
return;
|
|
208
152
|
}
|
|
209
|
-
await
|
|
210
|
-
refreshTeamUI(ctx);
|
|
153
|
+
await createAgentFromHistory(ctx, choice === "local");
|
|
211
154
|
},
|
|
212
155
|
});
|
|
213
156
|
}
|
|
157
|
+
|
|
158
|
+
interface GeneratedAgent { name?: string; description?: string; tools?: string[]; prompt?: string; content?: string; }
|
|
159
|
+
|
|
160
|
+
function parseGeneratedAgents(raw: string): GeneratedAgent[] {
|
|
161
|
+
const cleaned = raw.trim().replace(/^```(?:json)?\s*|\s*```$/g, "");
|
|
162
|
+
const start = cleaned.indexOf("[");
|
|
163
|
+
const end = cleaned.lastIndexOf("]");
|
|
164
|
+
if (start < 0 || end <= start) return [];
|
|
165
|
+
try {
|
|
166
|
+
const value: unknown = JSON.parse(cleaned.slice(start, end + 1));
|
|
167
|
+
return Array.isArray(value) ? value.filter((item): item is GeneratedAgent => Boolean(item && typeof item === "object")) : [];
|
|
168
|
+
} catch { return []; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderAgentDefinition(agent: GeneratedAgent): string {
|
|
172
|
+
const name = agent.name!.trim();
|
|
173
|
+
const description = (agent.description || "Agent created from session history").replace(/[\r\n]+/g, " ").trim();
|
|
174
|
+
const tools = Array.isArray(agent.tools) ? agent.tools.filter((tool) => typeof tool === "string" && tool.trim()).join(", ") : "";
|
|
175
|
+
return `---\nname: ${name}\ndescription: ${description}${tools ? `\ntools: ${tools}` : ""}\n---\n\n${(agent.prompt || "").trim()}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function createAgentFromHistory(ctx: ExtensionContext, local: boolean): Promise<void> {
|
|
179
|
+
const branch = ctx.sessionManager?.getBranch?.() ?? [];
|
|
180
|
+
const history = branch.map((entry: any) => {
|
|
181
|
+
const text = typeof entry?.text === "string" ? entry.text : typeof entry?.content === "string" ? entry.content : "";
|
|
182
|
+
return text ? text : Array.isArray(entry?.content) ? entry.content.map((part: any) => part?.text ?? "").join("") : "";
|
|
183
|
+
}).filter(Boolean).slice(-20).join("\n\n");
|
|
184
|
+
const generated = await generateAgentPrompt(ctx, history);
|
|
185
|
+
if (!generated) return;
|
|
186
|
+
const dir = path.join(ctx.cwd || process.cwd(), ".pi", "agents");
|
|
187
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
188
|
+
const definitions = parseGeneratedAgents(generated)
|
|
189
|
+
.filter((item) => typeof item.name === "string" && (item.content?.trim() || item.prompt?.trim()))
|
|
190
|
+
.map((item) => ({ name: item.name!.trim(), content: item.content?.trim() || renderAgentDefinition(item) }));
|
|
191
|
+
if (definitions.length === 0) { ctx.ui.notify("Model returned no usable agent definitions", "error"); return; }
|
|
192
|
+
let created = 0;
|
|
193
|
+
for (const definition of definitions) {
|
|
194
|
+
if (!isValidAgentName(definition.name)) continue;
|
|
195
|
+
const filename = `${definition.name}${local ? ".local" : ""}.md`;
|
|
196
|
+
try { fs.writeFileSync(path.join(dir, filename), definition.content, { encoding: "utf8", flag: "wx" }); created++; } catch (error) {
|
|
197
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
ctx.ui.notify(`Created ${created} agent definition${created === 1 ? "" : "s"}`, created ? "info" : "warning");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function generateAgentPrompt(ctx: ExtensionContext, history: string): Promise<string | undefined> {
|
|
204
|
+
const model = ctx.model;
|
|
205
|
+
const registry = ctx.modelRegistry;
|
|
206
|
+
if (!model || !registry?.complete) {
|
|
207
|
+
ctx.ui.notify("No model available to generate an agent", "error");
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const response = await registry.complete(model, {
|
|
211
|
+
systemPrompt: "Design a complementary resident coding agent team from session history. Return a JSON array of 2-4 complementary agent definitions. Each item must have name, description, tools, and prompt fields. Names use letters, digits, dots, dashes, underscores. Return JSON only, with no code fences.",
|
|
212
|
+
messages: [{ role: "user", content: `Session history:\n${history || "(empty)"}`, timestamp: Date.now() }],
|
|
213
|
+
}, { maxTokens: 2000, temperature: 0, cacheRetention: "none" });
|
|
214
|
+
const content = response.content?.filter((part: any) => part.type === "text").map((part: any) => part.text).join("\\n").trim();
|
|
215
|
+
if (!content) { ctx.ui.notify("Model returned no agent definition", "error"); return undefined; }
|
|
216
|
+
return content;
|
|
217
|
+
}
|