@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 +1 -1
- package/dist/adapter.d.ts +12 -1
- package/dist/adapter.js +127 -125
- package/dist/adapter.test.js +74 -0
- package/dist/adapters/cli-proxy.js +0 -11
- package/dist/adapters/copilot.js +0 -11
- package/dist/cli.js +84 -18
- package/dist/types.d.ts +4 -0
- package/package.json +7 -8
- package/scripts/patch-copilot-sdk.cjs +30 -0
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
|
|
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
|
-
|
|
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(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
//
|
|
80
|
-
if (
|
|
81
|
-
parts.
|
|
82
|
-
|
|
83
|
-
parts.push(
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
166
|
+
lines.push(`- **@${t.name}**: ${t.role}${owns}`);
|
|
105
167
|
}
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
178
|
+
lines.push(`### ${svc.name}\n`);
|
|
179
|
+
lines.push(svc.description);
|
|
180
|
+
lines.push(`\n**Usage:** \`${svc.usage}\`\n`);
|
|
116
181
|
}
|
|
117
|
-
|
|
182
|
+
lines.push("\n---\n");
|
|
183
|
+
parts.push(lines.join("\n"));
|
|
118
184
|
}
|
|
119
|
-
// ── Handoff context (
|
|
185
|
+
// ── Handoff context (required when present) ─────────────────────
|
|
120
186
|
if (options?.handoffContext) {
|
|
121
|
-
parts.push(
|
|
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(
|
|
128
|
-
|
|
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(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
194
|
-
parts.push(
|
|
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
|
/**
|
package/dist/adapter.test.js
CHANGED
|
@@ -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
|
}
|
package/dist/adapters/copilot.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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 (
|
|
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
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teammates/cli",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
35
|
-
"@teammates/recall": "0.
|
|
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
|
+
}
|