● Zion Boggan
repos/Zion Pm/lib/mastra.ts
zionboggan.com ↗
119 lines · typescript
History for this file →
1
import { Agent } from '@mastra/core/agent';
2
import { createTool } from '@mastra/core/tools';
3
import { z } from 'zod';
4
import { glmModel, PM_BRAIN_MODEL } from './glm';
5
import { queryMemory, queryGraph } from './memory';
6
import { delegate } from './workers/delegate';
7
import { createTask, updateTask, draftOutbox } from './db';
8
 
9
const delegateTool = createTool({
10
  id: 'delegate',
11
  description: 'Delegate a self-contained task to a worker model (claude=strong/CT215, codex=strong/CT241, ollama=local/free). Use for heavy or parallel work. Returns the worker output.',
12
  inputSchema: z.object({
13
    model: z.enum(['claude', 'codex', 'ollama']).describe('worker model'),
14
    task: z.string().describe('self-contained work envelope'),
15
    ollamaModel: z.string().optional().describe('required iff model=ollama, e.g. mistral-nemo:12b'),
16
    context: z.string().optional().describe('RAG/graph snippets to inject'),
17
  }),
18
  execute: async ({ context }) => {
19
    const c = context as unknown as { model: 'claude' | 'codex' | 'ollama'; task: string; ollamaModel?: string; context?: string };
20
    const r = await delegate({ model: c.model, task: c.task, ollamaModel: c.ollamaModel, context: c.context });
21
    return { ok: r.ok, model: r.model, output: r.output, error: r.error };
22
  },
23
});
24
 
25
const createTaskTool = createTool({
26
  id: 'createTask',
27
  description: 'Create a Board task (kanban card).',
28
  inputSchema: z.object({
29
    title: z.string(),
30
    status: z.enum(['todo', 'doing', 'done', 'blocked']).optional(),
31
    assignedModel: z.string().optional(),
32
    channelId: z.string().optional(),
33
  }),
34
  execute: async ({ context }) => {
35
    const c = context as unknown as { title: string; status?: string; assignedModel?: string; channelId?: string };
36
    const t = createTask(c.title, { status: c.status, assignedModel: c.assignedModel ?? null, channelId: c.channelId ?? null });
37
    return { id: t.id, title: t.title, status: t.status };
38
  },
39
});
40
 
41
const updateTaskTool = createTool({
42
  id: 'updateTask',
43
  description: 'Update a Board task status/assignment.',
44
  inputSchema: z.object({
45
    id: z.string(),
46
    status: z.enum(['todo', 'doing', 'done', 'blocked']).optional(),
47
    assignedModel: z.string().optional(),
48
    title: z.string().optional(),
49
  }),
50
  execute: async ({ context }) => {
51
    const c = context as unknown as { id: string; status?: string; assignedModel?: string; title?: string };
52
    const t = updateTask(c.id, { status: c.status, assignedModel: c.assignedModel, title: c.title });
53
    return t ? { id: t.id, status: t.status } : { error: 'task not found' };
54
  },
55
});
56
 
57
const draftApprovalTool = createTool({
58
  id: 'draftApproval',
59
  description: 'Draft an OUTWARD action (email|deploy|publish|infra) into the approval outbox. NEVER auto-sends - Zion approves in the UI. Use for anything leaving the homelab.',
60
  inputSchema: z.object({
61
    actionType: z.enum(['email', 'deploy', 'publish', 'infra']),
62
    payload: z.record(z.string(), z.any()).describe('action details'),
63
  }),
64
  execute: async ({ context }) => {
65
    const c = context as unknown as { actionType: string; payload: Record<string, unknown> };
66
    const o = draftOutbox(c.actionType, c.payload);
67
    return { id: o.id, status: o.status, note: 'drafted, awaiting human approval' };
68
  },
69
});
70
 
71
const queryMemoryTool = createTool({
72
  id: 'queryMemory',
73
  description: 'RAG-first: query the homelab memory (docs, configs, infra, project history) BEFORE reading files.',
74
  inputSchema: z.object({ query: z.string() }),
75
  execute: async ({ context }) => ({ context: await queryMemory((context as unknown as { query: string })?.query ?? '') }),
76
});
77
 
78
const queryGraphTool = createTool({
79
  id: 'queryGraph',
80
  description: 'Query the code-architecture graph (cbm-homelab): structure, call graph, blast radius.',
81
  inputSchema: z.object({ query: z.string() }),
82
  execute: async ({ context }) => ({ graph: await queryGraph((context as unknown as { query: string })?.query ?? '') }),
83
});
84
 
85
const INSTRUCTIONS = `You are the Zion PM conductor - the project-manager brain for Zion's homelab, running on GLM (${PM_BRAIN_MODEL}).
86
 
87
Your job: take any task, decompose it, delegate work to the worker fleet, supervise, and gate every OUTWARD action behind the approval outbox.
88
 
89
Operating rules (non-negotiable):
90
- GROUNDING: base every homelab/architecture fact STRICTLY on the "Homelab memory" and "Code graph" context provided in the message. NEVER invent services, tech, or infra. If the context does not cover something, say "not in memory" rather than guessing.
91
- Delegate heavy/parallel work via the delegate tool: claude (strong, CT215), codex (strong, CT241), ollama (local, free). You are the brain - you do NOT do worker execution yourself.
92
- Every OUTWARD action (email, deploy, publish, infra change) MUST go through draftApproval - it drafts only, never sends. Internal delegation and task edits are full-auto.
93
- OUTPUT: answer directly in the exact format requested (e.g. exactly N bullets). No preamble, no narration of your reasoning or tools - only the final answer.
94
- Track real work as Board tasks via createTask/updateTask when it helps.`;
95
 
96
void queryMemoryTool;
97
void queryGraphTool;
98
 
99
export const pmConductor = new Agent({
100
  name: 'pmConductor',
101
  instructions: INSTRUCTIONS,
102
  model: glmModel,
103
  tools: { delegate: delegateTool, createTask: createTaskTool, updateTask: updateTaskTool, draftApproval: draftApprovalTool },
104
});
105
 
106
 
107
export async function runConductor(
108
  userText: string,
109
  opts: { memoryContext?: string; graphContext?: string } = {}
110
): Promise<string> {
111
  const parts: string[] = [];
112
  if (opts.memoryContext) parts.push(`# Homelab memory (queryMemory)\n${opts.memoryContext}`);
113
  if (opts.graphContext) parts.push(`# Code graph (queryGraph)\n${opts.graphContext}`);
114
  parts.push(`# Task from Zion\n${userText}`);
115
  parts.push('Answer using ONLY the context above. Output only the final answer in the exact format requested - no preamble, no tool/reasoning narration.');
116
  const prompt = parts.join('\n\n');
117
  const res = await pmConductor.generate(prompt, { maxSteps: 3 });
118
  return ((res as { text?: string }).text ?? '').trim();
119
}