@teammates/cli 0.3.3 → 0.4.0

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
@@ -129,7 +129,7 @@ The CLI uses a generic adapter interface to support any coding agent. Each adapt
129
129
  ### How Adapters Work
130
130
 
131
131
  1. The adapter queries the recall index for relevant memories (automatic, in-process)
132
- 2. The orchestrator builds a full prompt (SOUL → WISDOM → recall results → daily logs weekly summaries → session history → roster → task)
132
+ 2. The orchestrator builds a full prompt within a 32k token budget (SOUL → WISDOM → recall results → daily logs (budget-trimmed) → session state → roster → task)
133
133
  3. The prompt is written to a temp file
134
134
  4. The agent CLI is spawned with the prompt
135
135
  5. stdout/stderr are captured for result parsing
package/dist/adapter.d.ts CHANGED
@@ -70,14 +70,25 @@ export declare function syncRecallIndex(teammatesDir: string, teammate?: string)
70
70
  /**
71
71
  * Build the full prompt for a teammate session.
72
72
  * Includes identity, memory, roster, output protocol, and the task.
73
+ *
74
+ * Context budget (32k tokens):
75
+ * - Current daily log (today): always included, outside budget
76
+ * - Days 2-7: up to 24k tokens (whole entries)
77
+ * - Recall results: at least 8k tokens + unused daily log budget
78
+ * (last entry may overflow by up to 4k tokens)
79
+ * - Weekly summaries: excluded (already indexed by recall)
80
+ *
81
+ * Identity, wisdom, roster, and protocol are never trimmed.
82
+ * The task prompt is never trimmed.
73
83
  */
74
84
  export declare function buildTeammatePrompt(teammate: TeammateConfig, taskPrompt: string, options?: {
75
85
  handoffContext?: string;
76
86
  roster?: RosterEntry[];
77
87
  services?: InstalledService[];
78
88
  sessionFile?: string;
79
- sessionContent?: string;
80
89
  recallResults?: SearchResult[];
90
+ /** Token budget for the prompt wrapper (default 64k). Task is excluded. */
91
+ tokenBudget?: number;
81
92
  }): string;
82
93
  /**
83
94
  * Format a handoff envelope into a human-readable context string.
package/dist/adapter.js CHANGED
@@ -40,164 +40,166 @@ export async function syncRecallIndex(teammatesDir, teammate) {
40
40
  await indexer.syncAll();
41
41
  }
42
42
  }
43
+ /**
44
+ * Default token budget for the prompt wrapper (everything except the task).
45
+ * ~64k tokens ≈ 256k chars at ~4 chars/token.
46
+ * The task prompt itself is excluded from this budget — if a user pastes
47
+ * a large input, that's intentional and we don't trim it.
48
+ */
49
+ const DEFAULT_TOKEN_BUDGET = 64_000;
50
+ const CHARS_PER_TOKEN = 4;
51
+ /**
52
+ * Context budget allocation:
53
+ * - Days 2-7 get up to DAILY_LOG_BUDGET tokens (whole entries)
54
+ * - Recall gets at least RECALL_MIN_BUDGET, plus whatever daily logs didn't use
55
+ * - Last recall entry can push total up to CONTEXT_BUDGET + RECALL_OVERFLOW (36k)
56
+ * - Weekly summaries are excluded (already indexed by recall)
57
+ */
58
+ const CONTEXT_BUDGET_TOKENS = 32_000;
59
+ const DAILY_LOG_BUDGET_TOKENS = 24_000;
60
+ const RECALL_MIN_BUDGET_TOKENS = 8_000;
61
+ const RECALL_OVERFLOW_TOKENS = 4_000;
62
+ /** Estimate tokens from character count. */
63
+ function estimateTokens(text) {
64
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
65
+ }
43
66
  /**
44
67
  * Build the full prompt for a teammate session.
45
68
  * Includes identity, memory, roster, output protocol, and the task.
69
+ *
70
+ * Context budget (32k tokens):
71
+ * - Current daily log (today): always included, outside budget
72
+ * - Days 2-7: up to 24k tokens (whole entries)
73
+ * - Recall results: at least 8k tokens + unused daily log budget
74
+ * (last entry may overflow by up to 4k tokens)
75
+ * - Weekly summaries: excluded (already indexed by recall)
76
+ *
77
+ * Identity, wisdom, roster, and protocol are never trimmed.
78
+ * The task prompt is never trimmed.
46
79
  */
47
80
  export function buildTeammatePrompt(teammate, taskPrompt, options) {
48
81
  const parts = [];
49
- // ── Identity ──────────────────────────────────────────────────────
50
- parts.push(`# You are ${teammate.name}\n`);
51
- parts.push(teammate.soul);
52
- parts.push("\n---\n");
53
- // ── Wisdom ───────────────────────────────────────────────────────
82
+ // ── Identity (required) ─────────────────────────────────────────
83
+ parts.push(`# You are ${teammate.name}\n\n${teammate.soul}\n\n---\n`);
84
+ // ── Wisdom (required) ───────────────────────────────────────────
54
85
  if (teammate.wisdom.trim()) {
55
- parts.push("## Your Wisdom\n");
56
- parts.push(teammate.wisdom);
57
- parts.push("\n---\n");
58
- }
59
- // ── Recall results (relevant episodic & semantic memories) ────────
60
- if (options?.recallResults && options.recallResults.length > 0) {
61
- parts.push("## Relevant Memories (from recall search)\n");
62
- parts.push("These memories were retrieved based on relevance to the current task:\n");
63
- for (const r of options.recallResults) {
86
+ parts.push(`## Your Wisdom\n\n${teammate.wisdom}\n\n---\n`);
87
+ }
88
+ // ── Budget-allocated context (daily logs → recall) ──────────────
89
+ // Today's log: always included, outside budget
90
+ // Days 2-7: up to 24k tokens (whole entries)
91
+ // Recall: at least 8k + unused daily budget, last entry may overflow by 4k
92
+ const todayLog = teammate.dailyLogs.slice(0, 1);
93
+ const pastLogs = teammate.dailyLogs.slice(1, 7); // days 2-7
94
+ let dailyBudget = DAILY_LOG_BUDGET_TOKENS;
95
+ // Current daily log (today) — never trimmed, always included
96
+ if (todayLog.length > 0) {
97
+ const todayLines = ["## Recent Daily Logs\n"];
98
+ for (const log of todayLog) {
99
+ todayLines.push(`### ${log.date}\n${log.content}\n`);
100
+ }
101
+ parts.push(todayLines.join("\n"));
102
+ }
103
+ // Days 2-7 — whole entries, up to 24k tokens
104
+ if (pastLogs.length > 0) {
105
+ const lines = [];
106
+ for (const log of pastLogs) {
107
+ const entry = `### ${log.date}\n${log.content}\n`;
108
+ const cost = estimateTokens(entry);
109
+ if (cost > dailyBudget)
110
+ break;
111
+ lines.push(entry);
112
+ dailyBudget -= cost;
113
+ }
114
+ if (lines.length > 0)
115
+ parts.push(lines.join("\n"));
116
+ }
117
+ // Recall results — gets at least 8k tokens, plus unused daily budget
118
+ // Last entry may overflow by up to 4k tokens
119
+ const recallBudget = Math.max(RECALL_MIN_BUDGET_TOKENS, RECALL_MIN_BUDGET_TOKENS + dailyBudget);
120
+ const recallResults = options?.recallResults ?? [];
121
+ if (recallResults.length > 0) {
122
+ const lines = [
123
+ "## Relevant Memories (from recall search)\n",
124
+ "These memories were retrieved based on relevance to the current task:\n",
125
+ ];
126
+ const headerCost = estimateTokens(lines.join("\n"));
127
+ let recallUsed = headerCost;
128
+ for (const r of recallResults) {
64
129
  const label = r.contentType
65
130
  ? `[${r.contentType}] ${r.uri}`
66
131
  : r.uri;
67
- parts.push(`### ${label}\n${r.text}\n`);
132
+ const entry = `### ${label}\n${r.text}\n`;
133
+ const cost = estimateTokens(entry);
134
+ if (recallUsed + cost > recallBudget + RECALL_OVERFLOW_TOKENS)
135
+ break;
136
+ lines.push(entry);
137
+ recallUsed += cost;
138
+ // Stop cleanly at budget — but allow the current entry (overflow grace)
139
+ if (recallUsed >= recallBudget)
140
+ break;
68
141
  }
69
- parts.push("\n---\n");
70
- }
71
- // ── Recent daily logs ──────────────────────────────────────────────
72
- if (teammate.dailyLogs.length > 0) {
73
- parts.push("## Recent Daily Logs\n");
74
- for (const log of teammate.dailyLogs.slice(0, 7)) {
75
- parts.push(`### ${log.date}\n${log.content}\n`);
142
+ if (lines.length > 2) {
143
+ lines.push("\n---\n");
144
+ parts.push(lines.join("\n"));
76
145
  }
77
- parts.push("\n---\n");
78
146
  }
79
- // ── Weekly summaries (recent episodic context) ─────────────────────
80
- if (teammate.weeklyLogs.length > 0) {
81
- parts.push("## Recent Weekly Summaries\n");
82
- for (const log of teammate.weeklyLogs.slice(0, 2)) {
83
- parts.push(`### ${log.week}\n${log.content}\n`);
147
+ // Close context section with separator if needed
148
+ if (todayLog.length > 0 || pastLogs.length > 0) {
149
+ const lastPart = parts[parts.length - 1];
150
+ if (!lastPart.endsWith("---\n")) {
151
+ parts.push("\n---\n");
84
152
  }
85
- parts.push("\n---\n");
86
- }
87
- // ── Session history (prior tasks in this session) ─────────────────
88
- if (options?.sessionContent?.trim()) {
89
- parts.push("## Session History\n");
90
- parts.push("These are entries from your prior tasks in this session:\n");
91
- parts.push(options.sessionContent);
92
- parts.push("\n---\n");
93
153
  }
94
- // ── Team roster ───────────────────────────────────────────────────
154
+ // ── Team roster (required, small) ───────────────────────────────
95
155
  if (options?.roster && options.roster.length > 0) {
96
- parts.push("## Your Team\n");
97
- parts.push("These are the other teammates you can hand off work to:\n");
156
+ const lines = [
157
+ "## Your Team\n",
158
+ "These are the other teammates you can hand off work to:\n",
159
+ ];
98
160
  for (const t of options.roster) {
99
161
  if (t.name === teammate.name)
100
162
  continue;
101
163
  const owns = t.ownership.primary.length > 0
102
164
  ? ` — owns: ${t.ownership.primary.join(", ")}`
103
165
  : "";
104
- parts.push(`- **@${t.name}**: ${t.role}${owns}`);
166
+ lines.push(`- **@${t.name}**: ${t.role}${owns}`);
105
167
  }
106
- parts.push("\n---\n");
168
+ lines.push("\n---\n");
169
+ parts.push(lines.join("\n"));
107
170
  }
108
- // ── Installed services ──────────────────────────────────────────────
171
+ // ── Installed services (required, small) ────────────────────────
109
172
  if (options?.services && options.services.length > 0) {
110
- parts.push("## Available Services\n");
111
- parts.push("These services are installed and available for you to use:\n");
173
+ const lines = [
174
+ "## Available Services\n",
175
+ "These services are installed and available for you to use:\n",
176
+ ];
112
177
  for (const svc of options.services) {
113
- parts.push(`### ${svc.name}\n`);
114
- parts.push(svc.description);
115
- parts.push(`\n**Usage:** \`${svc.usage}\`\n`);
178
+ lines.push(`### ${svc.name}\n`);
179
+ lines.push(svc.description);
180
+ lines.push(`\n**Usage:** \`${svc.usage}\`\n`);
116
181
  }
117
- parts.push("\n---\n");
182
+ lines.push("\n---\n");
183
+ parts.push(lines.join("\n"));
118
184
  }
119
- // ── Handoff context (if this task came from another teammate) ─────
185
+ // ── Handoff context (required when present) ─────────────────────
120
186
  if (options?.handoffContext) {
121
- parts.push("## Handoff Context\n");
122
- parts.push(options.handoffContext);
123
- parts.push("\n---\n");
187
+ parts.push(`## Handoff Context\n\n${options.handoffContext}\n\n---\n`);
124
188
  }
125
- // ── Session state ────────────────────────────────────────────────
189
+ // ── Session state (required) ────────────────────────────────────
126
190
  if (options?.sessionFile) {
127
- parts.push("## Session State\n");
128
- parts.push(`Your session file is at: \`${options.sessionFile}\`
129
-
130
- **Before returning your result**, append a brief entry to this file with:
131
- - What you did
132
- - Key decisions made
133
- - Files changed
134
- - Anything the next task should know
135
-
136
- This is how you maintain continuity across tasks. Always read it, always update it.
137
- `);
138
- parts.push("\n---\n");
139
- }
140
- // ── Memory updates ─────────────────────────────────────────────────
191
+ parts.push(`## Session State\n\nYour session file is at: \`${options.sessionFile}\`\n\n**Before returning your result**, append a brief entry to this file with:\n- What you did\n- Key decisions made\n- Files changed\n- Anything the next task should know\n\nThis is how you maintain continuity across tasks. Always read it, always update it.\n\n---\n`);
192
+ }
193
+ // ── Memory updates (required) ───────────────────────────────────
141
194
  const today = new Date().toISOString().slice(0, 10);
142
- parts.push("## Memory Updates\n");
143
- parts.push(`**Before returning your result**, update your memory files:
144
-
145
- 1. **Daily log** — Read \`.teammates/${teammate.name}/memory/${today}.md\` first (it may have entries from earlier tasks today), then write it back with your entry added. Create the file if it doesn't exist.
146
- - What you did
147
- - Key decisions made
148
- - Files changed
149
- - Anything the next task should know
150
-
151
- 2. **Typed memories** — If you learned something durable (a decision, pattern, feedback, or reference), create a typed memory file at \`.teammates/${teammate.name}/memory/<type>_<topic>.md\` with frontmatter (\`name\`, \`description\`, \`type\`). Update existing memory files if the topic already has one.
152
-
153
- 3. **WISDOM.md** — Do not edit directly. Wisdom entries are distilled from typed memories during compaction.
154
-
155
- These files are your persistent memory. Without them, your next session starts from scratch.
156
- `);
157
- parts.push("\n---\n");
158
- // ── Output protocol ───────────────────────────────────────────────
159
- parts.push("## Output Protocol (CRITICAL)\n");
160
- parts.push(`**Your #1 job is to produce a visible text response.** Session updates and memory writes are secondary — they support continuity but are not the deliverable. The user sees ONLY your text output. If you update files but return no text, the user sees an empty message and your work is invisible.
161
-
162
- Format your response as:
163
-
164
- \`\`\`
165
- TO: user
166
- # <Subject line>
167
-
168
- <Body — full markdown response>
169
- \`\`\`
170
-
171
- **Handoffs:** To hand off work to a teammate, include a fenced handoff block anywhere in your response:
172
-
173
- \`\`\`
174
- \`\`\`handoff
175
- @<teammate>
176
- <task description — what you need them to do, with full context>
177
- \`\`\`
178
- \`\`\`
179
-
180
- **Rules:**
181
- - **You MUST end your turn with visible text output.** A turn that ends with only tool calls and no text is a failed turn.
182
- - The \`# Subject\` line is REQUIRED — it becomes the message title.
183
- - Always write a substantive body. Never return just the subject.
184
- - Use markdown: headings, lists, code blocks, bold, etc.
185
- - Do as much work as you can before handing off.
186
- - Only hand off to teammates listed in "Your Team" above.
187
- - The handoff block can appear anywhere in your response — it will be detected automatically.
188
- `);
189
- parts.push("\n---\n");
190
- // ── Current date/time ────────────────────────────────────────────
195
+ parts.push(`## Memory Updates\n\n**Before returning your result**, update your memory files:\n\n1. **Daily log** — Read \`.teammates/${teammate.name}/memory/${today}.md\` first (it may have entries from earlier tasks today), then write it back with your entry added. Create the file if it doesn't exist.\n - What you did\n - Key decisions made\n - Files changed\n - Anything the next task should know\n\n2. **Typed memories** — If you learned something durable (a decision, pattern, feedback, or reference), create a typed memory file at \`.teammates/${teammate.name}/memory/<type>_<topic>.md\` with frontmatter (\`name\`, \`description\`, \`type\`). Update existing memory files if the topic already has one.\n\n3. **WISDOM.md** — Do not edit directly. Wisdom entries are distilled from typed memories during compaction.\n\nThese files are your persistent memory. Without them, your next session starts from scratch.\n\n---\n`);
196
+ // ── Output protocol (required) ──────────────────────────────────
197
+ parts.push(`## Output Protocol (CRITICAL)\n\n**Your #1 job is to produce a visible text response.** Session updates and memory writes are secondary — they support continuity but are not the deliverable. The user sees ONLY your text output. If you update files but return no text, the user sees an empty message and your work is invisible.\n\nFormat your response as:\n\n\`\`\`\nTO: user\n# <Subject line>\n\n<Body — full markdown response>\n\`\`\`\n\n**Handoffs:** To hand off work to a teammate, include a fenced handoff block anywhere in your response:\n\n\`\`\`\n\`\`\`handoff\n@<teammate>\n<task description — what you need them to do, with full context>\n\`\`\`\n\`\`\`\n\n**Rules:**\n- **You MUST end your turn with visible text output.** A turn that ends with only tool calls and no text is a failed turn.\n- The \`# Subject\` line is REQUIRED — it becomes the message title.\n- Always write a substantive body. Never return just the subject.\n- Use markdown: headings, lists, code blocks, bold, etc.\n- Do as much work as you can before handing off.\n- Only hand off to teammates listed in "Your Team" above.\n- The handoff block can appear anywhere in your response — it will be detected automatically.\n\n---\n`);
198
+ // ── Current date/time (required, small) ─────────────────────────
191
199
  const now = new Date();
192
- parts.push(`**Current date:** ${now.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })} (${today})`);
193
- parts.push(`**Current time:** ${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}\n`);
194
- parts.push("---\n");
195
- // ── Task ──────────────────────────────────────────────────────────
196
- parts.push("## Task\n");
197
- parts.push(taskPrompt);
198
- parts.push("\n---\n");
199
- // ── Final reminder (last thing the agent reads) ─────────────────
200
- parts.push("**REMINDER: After completing the task and updating session/memory files, you MUST produce a text response starting with `TO: user`. An empty response is a bug.**");
200
+ parts.push(`**Current date:** ${now.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })} (${today})\n**Current time:** ${now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}\n\n---\n`);
201
+ // ── Task (always included, excluded from budget) ────────────────
202
+ parts.push(`## Task\n\n${taskPrompt}\n\n---\n\n**REMINDER: After completing the task and updating session/memory files, you MUST produce a text response starting with \`TO: user\`. An empty response is a bug.**`);
201
203
  return parts.join("\n");
202
204
  }
203
205
  /**
@@ -99,6 +99,80 @@ describe("buildTeammatePrompt", () => {
99
99
  expect(prompt).toContain("## Session State");
100
100
  expect(prompt).toContain("/tmp/beacon-session.md");
101
101
  });
102
+ it("drops daily logs that exceed the 24k daily budget", () => {
103
+ // Each log is ~50k chars = ~12.5k tokens. Only 1 fits in 24k daily budget.
104
+ const bigContent = "D".repeat(50_000);
105
+ const config = makeConfig({
106
+ dailyLogs: [
107
+ { date: "2026-03-18", content: "Today's log — never trimmed" },
108
+ { date: "2026-03-17", content: bigContent }, // day 2 — fits in 24k
109
+ { date: "2026-03-16", content: bigContent }, // day 3 — exceeds 24k, dropped
110
+ ],
111
+ });
112
+ const prompt = buildTeammatePrompt(config, "task");
113
+ // Today's log is always fully present (never trimmed)
114
+ expect(prompt).toContain("Today's log — never trimmed");
115
+ // Day 2 fits within 24k
116
+ expect(prompt).toContain("2026-03-17");
117
+ // Day 3 doesn't fit (12.5k + 12.5k > 24k)
118
+ expect(prompt).not.toContain("2026-03-16");
119
+ });
120
+ it("recall gets at least 8k tokens even when daily logs use full 24k", () => {
121
+ // Daily logs fill their 24k budget. Recall still gets its guaranteed 8k minimum.
122
+ const dailyContent = "D".repeat(90_000); // ~22.5k tokens — fits in 24k
123
+ const config = makeConfig({
124
+ dailyLogs: [
125
+ { date: "2026-03-18", content: "today" },
126
+ { date: "2026-03-17", content: dailyContent },
127
+ ],
128
+ });
129
+ const recallText = "R".repeat(20_000); // ~5k tokens — fits in 8k min
130
+ const prompt = buildTeammatePrompt(config, "task", {
131
+ recallResults: [
132
+ { teammate: "beacon", uri: "memory/decision_foo.md", text: recallText, score: 0.9, contentType: "typed_memory" },
133
+ ],
134
+ });
135
+ expect(prompt).toContain("2026-03-17");
136
+ expect(prompt).toContain("## Relevant Memories");
137
+ });
138
+ it("recall gets unused daily log budget", () => {
139
+ // Small daily logs leave most of 24k unused — recall gets the surplus.
140
+ const config = makeConfig({
141
+ dailyLogs: [
142
+ { date: "2026-03-18", content: "today" },
143
+ { date: "2026-03-17", content: "short day 2" }, // ~3 tokens
144
+ ],
145
+ });
146
+ // Large recall result — should fit because daily logs barely used any budget
147
+ const recallText = "R".repeat(80_000); // ~20k tokens — fits in (8k + ~24k unused)
148
+ const prompt = buildTeammatePrompt(config, "task", {
149
+ recallResults: [
150
+ { teammate: "beacon", uri: "memory/big.md", text: recallText, score: 0.9, contentType: "typed_memory" },
151
+ ],
152
+ });
153
+ expect(prompt).toContain("## Relevant Memories");
154
+ expect(prompt).toContain("memory/big.md");
155
+ });
156
+ it("weekly summaries are excluded (indexed by recall)", () => {
157
+ const config = makeConfig({
158
+ dailyLogs: [{ date: "2026-03-13", content: "short log" }],
159
+ weeklyLogs: [{ week: "2026-W11", content: "short summary" }],
160
+ });
161
+ const prompt = buildTeammatePrompt(config, "task");
162
+ expect(prompt).toContain("## Recent Daily Logs");
163
+ expect(prompt).not.toContain("## Recent Weekly Summaries");
164
+ });
165
+ it("excludes task prompt from budget calculation", () => {
166
+ // Large task prompt should not trigger trimming of wrapper sections
167
+ const bigTask = "x".repeat(100_000);
168
+ const config = makeConfig({
169
+ dailyLogs: [{ date: "2026-03-13", content: "small log" }],
170
+ });
171
+ const prompt = buildTeammatePrompt(config, bigTask);
172
+ // Daily logs should still be included despite the huge task
173
+ expect(prompt).toContain("## Recent Daily Logs");
174
+ expect(prompt).toContain("small log");
175
+ });
102
176
  });
103
177
  describe("formatHandoffContext", () => {
104
178
  it("formats basic handoff", () => {
@@ -140,16 +140,6 @@ export class CliProxyAdapter {
140
140
  // If the teammate has no soul (e.g. the raw agent), skip identity/memory
141
141
  // wrapping but include handoff instructions so it can delegate to teammates
142
142
  const sessionFile = this.sessionFiles.get(teammate.name);
143
- // Read session file content for injection into the prompt
144
- let sessionContent;
145
- if (sessionFile) {
146
- try {
147
- sessionContent = await readFile(sessionFile, "utf-8");
148
- }
149
- catch {
150
- // Session file may not exist yet — that's fine
151
- }
152
- }
153
143
  let fullPrompt;
154
144
  if (teammate.soul) {
155
145
  // Query recall for relevant memories before building prompt
@@ -163,7 +153,6 @@ export class CliProxyAdapter {
163
153
  roster: this.roster,
164
154
  services: this.services,
165
155
  sessionFile,
166
- sessionContent,
167
156
  recallResults: recall?.results,
168
157
  });
169
158
  }
@@ -59,16 +59,6 @@ export class CopilotAdapter {
59
59
  async executeTask(_sessionId, teammate, prompt) {
60
60
  await this.ensureClient(teammate.cwd);
61
61
  const sessionFile = this.sessionFiles.get(teammate.name);
62
- // Read session file content for injection into the prompt
63
- let sessionContent;
64
- if (sessionFile) {
65
- try {
66
- sessionContent = await readFile(sessionFile, "utf-8");
67
- }
68
- catch {
69
- // Session file may not exist yet — that's fine
70
- }
71
- }
72
62
  // Build the full teammate prompt (identity + memory + task)
73
63
  let fullPrompt;
74
64
  if (teammate.soul) {
@@ -83,7 +73,6 @@ export class CopilotAdapter {
83
73
  roster: this.roster,
84
74
  services: this.services,
85
75
  sessionFile,
86
- sessionContent,
87
76
  recallResults: recall?.results,
88
77
  });
89
78
  }
package/dist/cli.js CHANGED
@@ -10,7 +10,6 @@
10
10
  import { exec as execCb, execSync, spawn } from "node:child_process";
11
11
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
13
- import { tmpdir } from "node:os";
14
13
  import { dirname, join, resolve } from "node:path";
15
14
  import { createInterface } from "node:readline";
16
15
  import { App, ChatView, concat, esc, Interview, pen, renderMarkdown, stripAnsi, } from "@teammates/consolonia";
@@ -39,24 +38,78 @@ class TeammatesREPL {
39
38
  lastResult = null;
40
39
  lastResults = new Map();
41
40
  conversationHistory = [];
41
+ /** Running summary of older conversation history maintained by the coding agent. */
42
+ conversationSummary = "";
42
43
  storeResult(result) {
43
44
  this.lastResult = result;
44
45
  this.lastResults.set(result.teammate, result);
45
46
  this.conversationHistory.push({
46
47
  role: result.teammate,
47
- text: result.rawOutput ?? result.summary,
48
+ text: result.summary,
48
49
  });
49
50
  }
51
+ /** Token budget for recent conversation history (24k tokens ≈ 96k chars). */
52
+ static CONV_HISTORY_CHARS = 24_000 * 4;
50
53
  buildConversationContext() {
51
- if (this.conversationHistory.length === 0)
54
+ if (this.conversationHistory.length === 0 && !this.conversationSummary)
52
55
  return "";
53
- // Keep last 10 exchanges to avoid blowing up prompt size
54
- const recent = this.conversationHistory.slice(-10);
55
- const lines = ["## Conversation History\n"];
56
- for (const entry of recent) {
57
- lines.push(`**${entry.role}:** ${entry.text}\n`);
56
+ const budget = TeammatesREPL.CONV_HISTORY_CHARS;
57
+ const parts = ["## Conversation History\n"];
58
+ // Include running summary of older conversation if present
59
+ if (this.conversationSummary) {
60
+ parts.push(`### Previous Conversation Summary\n\n${this.conversationSummary}\n`);
61
+ }
62
+ // Work backwards from newest — include whole entries up to 24k tokens
63
+ const entries = [];
64
+ let used = 0;
65
+ for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
66
+ const line = `**${this.conversationHistory[i].role}:** ${this.conversationHistory[i].text}\n`;
67
+ if (used + line.length > budget && entries.length > 0)
68
+ break;
69
+ entries.unshift(line);
70
+ used += line.length;
58
71
  }
59
- return lines.join("\n");
72
+ if (entries.length > 0)
73
+ parts.push(entries.join("\n"));
74
+ return parts.join("\n");
75
+ }
76
+ /**
77
+ * Check if conversation history exceeds the 24k token budget.
78
+ * If so, take the older entries that won't fit, combine with existing summary,
79
+ * and queue a summarization task to the coding agent.
80
+ */
81
+ maybeQueueSummarization() {
82
+ const budget = TeammatesREPL.CONV_HISTORY_CHARS;
83
+ // Calculate how many recent entries fit in the budget (newest first)
84
+ let recentChars = 0;
85
+ let splitIdx = this.conversationHistory.length;
86
+ for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
87
+ const line = `**${this.conversationHistory[i].role}:** ${this.conversationHistory[i].text}\n`;
88
+ if (recentChars + line.length > budget)
89
+ break;
90
+ recentChars += line.length;
91
+ splitIdx = i;
92
+ }
93
+ if (splitIdx === 0)
94
+ return; // everything fits — nothing to summarize
95
+ // Collect entries that are being pushed out
96
+ const toSummarize = this.conversationHistory.slice(0, splitIdx);
97
+ const entriesText = toSummarize
98
+ .map((e) => `**${e.role}:** ${e.text}`)
99
+ .join("\n");
100
+ // Build the summarization prompt
101
+ const prompt = this.conversationSummary
102
+ ? `You are maintaining a running summary of an ongoing conversation between a user and their AI teammates. Update the existing summary to incorporate the new conversation entries below.\n\n## Current Summary\n\n${this.conversationSummary}\n\n## New Entries to Incorporate\n\n${entriesText}\n\n## Instructions\n\nReturn ONLY the updated summary — no preamble, no explanation. The summary should:\n- Be a concise bulleted list of key topics discussed, decisions made, and work completed\n- Preserve important context that future messages might reference\n- Drop trivial or redundant details\n- Stay under 2000 characters\n- Do NOT include any output protocol (no TO:, no # Subject, no handoff blocks)`
103
+ : `You are maintaining a running summary of an ongoing conversation between a user and their AI teammates. Summarize the conversation entries below.\n\n## Entries to Summarize\n\n${entriesText}\n\n## Instructions\n\nReturn ONLY the summary — no preamble, no explanation. The summary should:\n- Be a concise bulleted list of key topics discussed, decisions made, and work completed\n- Preserve important context that future messages might reference\n- Drop trivial or redundant details\n- Stay under 2000 characters\n- Do NOT include any output protocol (no TO:, no # Subject, no handoff blocks)`;
104
+ // Remove the summarized entries — they'll be captured in the summary
105
+ this.conversationHistory.splice(0, splitIdx);
106
+ // Queue the summarization task to the base coding agent
107
+ this.taskQueue.push({
108
+ type: "summarize",
109
+ teammate: this.adapterName,
110
+ task: prompt,
111
+ });
112
+ this.kickDrain();
60
113
  }
61
114
  adapterName;
62
115
  teammatesDir;
@@ -2471,6 +2524,10 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2471
2524
  if (this.activeTasks.size === 0) {
2472
2525
  this.stopStatusAnimation();
2473
2526
  }
2527
+ // Suppress display for internal summarization tasks
2528
+ const activeEntry = this.agentActive.get(event.result.teammate);
2529
+ if (activeEntry?.type === "summarize")
2530
+ break;
2474
2531
  if (!this.chatView)
2475
2532
  this.input.deactivateAndErase();
2476
2533
  const raw = event.result.rawOutput ?? "";
@@ -2486,15 +2543,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2486
2543
  const subject = event.result.summary || "Task completed";
2487
2544
  this.feedLine(concat(tp.accent(`${event.result.teammate}: `), tp.text(subject)));
2488
2545
  this.lastCleanedOutput = cleaned;
2489
- if (sizeKB > 5) {
2490
- const tmpFile = join(tmpdir(), `teammates-${event.result.teammate}-${Date.now()}.md`);
2491
- writeFileSync(tmpFile, cleaned, "utf-8");
2492
- this.feedLine(tp.muted(` ${"─".repeat(40)}`));
2493
- this.feedLine(tp.warning(` ⚠ Response is ${sizeKB.toFixed(1)}KB — saved to temp file:`));
2494
- this.feedLine(tp.muted(` ${tmpFile}`));
2495
- this.feedLine(tp.muted(` ${"─".repeat(40)}`));
2496
- }
2497
- else if (cleaned) {
2546
+ if (cleaned) {
2498
2547
  this.feedMarkdown(cleaned);
2499
2548
  }
2500
2549
  else {
@@ -2719,6 +2768,20 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2719
2768
  if (entry.type === "compact") {
2720
2769
  await this.runCompact(entry.teammate);
2721
2770
  }
2771
+ else if (entry.type === "summarize") {
2772
+ // Internal housekeeping — summarize older conversation history
2773
+ const result = await this.orchestrator.assign({
2774
+ teammate: entry.teammate,
2775
+ task: entry.task,
2776
+ });
2777
+ // Extract the summary from the agent's output (strip protocol artifacts)
2778
+ const raw = result.rawOutput ?? "";
2779
+ this.conversationSummary = raw
2780
+ .replace(/^TO:\s*\S+\s*\n/im, "")
2781
+ .replace(/^#\s+.+\n*/m, "")
2782
+ .replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
2783
+ .trim();
2784
+ }
2722
2785
  else {
2723
2786
  // btw and debug tasks skip conversation context (not part of main thread)
2724
2787
  const extraContext = entry.type === "btw" || entry.type === "debug"
@@ -2736,6 +2799,8 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2736
2799
  // btw and debug results are not stored in conversation history
2737
2800
  if (entry.type !== "btw" && entry.type !== "debug") {
2738
2801
  this.storeResult(result);
2802
+ // Check if older history needs summarizing
2803
+ this.maybeQueueSummarization();
2739
2804
  }
2740
2805
  if (entry.type === "retro") {
2741
2806
  this.handleRetroResult(result);
@@ -2920,6 +2985,7 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
2920
2985
  }
2921
2986
  async cmdClear() {
2922
2987
  this.conversationHistory.length = 0;
2988
+ this.conversationSummary = "";
2923
2989
  this.lastResult = null;
2924
2990
  this.lastResults.clear();
2925
2991
  this.taskQueue.length = 0;
package/dist/types.d.ts CHANGED
@@ -124,6 +124,10 @@ export type QueueEntry = {
124
124
  type: "debug";
125
125
  teammate: string;
126
126
  task: string;
127
+ } | {
128
+ type: "summarize";
129
+ teammate: string;
130
+ task: string;
127
131
  };
128
132
  /** A registered slash command. */
129
133
  export interface SlashCommand {
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@teammates/cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Agent-agnostic CLI for teammates. Routes tasks, manages handoffs, and plugs into any coding agent backend.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "files": [
9
9
  "dist",
10
- "template"
10
+ "template",
11
+ "scripts"
11
12
  ],
12
13
  "bin": {
13
14
  "teammates": "dist/cli.js"
@@ -18,7 +19,8 @@
18
19
  "test": "vitest run",
19
20
  "test:coverage": "vitest run --coverage",
20
21
  "test:watch": "vitest",
21
- "typecheck": "tsc --noEmit"
22
+ "typecheck": "tsc --noEmit",
23
+ "postinstall": "node scripts/patch-copilot-sdk.cjs"
22
24
  },
23
25
  "keywords": [
24
26
  "teammates",
@@ -31,8 +33,8 @@
31
33
  "license": "MIT",
32
34
  "dependencies": {
33
35
  "@github/copilot-sdk": "^0.1.32",
34
- "@teammates/consolonia": "0.3.3",
35
- "@teammates/recall": "0.3.3",
36
+ "@teammates/consolonia": "0.4.0",
37
+ "@teammates/recall": "0.4.0",
36
38
  "chalk": "^5.6.2",
37
39
  "ora": "^9.3.0"
38
40
  },
@@ -42,9 +44,6 @@
42
44
  "typescript": "^5.5.0",
43
45
  "vitest": "^4.1.0"
44
46
  },
45
- "overrides": {
46
- "vscode-jsonrpc": "9.0.0-next.11"
47
- },
48
47
  "engines": {
49
48
  "node": ">=20.0.0"
50
49
  }
@@ -0,0 +1,30 @@
1
+ // Patches @github/copilot-sdk to fix ESM subpath import for vscode-jsonrpc.
2
+ // The SDK imports "vscode-jsonrpc/node" but vscode-jsonrpc@8.x has no exports
3
+ // map, so Node's ESM resolver fails. This adds the ".js" extension.
4
+ // Remove this patch once copilot-sdk ships a fix upstream.
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const target = path.join(
10
+ __dirname,
11
+ "..",
12
+ "node_modules",
13
+ "@github",
14
+ "copilot-sdk",
15
+ "dist",
16
+ "session.js"
17
+ );
18
+
19
+ if (!fs.existsSync(target)) {
20
+ // copilot-sdk not installed yet (e.g. during workspace linking) — skip
21
+ process.exit(0);
22
+ }
23
+
24
+ let src = fs.readFileSync(target, "utf8");
25
+
26
+ if (src.includes('vscode-jsonrpc/node"') && !src.includes('vscode-jsonrpc/node.js"')) {
27
+ src = src.replace(/vscode-jsonrpc\/node"/g, 'vscode-jsonrpc/node.js"');
28
+ fs.writeFileSync(target, src, "utf8");
29
+ console.log("Patched @github/copilot-sdk: vscode-jsonrpc/node -> vscode-jsonrpc/node.js");
30
+ }