@metrio-ai/cli 1.4.0 → 1.5.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.
@@ -0,0 +1,380 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentDirRefs = agentDirRefs;
4
+ exports.unresolvedRef = unresolvedRef;
5
+ exports.subAgentKeys = subAgentKeys;
6
+ exports.pushAgent = pushAgent;
7
+ exports.bumpUnchangedPins = bumpUnchangedPins;
8
+ exports.parentsOutOfScope = parentsOutOfScope;
9
+ exports.checkAgentRefs = checkAgentRefs;
10
+ exports.agentWriteOrder = agentWriteOrder;
11
+ exports.agentDeleteOrder = agentDeleteOrder;
12
+ const yaml_1 = require("yaml");
13
+ const errors_1 = require("../errors");
14
+ const push_1 = require("./push");
15
+ const render_1 = require("./render");
16
+ const workspace_1 = require("./workspace");
17
+ /*
18
+ * Agents (plan Decisions 3-4). agent.yaml names its entry prompt and sub-agents by workspace
19
+ * directory (resolved through state, or through what this push created: prompts and children
20
+ * are pushed first) or by numeric id. Pin bumping: a sub-agent link whose child's entry prompt
21
+ * got a new head version in this push (--activate, or a new prompt's first version) moves to
22
+ * that version's serial, written through the parent's own set_agent_tools when the parent is
23
+ * pushed anyway, else through bump_agent_pins; the parent's agent.yaml is rewritten so the next
24
+ * diff is clean. A plain push's non-head version is not live yet (push, verify, push --activate),
25
+ * so it moves no pin. Entry versions are never bumped.
26
+ */
27
+ /** The directory references of an agent file, with the field that holds each. */
28
+ function agentDirRefs(doc) {
29
+ const refs = doc.entry_prompt !== undefined ? [{ key: `prompts/${doc.entry_prompt}`, field: 'entry_prompt', dir: doc.entry_prompt }] : [];
30
+ doc.sub_agents.forEach((s, i) => {
31
+ if (s.agent !== undefined)
32
+ refs.push({ key: `agents/${s.agent}`, field: `sub_agents[${i}].agent`, dir: s.agent });
33
+ });
34
+ return refs;
35
+ }
36
+ function unresolvedRef(agentKey, ref, why = `${ref.key} is not tracked and not created by this push`) {
37
+ return new errors_1.CliError(`${agentKey}/agent.yaml: ${ref.field} "${ref.dir}" does not resolve (${why})`);
38
+ }
39
+ const parseAgentEntry = (e) => (0, render_1.parseAgent)(e.local['agent.yaml'] ?? '', `${e.key}/agent.yaml`);
40
+ /** Workspace keys of an agent's sub-agents among `entries` (local file, else the remote links), sorted. */
41
+ function subAgentKeys(e, entries) {
42
+ const keyOf = (id) => entries.find((x) => x.kind === 'agent' && (x.state?.id ?? x.remote?.id) === String(id))?.key;
43
+ let keys;
44
+ if (e.local) {
45
+ try {
46
+ keys = parseAgentEntry(e).sub_agents.map((s) => (s.agent !== undefined ? `agents/${s.agent}` : keyOf(s.agent_id)));
47
+ }
48
+ catch {
49
+ return []; // the push reports the parse error
50
+ }
51
+ }
52
+ else
53
+ keys = (e.remote?.data?.sub_agents ?? []).map((s) => keyOf(s.agent_id));
54
+ return [...new Set(keys.filter((k) => k !== undefined))].sort();
55
+ }
56
+ /** The console id of the entry prompt an agent file names, if its directory resolves. */
57
+ const entryPromptId = (sync, doc) => doc.entry_prompt !== undefined ? sync.ids.get(`prompts/${doc.entry_prompt}`) : String(doc.entry_prompt_id);
58
+ /** The local agent.yaml of a tracked agent outside this push's scope, if it has one. */
59
+ function trackedAgentFile(sync, key) {
60
+ const local = (0, workspace_1.readLocal)(sync.root, key, 'agent');
61
+ return local ? (0, render_1.parseAgent)(local['agent.yaml'] ?? '', `${key}/agent.yaml`) : undefined;
62
+ }
63
+ /** The serial this push gave the entry prompt of agent `agentId`, if it created one. */
64
+ function newEntrySerial(sync, agentId) {
65
+ const child = sync.entries.find((x) => x.kind === 'agent' && (sync.ids.get(x.key) ?? x.remote?.id) === String(agentId));
66
+ let promptId;
67
+ try {
68
+ if (child?.local)
69
+ promptId = entryPromptId(sync, parseAgentEntry(child));
70
+ else if (child?.remote)
71
+ promptId = String(child.remote.data.entry_prompt_external_id);
72
+ else if (!child) {
73
+ // A tracked child outside the scope: its local agent.yaml names the entry prompt.
74
+ const key = [...sync.ids].find(([k, id]) => k.startsWith('agents/') && id === String(agentId))?.[0];
75
+ const doc = key === undefined ? undefined : trackedAgentFile(sync, key);
76
+ promptId = doc && entryPromptId(sync, doc);
77
+ }
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ const serial = promptId === undefined ? undefined : sync.promptSerials.get(promptId);
83
+ return serial === undefined ? undefined : String(serial);
84
+ }
85
+ function resolveAgent(sync, e, doc) {
86
+ const refs = agentDirRefs(doc);
87
+ const idOf = (field) => {
88
+ const ref = refs.find((r) => r.field === field);
89
+ const id = sync.ids.get(ref.key);
90
+ if (id === undefined)
91
+ throw unresolvedRef(e.key, ref);
92
+ return Number(id);
93
+ };
94
+ const entryPrompt = doc.entry_prompt !== undefined ? idOf('entry_prompt') : doc.entry_prompt_id;
95
+ const remoteLinks = e.remote?.data?.sub_agents ?? [];
96
+ const pins = new Map();
97
+ const subAgents = doc.sub_agents.map((s, i) => {
98
+ const agentId = s.agent !== undefined ? idOf(`sub_agents[${i}].agent`) : s.agent_id;
99
+ // Only a numeric pin still equal to the console's moves: tags and pins edited by hand stay.
100
+ const untouched = /^\d+$/.test(s.version) && remoteLinks.some((l) => l.agent_id === agentId && l.version_pin === s.version);
101
+ const bumped = sync.bumpPins && untouched ? newEntrySerial(sync, agentId) : undefined;
102
+ if (bumped !== undefined && bumped !== s.version)
103
+ pins.set(i, bumped);
104
+ return { agent_id: agentId, version_pin: pins.get(i) ?? s.version };
105
+ });
106
+ return { entryPrompt, subAgents, pins };
107
+ }
108
+ const policy = (p) => ({
109
+ tool_name: p.tool_name, needs_approval: p.needs_approval, ...(typeof p.timeout_ms === 'number' ? { timeout_ms: p.timeout_ms } : {}),
110
+ });
111
+ const toolSet = (subAgents, policies) => (0, render_1.canonical)({
112
+ sub_agents: [...subAgents].sort((a, b) => a.agent_id - b.agent_id),
113
+ tool_policies: policies.map(policy).sort((a, b) => (a.tool_name < b.tool_name ? -1 : a.tool_name > b.tool_name ? 1 : 0)),
114
+ });
115
+ /** agent.yaml with agent_id written back (first key) and bumped pins; comments and layout are kept. */
116
+ function rewriteAgentYaml(text, agentId, pins) {
117
+ const doc = (0, yaml_1.parseDocument)(text);
118
+ if (agentId !== undefined)
119
+ doc.contents.items.unshift(doc.createPair('agent_id', agentId));
120
+ for (const [i, version] of pins)
121
+ doc.setIn(['sub_agents', i, 'version'], version);
122
+ return doc.toString();
123
+ }
124
+ function writeAgentFile(sync, e, text) {
125
+ const files = { 'agent.yaml': text };
126
+ if (text !== e.local['agent.yaml'])
127
+ (0, workspace_1.writeLocal)(sync.root, e.key, 'agent', files);
128
+ return files;
129
+ }
130
+ const bumpSummary = (doc, r) => [...r.pins].map(([i, v]) => `pinned ${doc.sub_agents[i].agent ?? doc.sub_agents[i].agent_id} to version ${v}`);
131
+ async function pushAgent(ctx, e) {
132
+ const sync = ctx.agents;
133
+ if (!sync)
134
+ throw new errors_1.CliError('pushing agents needs the workspace context');
135
+ const text = e.local['agent.yaml'] ?? '';
136
+ const doc = parseAgentEntry(e);
137
+ const resolved = resolveAgent(sync, e, doc);
138
+ const { entryPrompt, subAgents, pins } = resolved;
139
+ const fields = {
140
+ name: doc.name, description: doc.description, entry_prompt_external_id: entryPrompt, entry_version_pin: doc.entry_version,
141
+ max_turns: doc.max_turns, status: doc.status,
142
+ };
143
+ const remote = e.remote?.data;
144
+ if (!remote) {
145
+ const tools = subAgents.length || doc.tool_policies.length ? { sub_agents: subAgents, tool_policies: doc.tool_policies } : undefined;
146
+ const row = await (0, push_1.call)(ctx, 'create_agent', { external_id: doc.agent_id, ...fields, tools });
147
+ const created = row.agent?.agent_id;
148
+ if (typeof created !== 'number') {
149
+ throw new errors_1.CliError(`${e.key}: create_agent answered without an agent_id; check "metrio agent list" before pushing again`);
150
+ }
151
+ const id = String(created);
152
+ try {
153
+ const files = writeAgentFile(sync, e, rewriteAgentYaml(text, doc.agent_id === undefined ? created : undefined, pins));
154
+ return { id, files, summary: ['created', ...bumpSummary(doc, resolved)].join(', ') };
155
+ }
156
+ catch (error) {
157
+ throw await (0, push_1.undoCreate)(ctx, 'delete_agent', { agent_id: created }, `agent ${id}`, error);
158
+ }
159
+ }
160
+ const id = e.remote.id;
161
+ if (doc.agent_id !== undefined && String(doc.agent_id) !== id) {
162
+ throw new errors_1.CliError(`${e.key}/agent.yaml: agent_id must be ${id} (agent ids cannot change)`);
163
+ }
164
+ const before = {
165
+ name: remote.name, description: remote.description, entry_prompt_external_id: remote.entry_prompt_external_id,
166
+ entry_version_pin: remote.entry_version_pin, max_turns: remote.max_turns, status: remote.status,
167
+ };
168
+ const changed = Object.fromEntries(Object.entries(fields).filter(([k, v]) => v !== undefined && v !== before[k]));
169
+ const done = [];
170
+ if (Object.keys(changed).length) {
171
+ await (0, push_1.call)(ctx, 'update_agent', { agent_id: Number(id), ...changed });
172
+ done.push(`${Object.keys(changed).join(', ')} updated`);
173
+ }
174
+ if (toolSet(subAgents, doc.tool_policies) !== toolSet(remote.sub_agents, remote.tool_policies)) {
175
+ await (0, push_1.call)(ctx, 'set_agent_tools', { agent_id: Number(id), sub_agents: subAgents, tool_policies: doc.tool_policies });
176
+ done.push('tools replaced');
177
+ }
178
+ const files = pins.size ? writeAgentFile(sync, e, rewriteAgentYaml(text, undefined, pins)) : undefined;
179
+ return { id, ...(files ? { files } : {}), summary: [...done, ...bumpSummary(doc, resolved)].join(', ') || 'no change' };
180
+ }
181
+ /**
182
+ * Moves the sub-agent pins of agents this push did not write (an unchanged agent whose child's
183
+ * entry prompt got a new version) with bump_agent_pins, and rewrites their agent.yaml. Parents
184
+ * are sent in chunks of up to 200 pins; `onBumped` runs for each parent once its chunk applied,
185
+ * so a later failing chunk leaves the earlier bumps recorded.
186
+ */
187
+ async function bumpUnchangedPins(ctx, entries, onBumped) {
188
+ const sync = ctx.agents;
189
+ if (!sync.bumpPins || !sync.promptSerials.size)
190
+ return;
191
+ const bumps = [];
192
+ for (const e of entries) {
193
+ if (e.kind !== 'agent' || e.status !== 'unchanged' || !e.local || !e.remote)
194
+ continue;
195
+ try {
196
+ const doc = parseAgentEntry(e);
197
+ const resolved = resolveAgent(sync, e, doc);
198
+ const pins = [...resolved.pins].map(([i, version]) => ({ agent_id: Number(e.remote.id), sub_agent_id: resolved.subAgents[i].agent_id, version_pin: version }));
199
+ if (pins.length)
200
+ bumps.push({ entry: e, doc, resolved, pins });
201
+ }
202
+ catch {
203
+ continue; // an unchanged file that no longer resolves: nothing to bump
204
+ }
205
+ }
206
+ const chunks = [];
207
+ for (const b of bumps) {
208
+ const last = chunks[chunks.length - 1];
209
+ if (last && last.reduce((n, x) => n + x.pins.length, 0) + b.pins.length <= 200)
210
+ last.push(b);
211
+ else
212
+ chunks.push([b]);
213
+ }
214
+ for (const chunk of chunks) {
215
+ const pins = chunk.flatMap((b) => b.pins);
216
+ try {
217
+ // A parent with more than 200 links still needs several calls.
218
+ for (let i = 0; i < pins.length; i += 200)
219
+ await (0, push_1.call)(ctx, 'bump_agent_pins', { pins: pins.slice(i, i + 200) });
220
+ }
221
+ catch (error) {
222
+ throw new errors_1.CliError(`sub-agent pins of ${chunk.map((b) => b.entry.key).join(', ')} not bumped: ${(0, push_1.message)(error)}`);
223
+ }
224
+ for (const { entry, doc, resolved } of chunk) {
225
+ const files = writeAgentFile(sync, entry, rewriteAgentYaml(entry.local['agent.yaml'], undefined, resolved.pins));
226
+ onBumped(entry, { id: entry.remote.id, files, summary: bumpSummary(doc, resolved).join(', ') });
227
+ }
228
+ }
229
+ }
230
+ /**
231
+ * Tracked agents outside this push's scope with a numeric sub-agent pin behind the head version
232
+ * this push gave the child's entry prompt: their pins did not move.
233
+ */
234
+ function parentsOutOfScope(sync) {
235
+ if (!sync.bumpPins || !sync.promptSerials.size)
236
+ return [];
237
+ const inScope = new Set(sync.entries.map((e) => e.key));
238
+ const keys = [];
239
+ for (const key of sync.ids.keys()) {
240
+ if (!key.startsWith('agents/') || inScope.has(key))
241
+ continue;
242
+ try {
243
+ const doc = trackedAgentFile(sync, key);
244
+ if (!doc)
245
+ continue;
246
+ const stale = doc.sub_agents.some((s) => {
247
+ const id = s.agent !== undefined ? sync.ids.get(`agents/${s.agent}`) : String(s.agent_id);
248
+ const serial = id === undefined || !/^\d+$/.test(s.version) ? undefined : newEntrySerial(sync, Number(id));
249
+ return serial !== undefined && serial !== s.version;
250
+ });
251
+ if (stale)
252
+ keys.push(key);
253
+ }
254
+ catch {
255
+ continue; // unreadable files: nothing to hint
256
+ }
257
+ }
258
+ return keys.sort();
259
+ }
260
+ /**
261
+ * Every local agent in scope, written or not, must still resolve once this push is done: no
262
+ * reference (directory or numeric id) to something it deletes or that was deleted in the console,
263
+ * none to a directory it renames away, and every directory tracked or created by this push.
264
+ * Checked before any write.
265
+ */
266
+ function checkAgentRefs(agents, created, deleted, renamed, remoteDeleted, state) {
267
+ const gone = [
268
+ ...deleted.map((d) => ({ entry: d, why: `${d.key} is deleted by this push` })),
269
+ ...remoteDeleted.map((d) => ({ entry: d, why: `${d.key} was deleted in the console` })),
270
+ ];
271
+ const goneWhy = new Map([
272
+ ...gone.map((g) => [g.entry.key, g.why]),
273
+ ...renamed.map((r) => [r.from, `${r.from} is renamed to ${r.key} by this push`]),
274
+ ]);
275
+ for (const e of agents) {
276
+ let doc;
277
+ try {
278
+ doc = parseAgentEntry(e);
279
+ }
280
+ catch {
281
+ continue; // the push reports the parse error
282
+ }
283
+ const refs = agentDirRefs(doc);
284
+ const goneRef = refs.find((r) => goneWhy.has(r.key));
285
+ if (goneRef)
286
+ throw unresolvedRef(e.key, goneRef, goneWhy.get(goneRef.key));
287
+ const missing = refs.find((r) => !state.resources[r.key] && !created.has(r.key));
288
+ if (missing)
289
+ throw unresolvedRef(e.key, missing);
290
+ const idRefs = [
291
+ ...(doc.entry_prompt_id !== undefined ? [{ field: 'entry_prompt_id', kind: 'prompt', id: doc.entry_prompt_id }] : []),
292
+ ...doc.sub_agents.flatMap((s, i) => (s.agent_id !== undefined ? [{ field: `sub_agents[${i}].agent_id`, kind: 'agent', id: s.agent_id }] : [])),
293
+ ];
294
+ for (const ref of idRefs) {
295
+ const g = gone.find((x) => x.entry.kind === ref.kind && x.entry.state?.id === String(ref.id));
296
+ if (g)
297
+ throw new errors_1.CliError(`${e.key}/agent.yaml: ${ref.field} ${ref.id} does not resolve (${g.why})`);
298
+ }
299
+ }
300
+ }
301
+ /**
302
+ * The sub-agent graph over `nodes` as strongly connected components (Tarjan), each emitted after
303
+ * every component it calls: children first.
304
+ */
305
+ function agentComponents(nodes, entries) {
306
+ const keys = new Set(nodes.map((e) => e.key));
307
+ const edges = new Map(nodes.map((e) => [e.key, subAgentKeys(e, entries).filter((k) => keys.has(k))]));
308
+ const index = new Map();
309
+ const low = new Map();
310
+ const stack = [];
311
+ const onStack = new Set();
312
+ const components = [];
313
+ const connect = (v) => {
314
+ index.set(v, index.size);
315
+ low.set(v, index.get(v));
316
+ stack.push(v);
317
+ onStack.add(v);
318
+ for (const w of edges.get(v)) {
319
+ if (!index.has(w)) {
320
+ connect(w);
321
+ low.set(v, Math.min(low.get(v), low.get(w)));
322
+ }
323
+ else if (onStack.has(w))
324
+ low.set(v, Math.min(low.get(v), index.get(w)));
325
+ }
326
+ if (low.get(v) !== index.get(v))
327
+ return;
328
+ const members = [];
329
+ let w;
330
+ do {
331
+ w = stack.pop();
332
+ onStack.delete(w);
333
+ members.push(w);
334
+ } while (w !== v);
335
+ components.push({ keys: members.sort(), cyclic: members.length > 1 || edges.get(v).includes(v) });
336
+ };
337
+ for (const e of nodes)
338
+ if (!index.has(e.key))
339
+ connect(e.key);
340
+ return { components, edges };
341
+ }
342
+ /** A shortest cycle from `start` back to itself inside one component, for the error message. */
343
+ function cycleFrom(start, members, edges) {
344
+ const prev = new Map();
345
+ const queue = [start];
346
+ while (queue.length) {
347
+ const v = queue.shift();
348
+ for (const w of edges.get(v)) {
349
+ if (w === start) {
350
+ const path = [v];
351
+ for (let x = v; x !== start; x = prev.get(x))
352
+ path.unshift(prev.get(x));
353
+ return [...path, start];
354
+ }
355
+ if (members.has(w) && !prev.has(w)) {
356
+ prev.set(w, v);
357
+ queue.push(w);
358
+ }
359
+ }
360
+ }
361
+ return [start];
362
+ }
363
+ /**
364
+ * Agents being written, children first. A cycle (a component of several agents, or a self-link)
365
+ * holding an agent being written fails before any write; one only among agents this push leaves
366
+ * alone does not (the backend rejects only cycles through the agent being edited).
367
+ */
368
+ function agentWriteOrder(entries, written) {
369
+ const { components, edges } = agentComponents(entries.filter((e) => e.kind === 'agent' && e.local), entries);
370
+ for (const c of components) {
371
+ const start = c.keys.find((k) => written.has(k));
372
+ if (c.cyclic && start)
373
+ throw new errors_1.CliError(`sub-agent cycle in local agent files: ${cycleFrom(start, new Set(c.keys), edges).join(' -> ')}`);
374
+ }
375
+ return components.flatMap((c) => c.keys);
376
+ }
377
+ /** Agents being deleted, parents before children (so a parent's link never blocks its child's delete). */
378
+ function agentDeleteOrder(deleted, entries) {
379
+ return agentComponents(deleted, entries).components.flatMap((c) => c.keys).reverse();
380
+ }
package/dist/sync/push.js CHANGED
@@ -1,5 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.message = void 0;
4
+ exports.call = call;
5
+ exports.undoCreate = undoCreate;
3
6
  exports.versionContent = versionContent;
4
7
  exports.hasPendingVersion = hasPendingVersion;
5
8
  exports.pushEntry = pushEntry;
@@ -7,6 +10,7 @@ exports.deleteEntry = deleteEntry;
7
10
  const crypto_1 = require("crypto");
8
11
  const errors_1 = require("../errors");
9
12
  const remote_1 = require("./remote");
13
+ const push_agents_1 = require("./push-agents");
10
14
  const render_1 = require("./render");
11
15
  const defined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
12
16
  async function call(ctx, tool, args) {
@@ -14,14 +18,15 @@ async function call(ctx, tool, args) {
14
18
  }
15
19
  const sha = (text) => (0, crypto_1.createHash)('sha256').update(text).digest('hex');
16
20
  const message = (err) => (err instanceof Error ? err.message : String(err));
21
+ exports.message = message;
17
22
  /** Best-effort removal of a resource this push just created, so a failed push leaves no orphan. */
18
23
  async function undoCreate(ctx, tool, args, what, error) {
19
24
  try {
20
25
  await call(ctx, tool, args);
21
- return new errors_1.CliError(`${message(error)} (the half-created ${what.split(' ')[0]} was removed)`);
26
+ return new errors_1.CliError(`${(0, exports.message)(error)} (the half-created ${what.split(' ')[0]} was removed)`);
22
27
  }
23
28
  catch (cleanup) {
24
- return new errors_1.CliError(`${message(error)} (cleanup failed: ${message(cleanup)}; ${what} exists in the console)`);
29
+ return new errors_1.CliError(`${(0, exports.message)(error)} (cleanup failed: ${(0, exports.message)(cleanup)}; ${what} exists in the console)`);
25
30
  }
26
31
  }
27
32
  /*
@@ -164,6 +169,7 @@ async function pushPrompt(ctx, e) {
164
169
  });
165
170
  result.pushed_version_id = Number(v.id);
166
171
  result.pushed_serial = Number(v.serial);
172
+ result.pushed_head = head;
167
173
  result.pushed_content = content;
168
174
  result.versioned = true;
169
175
  result.summary = `version ${v.serial}${head ? ' (head)' : ''}`;
@@ -199,7 +205,7 @@ async function pushPrompt(ctx, e) {
199
205
  }
200
206
  catch (err) {
201
207
  failedIds.push(m.instance_id);
202
- failures.push(`instance ${m.instance_id}: ${message(err)}`);
208
+ failures.push(`instance ${m.instance_id}: ${(0, exports.message)(err)}`);
203
209
  }
204
210
  }
205
211
  if (failedIds.length) {
@@ -316,6 +322,8 @@ function pushEntry(ctx, e) {
316
322
  return pushPrompt(ctx, e);
317
323
  if (e.kind === 'skill')
318
324
  return pushSkill(ctx, e);
325
+ if (e.kind === 'agent')
326
+ return (0, push_agents_1.pushAgent)(ctx, e);
319
327
  return pushWorkflow(ctx, e);
320
328
  }
321
329
  async function deleteEntry(ctx, e) {
@@ -324,6 +332,10 @@ async function deleteEntry(ctx, e) {
324
332
  await call(ctx, 'delete_prompt', { prompt_external_id: Number(id) });
325
333
  else if (e.kind === 'skill')
326
334
  await call(ctx, 'delete_skill', { skill_id: Number(id) });
327
- else
335
+ else if (e.kind === 'workflow')
328
336
  await call(ctx, 'delete_workflow', { workflow_id: id });
337
+ else if (e.kind === 'agent')
338
+ await call(ctx, 'delete_agent', { agent_id: Number(id) });
339
+ else
340
+ throw new errors_1.CliError(`cannot delete unknown resource kind ${String(e.kind)}`);
329
341
  }
@@ -66,14 +66,62 @@ async function fetchWorkflows(client, project) {
66
66
  return { kind: 'workflow', id: data.workflow_id, name: data.workflow_id, files: (0, render_1.renderWorkflow)(data), data };
67
67
  });
68
68
  }
69
+ /** Agents, minus those whose entry prompt is gone (deleted prompts leave entry_prompt_external_id null). */
70
+ async function fetchAgents(client, project) {
71
+ const rows = await fetchAll(client, 'list_agents', { project_slug: project });
72
+ const fetched = await (0, shared_1.mapWithConcurrency)(rows, CONCURRENCY, async (row) => {
73
+ const res = (await read(client, 'get_agent', { project_slug: project, agent_id: row.agent_id }));
74
+ const a = (res.agent ?? res);
75
+ const tools = (a.tools ?? {});
76
+ for (const field of ['agent_id', 'max_turns']) {
77
+ if (!Number.isFinite(Number(a[field])) || a[field] === null || a[field] === undefined) {
78
+ throw new errors_1.CliError(`get_agent returned no valid ${field} for agent ${String(row.agent_id)}`);
79
+ }
80
+ }
81
+ if (a.entry_prompt_external_id === null || a.entry_prompt_external_id === undefined || !Number.isFinite(Number(a.entry_prompt_external_id))) {
82
+ return { skipped: { id: String(a.agent_id), name: String(a.name) } };
83
+ }
84
+ const data = {
85
+ agent_id: String(a.agent_id),
86
+ name: String(a.name),
87
+ description: a.description ?? null,
88
+ entry_prompt_external_id: Number(a.entry_prompt_external_id),
89
+ entry_version_pin: a.entry_version_pin ?? null,
90
+ max_turns: Number(a.max_turns),
91
+ status: String(a.status),
92
+ sub_agents: (tools.sub_agents ?? []).map((s) => ({ agent_id: Number(s.agent_id), version_pin: String(s.version_pin) })),
93
+ tool_policies: (tools.tool_policies ?? []),
94
+ };
95
+ // Directory references need the workspace's keys: computeEntries re-renders with them.
96
+ const files = { 'agent.yaml': (0, render_1.renderAgent)(data, { prompts: new Map(), agents: new Map() }) };
97
+ return { resource: { kind: 'agent', id: data.agent_id, name: data.name, files, data } };
98
+ });
99
+ return {
100
+ resources: fetched.flatMap((f) => (f.resource ? [f.resource] : [])),
101
+ skipped: fetched.flatMap((f) => (f.skipped ? [f.skipped] : [])),
102
+ };
103
+ }
69
104
  /** Rendered remote resources of the given kinds, in kind order. */
70
105
  async function fetchRemote(client, project, kinds) {
71
- const out = [];
106
+ const out = { resources: [], skippedAgents: [], agentsUnavailable: false };
72
107
  if (kinds.includes('prompt'))
73
- out.push(...(await fetchPrompts(client, project)));
108
+ out.resources.push(...(await fetchPrompts(client, project)));
74
109
  if (kinds.includes('skill'))
75
- out.push(...(await fetchSkills(client, project)));
110
+ out.resources.push(...(await fetchSkills(client, project)));
76
111
  if (kinds.includes('workflow'))
77
- out.push(...(await fetchWorkflows(client, project)));
112
+ out.resources.push(...(await fetchWorkflows(client, project)));
113
+ if (kinds.includes('agent')) {
114
+ try {
115
+ const agents = await fetchAgents(client, project);
116
+ out.resources.push(...agents.resources);
117
+ out.skippedAgents = agents.skipped;
118
+ }
119
+ catch (error) {
120
+ const unknownTool = error instanceof errors_1.ConsoleApiError && error.status === 404 && error.code === 'unknown_tool';
121
+ if (!unknownTool || kinds.length === 1)
122
+ throw error;
123
+ out.agentsUnavailable = true;
124
+ }
125
+ }
78
126
  return out;
79
127
  }
@@ -8,6 +8,8 @@ exports.renderSkill = renderSkill;
8
8
  exports.parseSkill = parseSkill;
9
9
  exports.renderWorkflow = renderWorkflow;
10
10
  exports.parseWorkflow = parseWorkflow;
11
+ exports.renderAgent = renderAgent;
12
+ exports.parseAgent = parseAgent;
11
13
  const yaml_1 = require("yaml");
12
14
  const errors_1 = require("../errors");
13
15
  const VERSION_FIELDS = ['output_format', 'model_settings', 'model_fallbacks', 'output_schema'];
@@ -179,3 +181,120 @@ function parseWorkflow(files, where) {
179
181
  }
180
182
  return { enabled: meta.enabled === undefined ? true : meta.enabled, definition: definition };
181
183
  }
184
+ const dirName = (key) => key.slice(key.indexOf('/') + 1);
185
+ function renderAgent(a, refs) {
186
+ const doc = { agent_id: Number(a.agent_id), name: a.name, description: a.description ?? null };
187
+ const promptKey = refs.prompts.get(String(a.entry_prompt_external_id));
188
+ if (promptKey)
189
+ doc.entry_prompt = dirName(promptKey);
190
+ else
191
+ doc.entry_prompt_id = a.entry_prompt_external_id;
192
+ doc.entry_version = a.entry_version_pin ?? null;
193
+ doc.max_turns = a.max_turns;
194
+ doc.status = a.status;
195
+ const subs = a.sub_agents.map((s) => {
196
+ const key = refs.agents.get(String(s.agent_id));
197
+ return { dir: key ? dirName(key) : undefined, entry: key ? { agent: dirName(key), version: s.version_pin } : { agent_id: s.agent_id, version: s.version_pin }, id: s.agent_id };
198
+ });
199
+ subs.sort((x, y) => x.dir !== undefined && y.dir !== undefined ? (x.dir < y.dir ? -1 : x.dir > y.dir ? 1 : 0)
200
+ : x.dir !== undefined ? -1 : y.dir !== undefined ? 1 : x.id - y.id);
201
+ doc.sub_agents = subs.map((s) => s.entry);
202
+ doc.tool_policies = [...a.tool_policies]
203
+ .sort((x, y) => (x.tool_name < y.tool_name ? -1 : x.tool_name > y.tool_name ? 1 : 0))
204
+ .map((p) => ({
205
+ tool_name: p.tool_name,
206
+ needs_approval: p.needs_approval,
207
+ ...(typeof p.timeout_ms === 'number' ? { timeout_ms: p.timeout_ms } : {}),
208
+ }));
209
+ return (0, yaml_1.stringify)(doc);
210
+ }
211
+ const AGENT_KEYS = ['agent_id', 'name', 'description', 'entry_prompt', 'entry_prompt_id', 'entry_version', 'max_turns', 'status', 'sub_agents', 'tool_policies'];
212
+ function checkKeys(obj, allowed, where) {
213
+ const unknown = Object.keys(obj).filter((k) => !allowed.includes(k));
214
+ if (unknown.length)
215
+ throw new errors_1.CliError(`${where}: unknown key${unknown.length > 1 ? 's' : ''} ${unknown.map((k) => `"${k}"`).join(', ')}`);
216
+ }
217
+ function mapping(value, where) {
218
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
219
+ throw new errors_1.CliError(`${where} must be a mapping`);
220
+ return value;
221
+ }
222
+ const isId = (v) => typeof v === 'number' && Number.isInteger(v) && v > 0;
223
+ const isText = (v) => typeof v === 'string' && v.trim() !== '';
224
+ /** A pin is a serial or tag; YAML may read an unquoted 3 as a number. */
225
+ function pin(value) {
226
+ if (typeof value === 'number' && Number.isInteger(value))
227
+ return String(value);
228
+ return isText(value) ? value : undefined;
229
+ }
230
+ function parseAgent(text, file) {
231
+ const raw = yamlObject(text, file);
232
+ checkKeys(raw, AGENT_KEYS, file);
233
+ if (!isText(raw.name))
234
+ throw new errors_1.CliError(`${file}: "name" is required`);
235
+ if (raw.agent_id !== undefined && !isId(raw.agent_id))
236
+ throw new errors_1.CliError(`${file}: "agent_id" must be a positive integer`);
237
+ if (raw.description !== undefined && raw.description !== null && typeof raw.description !== 'string') {
238
+ throw new errors_1.CliError(`${file}: "description" must be a string or null`);
239
+ }
240
+ if ((raw.entry_prompt === undefined) === (raw.entry_prompt_id === undefined)) {
241
+ throw new errors_1.CliError(`${file}: set exactly one of "entry_prompt" (directory) or "entry_prompt_id"`);
242
+ }
243
+ if (raw.entry_prompt !== undefined && !isText(raw.entry_prompt))
244
+ throw new errors_1.CliError(`${file}: "entry_prompt" must be a directory name`);
245
+ if (raw.entry_prompt_id !== undefined && !isId(raw.entry_prompt_id))
246
+ throw new errors_1.CliError(`${file}: "entry_prompt_id" must be a positive integer`);
247
+ let entryVersion = null;
248
+ if (raw.entry_version !== undefined && raw.entry_version !== null) {
249
+ const v = pin(raw.entry_version);
250
+ if (v === undefined)
251
+ throw new errors_1.CliError(`${file}: "entry_version" must be a serial, a tag, or null`);
252
+ entryVersion = v;
253
+ }
254
+ if (raw.max_turns !== undefined && !isId(raw.max_turns))
255
+ throw new errors_1.CliError(`${file}: "max_turns" must be a positive integer`);
256
+ if (raw.status !== undefined && !isText(raw.status))
257
+ throw new errors_1.CliError(`${file}: "status" must be a string`);
258
+ for (const field of ['sub_agents', 'tool_policies']) {
259
+ if (raw[field] !== undefined && !Array.isArray(raw[field]))
260
+ throw new errors_1.CliError(`${file}: "${field}" must be a list`);
261
+ }
262
+ const sub_agents = (raw.sub_agents ?? []).map((item, i) => {
263
+ const where = `${file}: sub_agents[${i}]`;
264
+ const s = mapping(item, where);
265
+ checkKeys(s, ['agent', 'agent_id', 'version'], where);
266
+ if ((s.agent === undefined) === (s.agent_id === undefined))
267
+ throw new errors_1.CliError(`${where} needs exactly one of "agent" (directory) or "agent_id"`);
268
+ if (s.agent !== undefined && !isText(s.agent))
269
+ throw new errors_1.CliError(`${where}: "agent" must be a directory name`);
270
+ if (s.agent_id !== undefined && !isId(s.agent_id))
271
+ throw new errors_1.CliError(`${where}: "agent_id" must be a positive integer`);
272
+ const version = pin(s.version);
273
+ if (version === undefined)
274
+ throw new errors_1.CliError(`${where}: "version" is required (a serial or tag)`);
275
+ return { ...(s.agent !== undefined ? { agent: s.agent } : { agent_id: s.agent_id }), version };
276
+ });
277
+ const tool_policies = (raw.tool_policies ?? []).map((item, i) => {
278
+ const where = `${file}: tool_policies[${i}]`;
279
+ const p = mapping(item, where);
280
+ checkKeys(p, ['tool_name', 'needs_approval', 'timeout_ms'], where);
281
+ if (!isText(p.tool_name))
282
+ throw new errors_1.CliError(`${where}: "tool_name" is required`);
283
+ if (typeof p.needs_approval !== 'boolean')
284
+ throw new errors_1.CliError(`${where}: "needs_approval" must be true or false`);
285
+ if (p.timeout_ms !== undefined && p.timeout_ms !== null && !isId(p.timeout_ms))
286
+ throw new errors_1.CliError(`${where}: "timeout_ms" must be a positive integer`);
287
+ return { tool_name: p.tool_name, needs_approval: p.needs_approval, ...(typeof p.timeout_ms === 'number' ? { timeout_ms: p.timeout_ms } : {}) };
288
+ });
289
+ return {
290
+ ...(raw.agent_id !== undefined ? { agent_id: raw.agent_id } : {}),
291
+ name: raw.name,
292
+ description: raw.description ?? null,
293
+ ...(raw.entry_prompt !== undefined ? { entry_prompt: raw.entry_prompt } : { entry_prompt_id: raw.entry_prompt_id }),
294
+ entry_version: entryVersion,
295
+ ...(raw.max_turns !== undefined ? { max_turns: raw.max_turns } : {}),
296
+ ...(raw.status !== undefined ? { status: raw.status } : {}),
297
+ sub_agents,
298
+ tool_policies,
299
+ };
300
+ }