bopodev-api 0.1.12 → 0.1.14

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.
@@ -10,24 +10,31 @@ import {
10
10
  createIssue,
11
11
  createProject,
12
12
  goals,
13
+ listAgents,
13
14
  listIssues,
14
15
  listProjects,
15
- projects
16
+ projects,
17
+ updatePluginConfig
16
18
  } from "bopodev-db";
17
19
  import {
18
20
  normalizeRuntimeConfig,
21
+ resolveRuntimeModelForProvider,
19
22
  requiresRuntimeCwd,
20
23
  runtimeConfigToDb,
21
24
  runtimeConfigToStateBlobPatch
22
25
  } from "../lib/agent-config";
26
+ import { resolveOpencodeRuntimeModel } from "../lib/opencode-model";
23
27
  import { hasText, resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
28
+ import { appendDurableFact } from "./memory-file-service";
24
29
 
25
30
  const approvalGatedActions = new Set([
26
31
  "hire_agent",
27
32
  "activate_goal",
28
33
  "override_budget",
29
34
  "pause_agent",
30
- "terminate_agent"
35
+ "terminate_agent",
36
+ "promote_memory_fact",
37
+ "grant_plugin_capabilities"
31
38
  ]);
32
39
 
33
40
  const hireAgentPayloadSchema = AgentCreateRequestSchema.extend({
@@ -56,6 +63,18 @@ const activateGoalPayloadSchema = z.object({
56
63
  title: z.string().min(1),
57
64
  description: z.string().optional()
58
65
  });
66
+ const promoteMemoryFactPayloadSchema = z.object({
67
+ agentId: z.string().min(1),
68
+ fact: z.string().min(1),
69
+ sourceRunId: z.string().optional()
70
+ });
71
+ const grantPluginCapabilitiesPayloadSchema = z.object({
72
+ pluginId: z.string().min(1),
73
+ enabled: z.boolean().optional(),
74
+ priority: z.number().int().min(0).max(1000).optional(),
75
+ grantedCapabilities: z.array(z.string().min(1)).default([]),
76
+ config: z.record(z.string(), z.unknown()).default({})
77
+ });
59
78
  const AGENT_STARTUP_PROJECT_NAME = "Agent Onboarding";
60
79
  const AGENT_STARTUP_TASK_MARKER = "[bopodev:onboarding:agent-startup:v1]";
61
80
 
@@ -102,7 +121,7 @@ export async function resolveApproval(
102
121
  let execution:
103
122
  | {
104
123
  applied: boolean;
105
- entityType?: "agent" | "goal";
124
+ entityType?: "agent" | "goal" | "memory";
106
125
  entityId?: string;
107
126
  entity?: Record<string, unknown>;
108
127
  }
@@ -176,12 +195,32 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
176
195
  },
177
196
  defaultRuntimeCwd
178
197
  });
198
+ runtimeConfig.runtimeModel = await resolveOpencodeRuntimeModel(parsed.data.providerType, runtimeConfig);
199
+ runtimeConfig.runtimeModel = resolveRuntimeModelForProvider(parsed.data.providerType, runtimeConfig.runtimeModel);
200
+ if (providerRequiresNamedModel(parsed.data.providerType) && !hasText(runtimeConfig.runtimeModel)) {
201
+ throw new GovernanceError("Approval payload for agent hiring must include a named runtime model.");
202
+ }
179
203
  if (requiresRuntimeCwd(parsed.data.providerType) && !hasText(runtimeConfig.runtimeCwd)) {
180
204
  throw new GovernanceError("Approval payload for agent hiring is missing runtime working directory.");
181
205
  }
182
206
  if (requiresRuntimeCwd(parsed.data.providerType) && hasText(runtimeConfig.runtimeCwd)) {
183
207
  await mkdir(runtimeConfig.runtimeCwd!, { recursive: true });
184
208
  }
209
+ const existingAgents = await listAgents(db, companyId);
210
+ const duplicate = existingAgents.find(
211
+ (agent) =>
212
+ agent.role === parsed.data.role &&
213
+ (agent.managerAgentId ?? null) === (parsed.data.managerAgentId ?? null) &&
214
+ agent.status !== "terminated"
215
+ );
216
+ if (duplicate) {
217
+ return {
218
+ applied: false,
219
+ entityType: "agent" as const,
220
+ entityId: duplicate.id,
221
+ entity: duplicate as unknown as Record<string, unknown>
222
+ };
223
+ }
185
224
 
186
225
  const agent = await createAgent(db, {
187
226
  companyId,
@@ -196,7 +235,7 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
196
235
  initialState: runtimeConfigToStateBlobPatch(runtimeConfig)
197
236
  });
198
237
  const startupProjectId = await ensureAgentStartupProject(db, companyId);
199
- await ensureAgentStartupIssue(db, companyId, startupProjectId, agent.id, agent.role, agent.name);
238
+ await ensureAgentStartupIssue(db, companyId, startupProjectId, agent.id, agent.role);
200
239
 
201
240
  return {
202
241
  applied: true,
@@ -262,9 +301,62 @@ async function applyApprovalAction(db: BopoDb, companyId: string, action: string
262
301
  return { applied: false };
263
302
  }
264
303
 
304
+ if (action === "promote_memory_fact") {
305
+ const parsed = promoteMemoryFactPayloadSchema.safeParse(payload);
306
+ if (!parsed.success) {
307
+ throw new GovernanceError("Approval payload for memory promotion is invalid.");
308
+ }
309
+ const targetFile = await appendDurableFact({
310
+ companyId,
311
+ agentId: parsed.data.agentId,
312
+ fact: parsed.data.fact,
313
+ sourceRunId: parsed.data.sourceRunId ?? null
314
+ });
315
+ return {
316
+ applied: Boolean(targetFile),
317
+ entityType: "memory" as const,
318
+ entityId: parsed.data.agentId,
319
+ entity: {
320
+ agentId: parsed.data.agentId,
321
+ sourceRunId: parsed.data.sourceRunId ?? null,
322
+ fact: parsed.data.fact,
323
+ targetFile
324
+ }
325
+ };
326
+ }
327
+ if (action === "grant_plugin_capabilities") {
328
+ const parsed = grantPluginCapabilitiesPayloadSchema.safeParse(payload);
329
+ if (!parsed.success) {
330
+ throw new GovernanceError("Approval payload for plugin capability grant is invalid.");
331
+ }
332
+ await updatePluginConfig(db, {
333
+ companyId,
334
+ pluginId: parsed.data.pluginId,
335
+ enabled: parsed.data.enabled,
336
+ priority: parsed.data.priority,
337
+ grantedCapabilitiesJson: JSON.stringify(parsed.data.grantedCapabilities),
338
+ configJson: JSON.stringify(parsed.data.config)
339
+ });
340
+ return {
341
+ applied: true,
342
+ entityType: "memory" as const,
343
+ entityId: parsed.data.pluginId,
344
+ entity: {
345
+ pluginId: parsed.data.pluginId,
346
+ enabled: parsed.data.enabled ?? null,
347
+ priority: parsed.data.priority ?? null,
348
+ grantedCapabilities: parsed.data.grantedCapabilities
349
+ }
350
+ };
351
+ }
352
+
265
353
  throw new GovernanceError(`Unsupported approval action: ${action}`);
266
354
  }
267
355
 
356
+ function providerRequiresNamedModel(providerType: string) {
357
+ return providerType !== "http" && providerType !== "shell";
358
+ }
359
+
268
360
  function parsePayload(payloadJson: string) {
269
361
  try {
270
362
  const parsed = JSON.parse(payloadJson) as unknown;
@@ -293,11 +385,10 @@ async function ensureAgentStartupIssue(
293
385
  companyId: string,
294
386
  projectId: string,
295
387
  agentId: string,
296
- role: string,
297
- name: string
388
+ role: string
298
389
  ) {
299
390
  const title = `Set up ${role} operating files`;
300
- const body = buildAgentStartupTaskBody(name, agentId);
391
+ const body = buildAgentStartupTaskBody(agentId);
301
392
  const existingIssues = await listIssues(db, companyId);
302
393
  const existing = existingIssues.find(
303
394
  (issue) =>
@@ -323,20 +414,20 @@ async function ensureAgentStartupIssue(
323
414
  return created.id;
324
415
  }
325
416
 
326
- function buildAgentStartupTaskBody(agentName: string, agentId: string) {
327
- const slug = slugifyAgentPath(agentName || agentId);
417
+ function buildAgentStartupTaskBody(agentId: string) {
418
+ const agentFolder = `agents/${agentId}`;
328
419
  return [
329
420
  AGENT_STARTUP_TASK_MARKER,
330
421
  "",
331
422
  `Create your operating baseline before starting feature delivery work.`,
332
423
  "",
333
- `1. Create the folder \`agents/${slug}/\` in the repository workspace.`,
424
+ `1. Create the folder \`${agentFolder}/\` in the repository workspace.`,
334
425
  "2. Author these files with your own responsibilities and working style:",
335
- ` - \`agents/${slug}/AGENTS.md\``,
336
- ` - \`agents/${slug}/HEARTBEAT.md\``,
337
- ` - \`agents/${slug}/SOUL.md\``,
338
- ` - \`agents/${slug}/TOOLS.md\``,
339
- `3. Update your own agent runtime config via \`PUT /agents/:agentId\` and set \`runtimeConfig.bootstrapPrompt\` to reference \`agents/${slug}/AGENTS.md\` as your primary guide.`,
426
+ ` - \`${agentFolder}/AGENTS.md\``,
427
+ ` - \`${agentFolder}/HEARTBEAT.md\``,
428
+ ` - \`${agentFolder}/SOUL.md\``,
429
+ ` - \`${agentFolder}/TOOLS.md\``,
430
+ `3. Update your own agent runtime config via \`PUT /agents/:agentId\` and set \`runtimeConfig.bootstrapPrompt\` to reference \`${agentFolder}/AGENTS.md\` as your primary guide.`,
340
431
  "4. Post an issue comment summarizing completed setup artifacts.",
341
432
  "",
342
433
  "Safety checks:",
@@ -345,11 +436,3 @@ function buildAgentStartupTaskBody(agentName: string, agentId: string) {
345
436
  ].join("\n");
346
437
  }
347
438
 
348
- function slugifyAgentPath(value: string) {
349
- const base = value
350
- .toLowerCase()
351
- .replace(/[^a-z0-9]+/g, "-")
352
- .replace(/^-+|-+$/g, "");
353
- return base || "agent";
354
- }
355
-