heyio 0.1.30 → 0.1.32

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/README.md CHANGED
@@ -12,10 +12,10 @@ A personal AI assistant daemon built on the GitHub Copilot SDK. IO runs 24/7 on
12
12
  - **Multi-Interface** — Web UI + Telegram bot + terminal TUI + HTTP API
13
13
  - **Web Frontend** — Vue 3 dashboard with chat, squad management, skills, and agent activity views
14
14
  - **Persistent Memory** — wiki-based knowledge base stored at `~/.io/wiki/`
15
- - **Squad System** — persistent project teams that remember decisions, context, and history
15
+ - **Squad System** — persistent project teams with **named specialist agents** themed from 80s pop culture (A-Team, Transformers, ThunderCats, GI Joe, Aliens, Ghostbusters)
16
16
  - **Skills** — modular skill system; install from git repos or the [skills.sh](https://skills.sh) registry
17
17
  - **Adaptive Sessions** — infinite sessions with automatic context compaction
18
- - **Worker Agents** — delegated task execution through specialized agent sessions
18
+ - **Named Agent Personas** — each squad agent gets a character persona with personality, dynamic role title, and specialized charter
19
19
  - **GitHub Integration** — create, list, view, and comment on issues and PRs via the `github` tool
20
20
  - **Smart Model Routing** — automatically selects the best model for each task based on complexity
21
21
  - **Self-Updating** — checks for updates and can apply them automatically
@@ -179,14 +179,21 @@ A skill is a directory with a `SKILL.md` file that describes the skill and its t
179
179
 
180
180
  ## 👥 Squad System
181
181
 
182
- Squads are persistent project teams that IO manages. Each squad:
182
+ Squads are persistent project teams with **named specialist agents**. Each squad:
183
183
 
184
- - Is associated with a specific project or domain
185
- - Remembers decisions, context, and conversation history
186
- - Can have multiple specialized agents working together
184
+ - Has an 80s pop culture **universe theme** (A-Team, Transformers, ThunderCats, GI Joe, Aliens, Ghostbusters)
185
+ - Contains dynamically-created **specialist agents** with roles tailored to the project (e.g., "Express API Engineer", "Vue.js Frontend Dev")
186
+ - Each agent is assigned a **character persona** with personality traits that color their work style
187
+ - Remembers decisions, context, and conversation history across sessions
187
188
  - Persists across sessions in the SQLite database
188
189
 
189
- IO's orchestrator automatically creates and manages squads based on your conversations.
190
+ ### How Squads Work
191
+
192
+ 1. **Create** — `squad_create` assigns a random 80s universe (or user picks one)
193
+ 2. **Analyze** — `squad_analyze` scans the project to determine languages, frameworks, and tools
194
+ 3. **Build the team** — `squad_add_agent` for each specialist the project needs; characters are drawn from the universe pool
195
+ 4. **Delegate** — `squad_delegate` sends tasks to specific agents by character name
196
+ 5. **Track** — `squad_task_status` monitors progress and retrieves results
190
197
 
191
198
  ## 🏗️ Architecture
192
199
 
@@ -197,12 +204,12 @@ User → [Web UI / TUI / Telegram / HTTP API]
197
204
  ↕ ↕
198
205
  Squad Manager Wiki/Memory
199
206
 
200
- Worker Agents
207
+ Named Agents (80s Characters)
201
208
  ```
202
209
 
203
210
  IO is built around the **Copilot SDK** which handles all LLM interactions, including tool calling and context management. The **Orchestrator** manages the primary conversation session with automatic context compaction for infinite-length sessions.
204
211
 
205
- For complex tasks, the orchestrator delegates work to **Worker Agents** — short-lived agent sessions that execute specific tasks and report back.
212
+ For complex tasks, the orchestrator delegates work to **Named Agents** — persistent agent sessions with character personas, specialized roles, and per-agent system prompts. Each agent works autonomously within their squad's project context.
206
213
 
207
214
  The **Squad System** provides persistent project context, while the **Wiki** serves as a long-term knowledge base that spans all conversations.
208
215
 
@@ -252,14 +259,15 @@ src/
252
259
  ├── copilot/
253
260
  │ ├── client.ts # CopilotClient singleton
254
261
  │ ├── orchestrator.ts # Main session management
255
- │ ├── agents.ts # Worker agent sessions
262
+ │ ├── agents.ts # Named agent sessions & personas
263
+ │ ├── universes.ts # 80s universe character data
256
264
  │ ├── tools.ts # Tool definitions
257
265
  │ ├── model-router.ts # Complexity-based model selection
258
266
  │ ├── skills.ts # Skills loader
259
267
  │ └── system-message.ts # System prompt builder
260
268
  ├── store/
261
269
  │ ├── db.ts # SQLite database
262
- │ ├── squads.ts # Squad CRUD
270
+ │ ├── squads.ts # Squad & agent CRUD
263
271
  │ └── tasks.ts # Agent task tracking
264
272
  ├── wiki/
265
273
  │ ├── fs.ts # Wiki filesystem
@@ -4,7 +4,7 @@ import { existsSync } from "node:fs";
4
4
  import express from "express";
5
5
  import { config } from "../config.js";
6
6
  import { listSkills } from "../copilot/skills.js";
7
- import { listSquads, createSquad } from "../store/squads.js";
7
+ import { listSquads, createSquad, listSquadAgents } from "../store/squads.js";
8
8
  import { getAgentInfo } from "../copilot/agents.js";
9
9
  import { IO_VERSION } from "../paths.js";
10
10
  import { requireAuth } from "./auth.js";
@@ -86,6 +86,17 @@ export async function startApiServer() {
86
86
  res.status(500).json({ error: "Failed to create squad" });
87
87
  }
88
88
  });
89
+ api.get("/squads/:slug/agents", (req, res) => {
90
+ try {
91
+ const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
92
+ const agents = listSquadAgents(slug);
93
+ res.json({ agents });
94
+ }
95
+ catch (e) {
96
+ console.error("Error listing squad agents:", e);
97
+ res.status(500).json({ error: "Failed to list squad agents" });
98
+ }
99
+ });
89
100
  // Agents endpoints
90
101
  api.get("/agents", (_req, res) => {
91
102
  try {
@@ -132,17 +143,20 @@ export async function startApiServer() {
132
143
  sseConnections.delete(res);
133
144
  });
134
145
  });
135
- // Mount API at /api (for frontend) and / (backward compat)
146
+ // Mount API at /api (for frontend)
136
147
  app.use("/api", api);
137
- app.use("/", api);
138
- // Serve Vue frontend if built assets exist
148
+ // Serve Vue frontend if built assets exist (before backward-compat API mount)
139
149
  if (existsSync(WEB_DIST)) {
140
150
  app.use(express.static(WEB_DIST));
141
- // SPA fallback — serve index.html for any non-API route
151
+ console.log("[io] Web frontend enabled");
152
+ }
153
+ // Backward-compat: mount API at / (after static files so HTML/CSS/JS are served first)
154
+ app.use("/", api);
155
+ // SPA fallback — serve index.html for any unmatched route
156
+ if (existsSync(WEB_DIST)) {
142
157
  app.get("/{*splat}", (_req, res) => {
143
158
  res.sendFile(path.join(WEB_DIST, "index.html"));
144
159
  });
145
- console.log("[io] Web frontend enabled");
146
160
  }
147
161
  return new Promise((resolve) => {
148
162
  app.listen(config.port, () => {
@@ -6,11 +6,16 @@ import { homedir } from "os";
6
6
  import { defineTool, approveAll } from "@github/copilot-sdk";
7
7
  import { z } from "zod";
8
8
  import { getClient } from "./client.js";
9
- import { getModelForTask } from "./model-router.js";
10
- import { getSquad, updateSquadSession, updateSquadStatus, getDecisionsSummary, logDecision, } from "../store/squads.js";
9
+ import { getModelForTask, getModelForTier } from "./model-router.js";
10
+ import { getSquad, updateSquadSession, updateSquadStatus, getDecisionsSummary, logDecision, listSquadAgents, getSquadAgent, updateAgentSession, updateAgentStatus, } from "../store/squads.js";
11
11
  import { createTask, completeTask, failTask, getActiveTasks, } from "../store/tasks.js";
12
12
  import { SESSIONS_DIR } from "../paths.js";
13
+ import { getUniverse } from "./universes.js";
14
+ // Key format: "squadSlug:characterName" for per-agent sessions, "squadSlug" for legacy
13
15
  const agentSessions = new Map();
16
+ function agentSessionKey(squadSlug, characterName) {
17
+ return characterName ? `${squadSlug}:${characterName}` : squadSlug;
18
+ }
14
19
  export function getAgentInfo() {
15
20
  const activeTasks = getActiveTasks();
16
21
  const tasksByAgent = new Map();
@@ -18,34 +23,71 @@ export function getAgentInfo() {
18
23
  tasksByAgent.set(task.agent_slug, task.description);
19
24
  }
20
25
  const agents = [];
21
- for (const [slug, _session] of agentSessions) {
22
- const squad = getSquad(slug);
23
- const currentTask = tasksByAgent.get(slug);
24
- let status = "idle";
25
- if (currentTask) {
26
- status = "working";
26
+ const seenSquads = new Set();
27
+ // Collect info from squad agents (named agents)
28
+ for (const [key, _session] of agentSessions) {
29
+ const parts = key.split(":");
30
+ const squadSlug = parts[0];
31
+ const characterName = parts[1];
32
+ seenSquads.add(squadSlug);
33
+ const squad = getSquad(squadSlug);
34
+ if (characterName) {
35
+ const agent = getSquadAgent(squadSlug, characterName);
36
+ const currentTask = tasksByAgent.get(key) ?? tasksByAgent.get(squadSlug);
37
+ agents.push({
38
+ slug: squadSlug,
39
+ name: agent ? `${agent.character_name} (${agent.role_title})` : characterName,
40
+ characterName,
41
+ roleTitle: agent?.role_title,
42
+ universe: squad?.universe ?? undefined,
43
+ status: agent?.status === "working" ? "working" : currentTask ? "working" : "idle",
44
+ currentTask,
45
+ });
27
46
  }
28
- if (squad?.status === "error") {
29
- status = "error";
47
+ else {
48
+ // Legacy generic agent
49
+ const currentTask = tasksByAgent.get(squadSlug);
50
+ agents.push({
51
+ slug: squadSlug,
52
+ name: squad?.name ?? squadSlug,
53
+ status: currentTask ? "working" : squad?.status === "error" ? "error" : "idle",
54
+ currentTask,
55
+ });
30
56
  }
31
- agents.push({
32
- slug,
33
- name: squad?.name ?? slug,
34
- status,
35
- currentTask,
36
- });
37
57
  }
38
58
  return agents;
39
59
  }
40
- export async function delegateToAgent(squadSlug, task, onComplete) {
60
+ export async function delegateToAgent(squadSlug, task, onComplete, targetAgent) {
41
61
  const squad = getSquad(squadSlug);
42
62
  if (!squad) {
43
63
  throw new Error(`Squad not found: ${squadSlug}`);
44
64
  }
45
- const session = await getOrCreateSession(squadSlug, task);
65
+ // Determine which agent session to use
66
+ let agent;
67
+ if (targetAgent) {
68
+ agent = getSquadAgent(squadSlug, targetAgent);
69
+ if (!agent) {
70
+ throw new Error(`Agent "${targetAgent}" not found in squad "${squadSlug}". Use squad_agents to list the roster.`);
71
+ }
72
+ }
73
+ else {
74
+ // If squad has named agents, pick the best match (first idle, or first one)
75
+ const agents = listSquadAgents(squadSlug);
76
+ if (agents.length > 0) {
77
+ agent = agents.find((a) => a.status === "idle") ?? agents[0];
78
+ }
79
+ }
80
+ const session = agent
81
+ ? await getOrCreateAgentSession(squadSlug, agent, task)
82
+ : await getOrCreateSession(squadSlug, task);
46
83
  const taskId = randomUUID();
47
- createTask(taskId, squadSlug, task);
84
+ const agentKey = agent
85
+ ? agentSessionKey(squadSlug, agent.character_name)
86
+ : squadSlug;
87
+ createTask(taskId, agentKey, task);
48
88
  updateSquadStatus(squadSlug, "working");
89
+ if (agent)
90
+ updateAgentStatus(squadSlug, agent.character_name, "working");
49
91
  // Run the task in the background — return taskId immediately
50
92
  void (async () => {
51
93
  try {
@@ -53,25 +95,32 @@ export async function delegateToAgent(squadSlug, task, onComplete) {
53
95
  const result = response?.data?.content ?? "Task completed (no output)";
54
96
  completeTask(taskId, result);
55
97
  updateSquadStatus(squadSlug, "idle");
98
+ if (agent)
99
+ updateAgentStatus(squadSlug, agent.character_name, "idle");
56
100
  onComplete(taskId, result);
57
101
  }
58
102
  catch (err) {
59
103
  const message = err instanceof Error ? err.message : String(err);
60
104
  failTask(taskId, message);
61
105
  updateSquadStatus(squadSlug, "error");
106
+ if (agent)
107
+ updateAgentStatus(squadSlug, agent.character_name, "error");
62
108
  }
63
109
  })();
110
+ const agentLabel = agent
111
+ ? `${agent.character_name} (${agent.role_title})`
112
+ : `squad "${squadSlug}"`;
64
113
  return taskId;
65
114
  }
66
115
  export async function shutdownAgents() {
67
- for (const [slug, session] of agentSessions) {
116
+ for (const [key, session] of agentSessions) {
68
117
  try {
69
118
  await session.destroy();
70
119
  }
71
120
  catch {
72
121
  // best-effort cleanup
73
122
  }
74
- agentSessions.delete(slug);
123
+ agentSessions.delete(key);
75
124
  }
76
125
  }
77
126
  export function getActiveAgentTasks() {
@@ -85,6 +134,76 @@ export function getActiveAgentTasks() {
85
134
  // ---------------------------------------------------------------------------
86
135
  // Internal helpers
87
136
  // ---------------------------------------------------------------------------
137
+ /**
138
+ * Create or resume a Copilot session for a specific named agent.
139
+ * The system message includes the agent's character personality, role, and charter.
140
+ */
141
+ async function getOrCreateAgentSession(squadSlug, agent, taskDescription) {
142
+ const key = agentSessionKey(squadSlug, agent.character_name);
143
+ const existing = agentSessions.get(key);
144
+ if (existing)
145
+ return existing;
146
+ const squad = getSquad(squadSlug);
147
+ const client = await getClient();
148
+ const decisions = getDecisionsSummary(squadSlug);
149
+ // Resolve model from agent's tier preference
150
+ const model = getModelForTier(agent.model_tier);
151
+ const universeName = squad.universe
152
+ ? getUniverse(squad.universe)?.name ?? squad.universe
153
+ : "Unknown";
154
+ const agentTools = buildAgentTools(squadSlug);
155
+ const systemMessage = `You are ${agent.character_name}, a specialist agent on the "${squad.name}" project team (${universeName} universe).
156
+
157
+ ## Your Identity
158
+ - **Name**: ${agent.character_name}
159
+ - **Role**: ${agent.role_title}
160
+ - **Personality**: ${agent.personality ?? "Professional and focused."}
161
+
162
+ ## Your Charter
163
+ ${agent.charter ?? "General-purpose agent. Handle tasks as they come."}
164
+
165
+ ## Project
166
+ - **Path**: ${squad.project_path}
167
+
168
+ ## Past Decisions
169
+ ${decisions}
170
+
171
+ ## Instructions
172
+ You are a coding agent. Use the shell tool to run commands and file_ops to read/write files.
173
+ Log important decisions with squad_log_decision so they persist.
174
+ Stay in character — let your personality color your work style and communication, but always deliver quality results.`;
175
+ const commonConfig = {
176
+ model,
177
+ configDir: SESSIONS_DIR,
178
+ streaming: false,
179
+ systemMessage: { content: systemMessage },
180
+ tools: agentTools,
181
+ onPermissionRequest: approveAll,
182
+ infiniteSessions: {
183
+ enabled: true,
184
+ backgroundCompactionThreshold: 0.8,
185
+ bufferExhaustionThreshold: 0.95,
186
+ },
187
+ };
188
+ let session;
189
+ if (agent.copilot_session_id) {
190
+ try {
191
+ session = await client.resumeSession(agent.copilot_session_id, commonConfig);
192
+ }
193
+ catch {
194
+ session = await client.createSession(commonConfig);
195
+ }
196
+ }
197
+ else {
198
+ session = await client.createSession(commonConfig);
199
+ }
200
+ updateAgentSession(squadSlug, agent.character_name, session.sessionId);
201
+ agentSessions.set(key, session);
202
+ return session;
203
+ }
204
+ /**
205
+ * Legacy: create a generic squad session (for squads without named agents).
206
+ */
88
207
  async function getOrCreateSession(squadSlug, taskDescription) {
89
208
  const existing = agentSessions.get(squadSlug);
90
209
  if (existing)
@@ -4,7 +4,7 @@ import { config } from "../config.js";
4
4
  import { SESSIONS_DIR, IO_VERSION } from "../paths.js";
5
5
  import { getState, setState, deleteState, logConversation } from "../store/db.js";
6
6
  import { clearStaleTasks, getTask } from "../store/tasks.js";
7
- import { getSquad, listSquads, createSquad, deleteSquad, logDecision, getDecisionsSummary, updateSquadStatus, } from "../store/squads.js";
7
+ import { getSquad, listSquads, createSquad, deleteSquad, logDecision, getDecisionsSummary, updateSquadStatus, addSquadAgent, listSquadAgents, removeSquadAgent, } from "../store/squads.js";
8
8
  import { readPage, writePage, assertPagePath, deletePage, listPages } from "../wiki/fs.js";
9
9
  import { resolveModelTiers } from "./model-router.js";
10
10
  import { searchWiki, getWikiSummary } from "../wiki/search.js";
@@ -37,7 +37,7 @@ let processing = false;
37
37
  // Session config helpers
38
38
  // ---------------------------------------------------------------------------
39
39
  function mapSquad(s) {
40
- return { slug: s.slug, name: s.name, projectPath: s.project_path, status: s.status };
40
+ return { slug: s.slug, name: s.name, projectPath: s.project_path, status: s.status, universe: s.universe };
41
41
  }
42
42
  function getToolDeps() {
43
43
  return {
@@ -65,6 +65,16 @@ function getToolDeps() {
65
65
  description: t.description,
66
66
  status: t.status,
67
67
  })),
68
+ addSquadAgent,
69
+ listSquadAgents: (slug) => listSquadAgents(slug).map((a) => ({
70
+ character_name: a.character_name,
71
+ role_title: a.role_title,
72
+ charter: a.charter,
73
+ model_tier: a.model_tier,
74
+ personality: a.personality,
75
+ status: a.status,
76
+ })),
77
+ removeSquadAgent,
68
78
  listSkills,
69
79
  installSkill,
70
80
  removeSkill,
@@ -56,21 +56,34 @@ You receive messages and decide how to handle them:
56
56
  ${squadBlock}
57
57
  ## Squad System
58
58
 
59
- Squads are persistent project teams. When a user works on a codebase:
60
- 1. Create a squad with \`squad_create\` — this sets up a persistent team for that project.
61
- 2. The squad remembers decisions via \`squad_log_decision\`.
62
- 3. Recall squad context with \`squad_recall\` before doing project work.
63
- 4. Check squad status with \`squad_status\`.
59
+ Squads are persistent project teams with **named specialist agents**. Each squad has an 80s pop culture theme (A-Team, Transformers, Thundercats, GI Joe, Aliens, Ghostbusters).
60
+
61
+ ### Creating a Squad
62
+ 1. **Create**: \`squad_create\` — creates the squad and assigns a random 80s universe (or specify one).
63
+ 2. **Analyze**: \`squad_analyze\` scan the project directory to understand languages, frameworks, tools.
64
+ 3. **Build the team**: Based on the analysis, use \`squad_add_agent\` for each specialist the project needs. Choose **dynamic role titles** based on what the project actually uses (e.g., "Express API Engineer", "Vue.js Frontend Dev", "Vitest Test Engineer"). Each agent gets the next character from the squad's universe.
65
+ 4. **Review**: \`squad_agents\` — see the full roster with character names, roles, and personalities.
66
+
67
+ ### Working with Squad Agents
68
+ - The squad remembers decisions via \`squad_log_decision\`.
69
+ - Recall context with \`squad_recall\` before doing project work.
70
+ - Check overall status with \`squad_status\`.
64
71
 
65
72
  ### Delegating Work
66
- After planning tasks with the user, **use \`squad_delegate\` to send each task to the squad agent for implementation**. The workflow is:
73
+ After planning tasks with the user, **use \`squad_delegate\` to send each task to the right agent**:
67
74
  1. Plan the work with the user (break into concrete tasks).
68
- 2. Call \`squad_delegate\` for each task provide detailed instructions including file paths, expected behavior, and acceptance criteria.
69
- 3. The agent works autonomously in the background. You get a task ID immediately.
75
+ 2. Call \`squad_delegate\` with the squad slug and task. Optionally specify an \`agent\` (character name) to target a specific specialist. If omitted, the system picks the best available agent.
76
+ 3. The agent works autonomously in the background with their specialized system prompt.
70
77
  4. Use \`squad_task_status\` to check progress and retrieve results.
71
- 5. Report results back to the user.
78
+ 5. Report results back to the user, mentioning the character name.
79
+
80
+ You can delegate multiple tasks to different agents in parallel.
72
81
 
73
- You can delegate multiple tasks in parallel — each gets its own task ID.
82
+ ### Agent Roles Are Dynamic
83
+ **Do NOT use generic roles** like "developer" or "tester". Analyze the project first and create roles that match its actual technology stack. Examples:
84
+ - IO project → "Copilot SDK Specialist", "Vue.js Frontend Dev", "Express API Engineer"
85
+ - .NET web app → "ASP.NET Core Backend", "Blazor UI Developer", "xUnit Test Engineer"
86
+ - Rust CLI → "Rust Systems Programmer", "CLI UX Designer", "Integration Test Engineer"
74
87
 
75
88
  ### Model Selection
76
89
  Squad agents are automatically assigned a model based on task complexity:
@@ -90,13 +103,17 @@ The model is selected automatically. Tell the user which model tier was chosen w
90
103
  - \`wiki_list\`: List all pages in your knowledge base.
91
104
 
92
105
  ### Squad Management
93
- - \`squad_create\`: Create a project squad.
106
+ - \`squad_create\`: Create a project squad (with optional 80s universe theme).
107
+ - \`squad_analyze\`: **Analyze a project** to determine what specialists are needed.
108
+ - \`squad_add_agent\`: **Add a named specialist** to a squad with a dynamic role title and charter.
109
+ - \`squad_agents\`: List a squad's agent roster.
110
+ - \`squad_remove_agent\`: Remove an agent from a squad.
94
111
  - \`squad_recall\`: Get a squad's context and decisions.
95
- - \`squad_status\`: Check squad status.
112
+ - \`squad_status\`: Check all squads and their rosters.
96
113
  - \`squad_log_decision\`: Log a decision for a squad.
97
- - \`squad_delegate\`: **Delegate a task to a squad agent.** The agent works autonomously in the background. Returns a task ID.
114
+ - \`squad_delegate\`: **Delegate a task to a specific agent** (by character name) or let the system pick.
98
115
  - \`squad_task_status\`: Check the status/result of a delegated task, or list all active tasks.
99
- - \`squad_delete\`: Delete a squad and all its decisions permanently.
116
+ - \`squad_delete\`: Delete a squad and all its agents/decisions permanently.
100
117
 
101
118
  ### Skills
102
119
  - \`skill_list\`: List all installed skills.