heyio 0.1.31 → 0.1.33

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 {
@@ -6,11 +6,17 @@ 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, classifyComplexity } 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
+ const agentSessionModels = new Map();
17
+ function agentSessionKey(squadSlug, characterName) {
18
+ return characterName ? `${squadSlug}:${characterName}` : squadSlug;
19
+ }
14
20
  export function getAgentInfo() {
15
21
  const activeTasks = getActiveTasks();
16
22
  const tasksByAgent = new Map();
@@ -18,34 +24,71 @@ export function getAgentInfo() {
18
24
  tasksByAgent.set(task.agent_slug, task.description);
19
25
  }
20
26
  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";
27
+ const seenSquads = new Set();
28
+ // Collect info from squad agents (named agents)
29
+ for (const [key, _session] of agentSessions) {
30
+ const parts = key.split(":");
31
+ const squadSlug = parts[0];
32
+ const characterName = parts[1];
33
+ seenSquads.add(squadSlug);
34
+ const squad = getSquad(squadSlug);
35
+ if (characterName) {
36
+ const agent = getSquadAgent(squadSlug, characterName);
37
+ const currentTask = tasksByAgent.get(key) ?? tasksByAgent.get(squadSlug);
38
+ agents.push({
39
+ slug: squadSlug,
40
+ name: agent ? `${agent.character_name} (${agent.role_title})` : characterName,
41
+ characterName,
42
+ roleTitle: agent?.role_title,
43
+ universe: squad?.universe ?? undefined,
44
+ status: agent?.status === "working" ? "working" : currentTask ? "working" : "idle",
45
+ currentTask,
46
+ });
27
47
  }
28
- if (squad?.status === "error") {
29
- status = "error";
48
+ else {
49
+ // Legacy generic agent
50
+ const currentTask = tasksByAgent.get(squadSlug);
51
+ agents.push({
52
+ slug: squadSlug,
53
+ name: squad?.name ?? squadSlug,
54
+ status: currentTask ? "working" : squad?.status === "error" ? "error" : "idle",
55
+ currentTask,
56
+ });
30
57
  }
31
- agents.push({
32
- slug,
33
- name: squad?.name ?? slug,
34
- status,
35
- currentTask,
36
- });
37
58
  }
38
59
  return agents;
39
60
  }
40
- export async function delegateToAgent(squadSlug, task, onComplete) {
61
+ export async function delegateToAgent(squadSlug, task, onComplete, targetAgent) {
41
62
  const squad = getSquad(squadSlug);
42
63
  if (!squad) {
43
64
  throw new Error(`Squad not found: ${squadSlug}`);
44
65
  }
45
- const session = await getOrCreateSession(squadSlug, task);
66
+ // Determine which agent session to use
67
+ let agent;
68
+ if (targetAgent) {
69
+ agent = getSquadAgent(squadSlug, targetAgent);
70
+ if (!agent) {
71
+ throw new Error(`Agent "${targetAgent}" not found in squad "${squadSlug}". Use squad_agents to list the roster.`);
72
+ }
73
+ }
74
+ else {
75
+ // If squad has named agents, pick the best match (first idle, or first one)
76
+ const agents = listSquadAgents(squadSlug);
77
+ if (agents.length > 0) {
78
+ agent = agents.find((a) => a.status === "idle") ?? agents[0];
79
+ }
80
+ }
81
+ const session = agent
82
+ ? await getOrCreateAgentSession(squadSlug, agent, task)
83
+ : await getOrCreateSession(squadSlug, task);
46
84
  const taskId = randomUUID();
47
- createTask(taskId, squadSlug, task);
85
+ const agentKey = agent
86
+ ? agentSessionKey(squadSlug, agent.character_name)
87
+ : squadSlug;
88
+ createTask(taskId, agentKey, task);
48
89
  updateSquadStatus(squadSlug, "working");
90
+ if (agent)
91
+ updateAgentStatus(squadSlug, agent.character_name, "working");
49
92
  // Run the task in the background — return taskId immediately
50
93
  void (async () => {
51
94
  try {
@@ -53,25 +96,33 @@ export async function delegateToAgent(squadSlug, task, onComplete) {
53
96
  const result = response?.data?.content ?? "Task completed (no output)";
54
97
  completeTask(taskId, result);
55
98
  updateSquadStatus(squadSlug, "idle");
99
+ if (agent)
100
+ updateAgentStatus(squadSlug, agent.character_name, "idle");
56
101
  onComplete(taskId, result);
57
102
  }
58
103
  catch (err) {
59
104
  const message = err instanceof Error ? err.message : String(err);
60
105
  failTask(taskId, message);
61
106
  updateSquadStatus(squadSlug, "error");
107
+ if (agent)
108
+ updateAgentStatus(squadSlug, agent.character_name, "error");
62
109
  }
63
110
  })();
111
+ const agentLabel = agent
112
+ ? `${agent.character_name} (${agent.role_title})`
113
+ : `squad "${squadSlug}"`;
64
114
  return taskId;
65
115
  }
66
116
  export async function shutdownAgents() {
67
- for (const [slug, session] of agentSessions) {
117
+ for (const [key, session] of agentSessions) {
68
118
  try {
69
119
  await session.destroy();
70
120
  }
71
121
  catch {
72
122
  // best-effort cleanup
73
123
  }
74
- agentSessions.delete(slug);
124
+ agentSessions.delete(key);
125
+ agentSessionModels.delete(key);
75
126
  }
76
127
  }
77
128
  export function getActiveAgentTasks() {
@@ -85,6 +136,97 @@ export function getActiveAgentTasks() {
85
136
  // ---------------------------------------------------------------------------
86
137
  // Internal helpers
87
138
  // ---------------------------------------------------------------------------
139
+ /**
140
+ * Create or resume a Copilot session for a specific named agent.
141
+ * Model is selected per-task: uses the higher of the agent's default tier
142
+ * and the task's classified complexity. This means an agent never gets a
143
+ * model worse than their baseline, but can be upgraded for complex tasks.
144
+ */
145
+ async function getOrCreateAgentSession(squadSlug, agent, taskDescription) {
146
+ const key = agentSessionKey(squadSlug, agent.character_name);
147
+ // Determine model based on task complexity vs agent's default tier
148
+ const agentTier = agent.model_tier;
149
+ const taskTier = taskDescription ? classifyComplexity(taskDescription) : agentTier;
150
+ const tierRank = { high: 3, medium: 2, low: 1 };
151
+ const effectiveTier = tierRank[taskTier] >= tierRank[agentTier] ? taskTier : agentTier;
152
+ const model = getModelForTier(effectiveTier);
153
+ // If we have a cached session, check if the model matches; if not, destroy and recreate
154
+ const existing = agentSessions.get(key);
155
+ if (existing) {
156
+ // Sessions don't expose their model, so track it separately
157
+ const cachedModel = agentSessionModels.get(key);
158
+ if (cachedModel === model)
159
+ return existing;
160
+ // Model changed — destroy old session for the upgraded model
161
+ console.error(`[io] Agent ${agent.character_name}: upgrading model ${cachedModel} → ${model} for task complexity`);
162
+ try {
163
+ await existing.destroy();
164
+ }
165
+ catch { /* best-effort */ }
166
+ agentSessions.delete(key);
167
+ agentSessionModels.delete(key);
168
+ }
169
+ const squad = getSquad(squadSlug);
170
+ const client = await getClient();
171
+ const decisions = getDecisionsSummary(squadSlug);
172
+ console.error(`[io] Agent ${agent.character_name}: using model "${model}" (agent tier: ${agentTier}, task tier: ${taskTier}, effective: ${effectiveTier})`);
173
+ const universeName = squad.universe
174
+ ? getUniverse(squad.universe)?.name ?? squad.universe
175
+ : "Unknown";
176
+ const agentTools = buildAgentTools(squadSlug);
177
+ const systemMessage = `You are ${agent.character_name}, a specialist agent on the "${squad.name}" project team (${universeName} universe).
178
+
179
+ ## Your Identity
180
+ - **Name**: ${agent.character_name}
181
+ - **Role**: ${agent.role_title}
182
+ - **Personality**: ${agent.personality ?? "Professional and focused."}
183
+
184
+ ## Your Charter
185
+ ${agent.charter ?? "General-purpose agent. Handle tasks as they come."}
186
+
187
+ ## Project
188
+ - **Path**: ${squad.project_path}
189
+
190
+ ## Past Decisions
191
+ ${decisions}
192
+
193
+ ## Instructions
194
+ You are a coding agent. Use the shell tool to run commands and file_ops to read/write files.
195
+ Log important decisions with squad_log_decision so they persist.
196
+ Stay in character — let your personality color your work style and communication, but always deliver quality results.`;
197
+ const commonConfig = {
198
+ model,
199
+ configDir: SESSIONS_DIR,
200
+ streaming: false,
201
+ systemMessage: { content: systemMessage },
202
+ tools: agentTools,
203
+ onPermissionRequest: approveAll,
204
+ infiniteSessions: {
205
+ enabled: true,
206
+ backgroundCompactionThreshold: 0.8,
207
+ bufferExhaustionThreshold: 0.95,
208
+ },
209
+ };
210
+ let session;
211
+ if (agent.copilot_session_id) {
212
+ try {
213
+ session = await client.resumeSession(agent.copilot_session_id, commonConfig);
214
+ }
215
+ catch {
216
+ session = await client.createSession(commonConfig);
217
+ }
218
+ }
219
+ else {
220
+ session = await client.createSession(commonConfig);
221
+ }
222
+ updateAgentSession(squadSlug, agent.character_name, session.sessionId);
223
+ agentSessions.set(key, session);
224
+ agentSessionModels.set(key, model);
225
+ return session;
226
+ }
227
+ /**
228
+ * Legacy: create a generic squad session (for squads without named agents).
229
+ */
88
230
  async function getOrCreateSession(squadSlug, taskDescription) {
89
231
  const existing = agentSessions.get(squadSlug);
90
232
  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.