| 1 | /** |
| 2 | * Agent templates (routes/workspace/agents/templates.tsx): the starting |
| 3 | * points g1t provides for an agent, each a role with responsibilities, |
| 4 | * helpers, a voice, model limits and instructions. A template isn't |
| 5 | * installed: an owner starts a new agent from one and configures it, and |
| 6 | * the agent is the workspace's own from then on. |
| 7 | * |
| 8 | * No Workers or React imports, so it can be tested under Node. |
| 9 | */ |
| 10 | import type { AgentTemplate, WorkspaceAgent } from "@g1t/contracts"; |
| 11 | |
| 12 | /** Every template, under Agents. */ |
| 13 | export function templatesPath(slug: string): string { |
| 14 | return `/${slug}/-/agents/templates`; |
| 15 | } |
| 16 | |
| 17 | /** One template's page. */ |
| 18 | export function templatePath(slug: string, template: string): string { |
| 19 | return `/${slug}/-/agents/templates/${template}`; |
| 20 | } |
| 21 | |
| 22 | /** Where an owner starts an agent from a template: the new-agent form, with the template chosen. */ |
| 23 | export function startPath(slug: string, template: string): string { |
| 24 | return `/${slug}/-/agents/new?template=${encodeURIComponent(template)}`; |
| 25 | } |
| 26 | |
| 27 | /** An agent as a template's page needs it: who it is and which template it started from. */ |
| 28 | export type StartedAgent = Pick<WorkspaceAgent, "id" | "handle" | "display_name" | "template" | "archived_at">; |
| 29 | |
| 30 | /** A template, with the workspace's agents that started from it. */ |
| 31 | export type TemplateListing = { |
| 32 | template: AgentTemplate; |
| 33 | /** Agents started from it, not archived. */ |
| 34 | agents: Pick<WorkspaceAgent, "id" | "handle" | "display_name">[]; |
| 35 | }; |
| 36 | |
| 37 | /** |
| 38 | * Every template, in the order the agents service gives them, with the |
| 39 | * agents started from each. `agents` null: the agents service could not |
| 40 | * say, so none is counted. |
| 41 | */ |
| 42 | export function templateListings(templates: AgentTemplate[], agents: StartedAgent[] | null): TemplateListing[] { |
| 43 | return templates.map((template) => ({ |
| 44 | template, |
| 45 | agents: (agents ?? []) |
| 46 | .filter((agent) => agent.template === template.id && !agent.archived_at) |
| 47 | .map(({ id, handle, display_name }) => ({ id, handle, display_name })), |
| 48 | })); |
| 49 | } |
| 50 | |
| 51 | /** Listings by department, in the order the templates come, "Other" for none. */ |
| 52 | export function byDepartment(listings: TemplateListing[]): [string, TemplateListing[]][] { |
| 53 | const departments = new Map<string, TemplateListing[]>(); |
| 54 | for (const listing of listings) { |
| 55 | const key = listing.template.department || "Other"; |
| 56 | departments.set(key, [...(departments.get(key) ?? []), listing]); |
| 57 | } |
| 58 | return [...departments]; |
| 59 | } |
| 60 | |
| 61 | /** "Software Engineer · Engineering": a role as one line. */ |
| 62 | export function roleLine(template: Pick<AgentTemplate, "title" | "department">): string { |
| 63 | return template.department ? `${template.title} · ${template.department}` : template.title; |
| 64 | } |
| 65 | |
| 66 | /** How a routing limit reads: "Any model the work needs", "Large models or better", "Up to large models", "Large to frontier models". */ |
| 67 | export function routingWords(routing: Pick<AgentTemplate["routing"], "floor" | "ceiling">): string { |
| 68 | const name = (tier: string) => tier.charAt(0).toUpperCase() + tier.slice(1); |
| 69 | if (routing.floor && routing.ceiling) return `${name(routing.floor)} to ${routing.ceiling} models`; |
| 70 | if (routing.floor) return `${name(routing.floor)} models or better`; |
| 71 | if (routing.ceiling) return `Up to ${routing.ceiling} models`; |
| 72 | return "Any model the work needs"; |
| 73 | } |