@vietor/agent-core 0.7.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/LICENSE +21 -0
- package/README.md +849 -0
- package/dist/create-session.d.ts +4 -0
- package/dist/create-session.js +51 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +5 -0
- package/dist/llm/anthropic.d.ts +14 -0
- package/dist/llm/anthropic.js +200 -0
- package/dist/llm/base.d.ts +10 -0
- package/dist/llm/base.js +12 -0
- package/dist/llm/client.d.ts +4 -0
- package/dist/llm/client.js +61 -0
- package/dist/llm/completions.d.ts +8 -0
- package/dist/llm/completions.js +88 -0
- package/dist/llm/messages.d.ts +50 -0
- package/dist/llm/messages.js +28 -0
- package/dist/llm/responses.d.ts +14 -0
- package/dist/llm/responses.js +130 -0
- package/dist/llm/types.d.ts +40 -0
- package/dist/llm/types.js +1 -0
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +53 -0
- package/dist/mcp/manager.d.ts +18 -0
- package/dist/mcp/manager.js +155 -0
- package/dist/mcp/types.d.ts +26 -0
- package/dist/mcp/types.js +1 -0
- package/dist/runtime/agent.d.ts +56 -0
- package/dist/runtime/agent.js +253 -0
- package/dist/runtime/events.d.ts +69 -0
- package/dist/runtime/events.js +1 -0
- package/dist/runtime/prompts.d.ts +5 -0
- package/dist/runtime/prompts.js +45 -0
- package/dist/runtime/session-messages.d.ts +43 -0
- package/dist/runtime/session-messages.js +174 -0
- package/dist/runtime/session.d.ts +102 -0
- package/dist/runtime/session.js +375 -0
- package/dist/runtime/sub-agent-runner.d.ts +18 -0
- package/dist/runtime/sub-agent-runner.js +26 -0
- package/dist/runtime/timeline.d.ts +21 -0
- package/dist/runtime/timeline.js +146 -0
- package/dist/runtime/todo-store.d.ts +8 -0
- package/dist/runtime/todo-store.js +15 -0
- package/dist/skills/loader.d.ts +6 -0
- package/dist/skills/loader.js +43 -0
- package/dist/tools/ask-user.d.ts +3 -0
- package/dist/tools/ask-user.js +26 -0
- package/dist/tools/file-edit.d.ts +2 -0
- package/dist/tools/file-edit.js +43 -0
- package/dist/tools/file-read.d.ts +2 -0
- package/dist/tools/file-read.js +93 -0
- package/dist/tools/file-write.d.ts +2 -0
- package/dist/tools/file-write.js +25 -0
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +31 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +67 -0
- package/dist/tools/registry.d.ts +30 -0
- package/dist/tools/registry.js +107 -0
- package/dist/tools/shell.d.ts +2 -0
- package/dist/tools/shell.js +57 -0
- package/dist/tools/skill.d.ts +3 -0
- package/dist/tools/skill.js +30 -0
- package/dist/tools/sub-agent.d.ts +7 -0
- package/dist/tools/sub-agent.js +81 -0
- package/dist/tools/todo-write.d.ts +3 -0
- package/dist/tools/todo-write.js +72 -0
- package/dist/tools/types.d.ts +32 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/web-fetch.d.ts +2 -0
- package/dist/tools/web-fetch.js +104 -0
- package/dist/util/async.d.ts +19 -0
- package/dist/util/async.js +93 -0
- package/dist/util/constants.d.ts +25 -0
- package/dist/util/constants.js +27 -0
- package/dist/util/emitter.d.ts +5 -0
- package/dist/util/emitter.js +15 -0
- package/dist/util/file.d.ts +3 -0
- package/dist/util/file.js +19 -0
- package/dist/util/html.d.ts +1 -0
- package/dist/util/html.js +14 -0
- package/dist/util/index.d.ts +7 -0
- package/dist/util/index.js +7 -0
- package/dist/util/net.d.ts +1 -0
- package/dist/util/net.js +31 -0
- package/dist/util/ripgrep.d.ts +10 -0
- package/dist/util/ripgrep.js +34 -0
- package/dist/util/subprocess.d.ts +15 -0
- package/dist/util/subprocess.js +113 -0
- package/dist/util/text.d.ts +15 -0
- package/dist/util/text.js +72 -0
- package/package.json +52 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { isAbortError, mapWithConcurrency, withAbort } from "../util/async.js";
|
|
2
|
+
import { MAX_PARALLEL_TOOL_CALLS, NOT_EXECUTED_PREFIX, SKILL_TOOL_NAME } from "../util/constants.js";
|
|
3
|
+
import { summarizeText, toErrorMessage } from "../util/text.js";
|
|
4
|
+
import { parseToolArgs, toText } from "../llm/messages.js";
|
|
5
|
+
import { SessionMessages } from "./session-messages.js";
|
|
6
|
+
import { COMPACT_PROMPT, renderTodoReminder, renderIncompleteTodoNudge } from "./prompts.js";
|
|
7
|
+
import { toolError } from "../tools/types.js";
|
|
8
|
+
export class Agent {
|
|
9
|
+
llm;
|
|
10
|
+
conversation;
|
|
11
|
+
tools;
|
|
12
|
+
cwd;
|
|
13
|
+
setTodos;
|
|
14
|
+
getTodos;
|
|
15
|
+
stallThreshold;
|
|
16
|
+
maxTurns;
|
|
17
|
+
contextLimit;
|
|
18
|
+
todoSnapshot = [];
|
|
19
|
+
resolveSkill;
|
|
20
|
+
onCompact;
|
|
21
|
+
inputTokens = 0;
|
|
22
|
+
outputTokens = 0;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.llm = opts.llm;
|
|
25
|
+
this.conversation = opts.conversation;
|
|
26
|
+
this.tools = opts.tools;
|
|
27
|
+
this.cwd = opts.cwd;
|
|
28
|
+
this.setTodos = opts.setTodos;
|
|
29
|
+
this.getTodos = opts.getTodos;
|
|
30
|
+
this.stallThreshold = opts.stallThreshold;
|
|
31
|
+
this.maxTurns = opts.maxTurns;
|
|
32
|
+
this.contextLimit = opts.contextLimit;
|
|
33
|
+
this.resolveSkill = opts.resolveSkill;
|
|
34
|
+
this.onCompact = opts.onCompact;
|
|
35
|
+
}
|
|
36
|
+
get contextTokens() {
|
|
37
|
+
return this.conversation.getEstimatedTokens();
|
|
38
|
+
}
|
|
39
|
+
get usage() {
|
|
40
|
+
return { inputTokens: this.inputTokens, outputTokens: this.outputTokens };
|
|
41
|
+
}
|
|
42
|
+
resetUsage() {
|
|
43
|
+
this.inputTokens = 0;
|
|
44
|
+
this.outputTokens = 0;
|
|
45
|
+
}
|
|
46
|
+
get model() {
|
|
47
|
+
return this.llm.model;
|
|
48
|
+
}
|
|
49
|
+
get thinkingEffort() {
|
|
50
|
+
return this.llm.thinkingEffort;
|
|
51
|
+
}
|
|
52
|
+
clear() {
|
|
53
|
+
this.conversation.clear();
|
|
54
|
+
}
|
|
55
|
+
export() {
|
|
56
|
+
return this.conversation.export();
|
|
57
|
+
}
|
|
58
|
+
async compact(onEvent, signal) {
|
|
59
|
+
const history = this.conversation.toLLM().slice(1);
|
|
60
|
+
if (history.length === 0)
|
|
61
|
+
return "ok";
|
|
62
|
+
const request = [...history];
|
|
63
|
+
const todos = this.getTodos();
|
|
64
|
+
if (todos.length) {
|
|
65
|
+
request.push({ role: "user", content: renderTodoReminder(todos) });
|
|
66
|
+
}
|
|
67
|
+
request.push({ role: "user", content: COMPACT_PROMPT });
|
|
68
|
+
const chat = await this.chatOnce({ messages: request, tools: [], thinking: false, onEvent, signal }, () => onEvent?.({ type: "interrupted" }));
|
|
69
|
+
if (!chat.ok)
|
|
70
|
+
return chat.status;
|
|
71
|
+
if (signal?.aborted)
|
|
72
|
+
return "aborted";
|
|
73
|
+
const compactText = toText(chat.message.content);
|
|
74
|
+
if (!compactText) {
|
|
75
|
+
onEvent?.({ type: "error", text: "compact failed: LLM returned no summary text" });
|
|
76
|
+
return "error";
|
|
77
|
+
}
|
|
78
|
+
this.conversation.compact(compactText);
|
|
79
|
+
this.onCompact?.();
|
|
80
|
+
return "ok";
|
|
81
|
+
}
|
|
82
|
+
async run(userInput, onEvent, signal) {
|
|
83
|
+
return this.runTurn({ role: "user", content: userInput }, onEvent, signal);
|
|
84
|
+
}
|
|
85
|
+
async runSkill(skill, onEvent, signal) {
|
|
86
|
+
return this.runTurn({ role: "skill", name: skill.name, content: skill.prompt }, onEvent, signal);
|
|
87
|
+
}
|
|
88
|
+
async runTurn(msg, onEvent, signal) {
|
|
89
|
+
this.conversation.add(msg);
|
|
90
|
+
this.conversation.createSnapshot();
|
|
91
|
+
this.todoSnapshot = this.getTodos();
|
|
92
|
+
let aborted = false;
|
|
93
|
+
const onAbort = () => {
|
|
94
|
+
if (aborted)
|
|
95
|
+
return;
|
|
96
|
+
aborted = true;
|
|
97
|
+
this.conversation.restoreFromSnapshot();
|
|
98
|
+
this.setTodos([...this.todoSnapshot]);
|
|
99
|
+
onEvent?.({ type: "interrupted" });
|
|
100
|
+
};
|
|
101
|
+
let status;
|
|
102
|
+
try {
|
|
103
|
+
status = await withAbort(this.loop(onEvent, signal), signal);
|
|
104
|
+
if (status === "aborted") {
|
|
105
|
+
onAbort();
|
|
106
|
+
}
|
|
107
|
+
return status;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (signal?.aborted || isAbortError(e)) {
|
|
111
|
+
onAbort();
|
|
112
|
+
return "aborted";
|
|
113
|
+
}
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
this.conversation.clearSnapshot();
|
|
118
|
+
this.todoSnapshot = [];
|
|
119
|
+
this.conversation.normalizeInterruptedToolCalls();
|
|
120
|
+
this.conversation.collapseSkills();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async loop(onEvent, signal) {
|
|
124
|
+
let lastSig = "";
|
|
125
|
+
let stall = 0;
|
|
126
|
+
let turns = 0;
|
|
127
|
+
let textOnlyStreak = 0;
|
|
128
|
+
let pendingNudge = "";
|
|
129
|
+
while (true) {
|
|
130
|
+
if (this.conversation.getEstimatedTokens() > this.contextLimit) {
|
|
131
|
+
onEvent?.({ type: "notice", text: "auto-compacting context" });
|
|
132
|
+
const compactStatus = await this.compact((e) => { if (e.type === "error")
|
|
133
|
+
onEvent?.(e); }, signal);
|
|
134
|
+
if (compactStatus !== "ok")
|
|
135
|
+
return compactStatus;
|
|
136
|
+
}
|
|
137
|
+
const messages = this.conversation.toLLM();
|
|
138
|
+
const todos = this.getTodos();
|
|
139
|
+
if (todos.length) {
|
|
140
|
+
messages.push({ role: "user", content: renderTodoReminder(todos) });
|
|
141
|
+
}
|
|
142
|
+
if (pendingNudge) {
|
|
143
|
+
messages.push({ role: "user", content: pendingNudge });
|
|
144
|
+
pendingNudge = "";
|
|
145
|
+
}
|
|
146
|
+
const chat = await this.chatOnce({ messages, tools: this.tools.schemas(), onEvent, signal }, () => { });
|
|
147
|
+
if (!chat.ok)
|
|
148
|
+
return chat.status;
|
|
149
|
+
if (signal?.aborted)
|
|
150
|
+
return "aborted";
|
|
151
|
+
const msg = chat.message;
|
|
152
|
+
this.conversation.add(msg);
|
|
153
|
+
this.conversation.collapseSkills();
|
|
154
|
+
if (!msg.tool_calls?.length) {
|
|
155
|
+
if (todos.length > 0 && todos.some(t => t.status !== "completed")) {
|
|
156
|
+
if (++textOnlyStreak >= this.stallThreshold) {
|
|
157
|
+
onEvent?.({ type: "error", text: `agent stalled: ${textOnlyStreak} text-only responses with incomplete tasks` });
|
|
158
|
+
return "stalled";
|
|
159
|
+
}
|
|
160
|
+
pendingNudge = renderIncompleteTodoNudge(todos);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
return "ok";
|
|
164
|
+
}
|
|
165
|
+
textOnlyStreak = 0;
|
|
166
|
+
const sig = msg.tool_calls
|
|
167
|
+
.map((c) => `${c.function.name}:${c.function.arguments}`)
|
|
168
|
+
.join("|");
|
|
169
|
+
stall = sig === lastSig ? stall + 1 : 1;
|
|
170
|
+
lastSig = sig;
|
|
171
|
+
if (stall >= this.stallThreshold) {
|
|
172
|
+
const reason = `stalled: repeated identical tool calls: ${summarizeText(sig, 200)}`;
|
|
173
|
+
this.resolvePendingToolCalls(msg.tool_calls, reason);
|
|
174
|
+
onEvent?.({ type: "error", text: `agent stalled: ${reason}` });
|
|
175
|
+
return "stalled";
|
|
176
|
+
}
|
|
177
|
+
if (++turns >= this.maxTurns) {
|
|
178
|
+
this.resolvePendingToolCalls(msg.tool_calls, `max turns reached (${this.maxTurns})`);
|
|
179
|
+
onEvent?.({ type: "error", text: `agent exceeded max turns (${this.maxTurns})` });
|
|
180
|
+
return "maxTurns";
|
|
181
|
+
}
|
|
182
|
+
const results = await this.runToolCalls(msg.tool_calls, onEvent, signal);
|
|
183
|
+
if (!results)
|
|
184
|
+
return "aborted";
|
|
185
|
+
for (const r of results) {
|
|
186
|
+
this.conversation.add({ role: "tool", tool_call_id: r.id, content: r.content, resultSummary: r.resultSummary, isError: r.isError });
|
|
187
|
+
}
|
|
188
|
+
for (let i = 0; i < msg.tool_calls.length; i++) {
|
|
189
|
+
const tc = msg.tool_calls[i];
|
|
190
|
+
if (tc.function.name !== SKILL_TOOL_NAME)
|
|
191
|
+
continue;
|
|
192
|
+
const name = results[i].args.name;
|
|
193
|
+
if (typeof name !== "string" || !name)
|
|
194
|
+
continue;
|
|
195
|
+
const skill = this.resolveSkill?.(name);
|
|
196
|
+
if (!skill)
|
|
197
|
+
continue;
|
|
198
|
+
this.conversation.add({ role: "skill", name: skill.name, content: skill.prompt });
|
|
199
|
+
onEvent?.({ type: "skill", name: skill.name });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
resolvePendingToolCalls(calls, reason) {
|
|
204
|
+
for (const tc of calls) {
|
|
205
|
+
this.conversation.add({ role: "tool", tool_call_id: tc.id, content: `${NOT_EXECUTED_PREFIX}${reason})`, isError: true });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async chatOnce(opts, onAbort) {
|
|
209
|
+
try {
|
|
210
|
+
const message = await withAbort(this.llm.chat({
|
|
211
|
+
messages: opts.messages,
|
|
212
|
+
tools: opts.tools,
|
|
213
|
+
thinking: opts.thinking,
|
|
214
|
+
onDelta: (text) => opts.onEvent?.({ type: "assistant_delta", text }),
|
|
215
|
+
onThinking: (text) => opts.onEvent?.({ type: "thinking_delta", text }),
|
|
216
|
+
onRetry: (attempt, max, error) => opts.onEvent?.({ type: "retry", attempt, max, reason: toErrorMessage(error) }),
|
|
217
|
+
onUsage: (inputTokens, outputTokens) => {
|
|
218
|
+
this.inputTokens = inputTokens;
|
|
219
|
+
this.outputTokens = outputTokens;
|
|
220
|
+
},
|
|
221
|
+
signal: opts.signal,
|
|
222
|
+
}), opts.signal);
|
|
223
|
+
return { ok: true, message };
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
if (opts.signal?.aborted || isAbortError(e)) {
|
|
227
|
+
onAbort();
|
|
228
|
+
return { ok: false, status: "aborted" };
|
|
229
|
+
}
|
|
230
|
+
opts.onEvent?.({ type: "error", text: toErrorMessage(e) });
|
|
231
|
+
return { ok: false, status: "error" };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async runToolCalls(calls, onEvent, signal) {
|
|
235
|
+
const results = await mapWithConcurrency(calls, MAX_PARALLEL_TOOL_CALLS, (call) => this.executeToolCall(call, onEvent, signal), signal);
|
|
236
|
+
return signal?.aborted ? null : results;
|
|
237
|
+
}
|
|
238
|
+
async executeToolCall(call, onEvent, signal) {
|
|
239
|
+
const parsed = parseToolArgs(call.function.arguments);
|
|
240
|
+
const args = parsed.ok ? parsed.args : {};
|
|
241
|
+
const argsError = parsed.ok ? undefined : toolError(`invalid arguments: ${parsed.error}`);
|
|
242
|
+
const argsSummary = this.tools.summarizeArgs(call.function.name, args);
|
|
243
|
+
onEvent?.({ type: "tool_start", id: call.id, name: call.function.name, argsSummary });
|
|
244
|
+
const ctx = { signal, cwd: this.cwd };
|
|
245
|
+
const start = performance.now();
|
|
246
|
+
const result = argsError ?? await this.tools.execute(call.function.name, args, ctx);
|
|
247
|
+
const duration = performance.now() - start;
|
|
248
|
+
const resultSummary = this.tools.summarizeResult(call.function.name, result, duration);
|
|
249
|
+
if (!signal?.aborted)
|
|
250
|
+
onEvent?.({ type: "tool_end", id: call.id, result: result.content, isError: result.isError, resultSummary });
|
|
251
|
+
return { id: call.id, content: result.content, resultSummary, isError: result.isError, args };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface RunMetrics {
|
|
2
|
+
running: boolean;
|
|
3
|
+
elapsed: number;
|
|
4
|
+
thinkingElapsed: number;
|
|
5
|
+
replyElapsed: number;
|
|
6
|
+
inputTokens: number;
|
|
7
|
+
outputTokens: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const INITIAL_RUN_METRICS: RunMetrics;
|
|
10
|
+
export type TimelineEvent = {
|
|
11
|
+
type: "user";
|
|
12
|
+
text: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: "skill";
|
|
15
|
+
name: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: "assistant";
|
|
18
|
+
text: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: "tool";
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
argsSummary: string;
|
|
24
|
+
result: string | null;
|
|
25
|
+
isError?: boolean;
|
|
26
|
+
resultSummary?: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: "retry";
|
|
29
|
+
attempt: number;
|
|
30
|
+
max: number;
|
|
31
|
+
reason: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: "error";
|
|
34
|
+
text: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: "interrupted";
|
|
37
|
+
} | {
|
|
38
|
+
type: "question";
|
|
39
|
+
id: string;
|
|
40
|
+
text: string;
|
|
41
|
+
options: string[];
|
|
42
|
+
answer: string | null;
|
|
43
|
+
} | {
|
|
44
|
+
type: "notice";
|
|
45
|
+
text: string;
|
|
46
|
+
};
|
|
47
|
+
export type StreamEvent = {
|
|
48
|
+
type: "assistant_delta";
|
|
49
|
+
text: string;
|
|
50
|
+
} | {
|
|
51
|
+
type: "thinking_delta";
|
|
52
|
+
text: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: "thinking_cleared";
|
|
55
|
+
} | {
|
|
56
|
+
type: "tool_start";
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
argsSummary: string;
|
|
60
|
+
} | {
|
|
61
|
+
type: "tool_end";
|
|
62
|
+
id: string;
|
|
63
|
+
result: string;
|
|
64
|
+
isError?: boolean;
|
|
65
|
+
resultSummary?: string;
|
|
66
|
+
} | ({
|
|
67
|
+
type: "run_metrics";
|
|
68
|
+
} & RunMetrics);
|
|
69
|
+
export type SessionEvent = TimelineEvent | StreamEvent;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const INITIAL_RUN_METRICS = { running: false, elapsed: 0, thinkingElapsed: 0, replyElapsed: 0, inputTokens: 0, outputTokens: 0 };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Todo } from "../tools/types.js";
|
|
2
|
+
export declare const TOOL_USE_PROMPT: string;
|
|
3
|
+
export declare const COMPACT_PROMPT: string;
|
|
4
|
+
export declare function renderTodoReminder(todos: readonly Todo[]): string;
|
|
5
|
+
export declare function renderIncompleteTodoNudge(todos: readonly Todo[]): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const TOOL_USE_PROMPT = [
|
|
2
|
+
"Tool-Use Guidelines:",
|
|
3
|
+
"The user's instructions in the preceding sections take precedence over these defaults.",
|
|
4
|
+
"",
|
|
5
|
+
"- Prefer emitting independent tool calls together in one turn so they run concurrently (2-8 calls per turn is normal); do not batch calls that depend on a prior result or that modify the same file or resource.",
|
|
6
|
+
"- Scale planning: when a task involves many independent items (queries, reads, searches), estimate the count up front and pick a strategy: a few — do them directly; more — spread over a few turns with several calls per turn.",
|
|
7
|
+
"- A run has a limited budget of tool-calling turns. If a task needs far more turns than the budget, do not work item-by-item in the main loop — narrow the scope.",
|
|
8
|
+
"- For file operations (read/write/edit/glob/grep) and fetching URLs, use the dedicated tool. Fall back to Shell only when no dedicated tool covers the task and Shell is available. A runtime error does not make Shell the fallback; do not retry that same operation through Shell.",
|
|
9
|
+
"- If a tool call fails, read the error, adjust the arguments or approach, and continue; do not repeat the identical call and do not abandon the task over a single failure.",
|
|
10
|
+
].join("\n");
|
|
11
|
+
export const COMPACT_PROMPT = [
|
|
12
|
+
"Summarize the conversation above for context continuation. Preserve:\n",
|
|
13
|
+
"1. Primary goal, sub-goals, constraints, acceptance criteria.\n",
|
|
14
|
+
"2. Decisions and rationale (including rejected approaches).\n",
|
|
15
|
+
"3. Files (paths, signatures, config values, key code snippets).\n",
|
|
16
|
+
"4. Tool calls and relevant results (commands, search hits, test output).\n",
|
|
17
|
+
"5. Errors/failures and how they were resolved.\n",
|
|
18
|
+
"6. Current progress: what is done, verified, and in-progress state.\n",
|
|
19
|
+
"7. Pending tasks, open questions, concrete next step.\n",
|
|
20
|
+
"Discard: completed small talk, verbose tool outputs already absorbed, resolved dead ends. The full conversation will be replaced by this summary, so anything omitted is lost — keep only what the next turn needs to continue without re-reading history.\n",
|
|
21
|
+
"Concise but thorough; keep technical specifics; use the conversation language. Aim for roughly 500-1000 tokens — under 1% of the original length, never a generic recap; technical specifics over prose. ",
|
|
22
|
+
"Start with \"Summary of conversation so far\":",
|
|
23
|
+
].join("");
|
|
24
|
+
const STATUS_GLYPHS = {
|
|
25
|
+
pending: "○",
|
|
26
|
+
inProgress: "◐",
|
|
27
|
+
completed: "✓",
|
|
28
|
+
};
|
|
29
|
+
export function renderTodoReminder(todos) {
|
|
30
|
+
const items = todos.map((t) => {
|
|
31
|
+
return `${STATUS_GLYPHS[t.status]} ${t.content}`;
|
|
32
|
+
});
|
|
33
|
+
const focus = todos.find((t) => t.status === "inProgress");
|
|
34
|
+
const focusLine = focus ? ` Current focus: ${focus.content}` : "";
|
|
35
|
+
const incomplete = todos.filter(t => t.status !== "completed");
|
|
36
|
+
const warning = incomplete.length > 0
|
|
37
|
+
? ` ${incomplete.length} incomplete. You MUST complete EVERY task before your final text-only response — update status via TodoWrite after each task finishes.`
|
|
38
|
+
: "";
|
|
39
|
+
return `<system-reminder>Tasks: ${items.join(" | ")}${focusLine}${warning}</system-reminder>`;
|
|
40
|
+
}
|
|
41
|
+
export function renderIncompleteTodoNudge(todos) {
|
|
42
|
+
const incomplete = todos.filter(t => t.status !== "completed");
|
|
43
|
+
const names = incomplete.map(t => `"${t.content}"`).join(", ");
|
|
44
|
+
return `<system-reminder>STOP! You have ${incomplete.length} incomplete task(s): ${names}. Use tools to complete them. Call TodoWrite to mark each one completed before your final text response.</system-reminder>`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type LLMAssistantMessage, type LLMMessage } from "../llm/messages.js";
|
|
2
|
+
export type SessionMessage = {
|
|
3
|
+
role: "system";
|
|
4
|
+
content: string;
|
|
5
|
+
} | {
|
|
6
|
+
role: "user";
|
|
7
|
+
content: string;
|
|
8
|
+
} | {
|
|
9
|
+
role: "skill";
|
|
10
|
+
name: string;
|
|
11
|
+
content: string;
|
|
12
|
+
} | LLMAssistantMessage | {
|
|
13
|
+
role: "tool";
|
|
14
|
+
tool_call_id: string;
|
|
15
|
+
content: string;
|
|
16
|
+
resultSummary?: string;
|
|
17
|
+
isError?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function lastAssistantText(messages: SessionMessage[]): string;
|
|
20
|
+
export declare class SessionMessages {
|
|
21
|
+
private system;
|
|
22
|
+
private readonly systemEstimateTokens;
|
|
23
|
+
private messages;
|
|
24
|
+
private estimatedTokens;
|
|
25
|
+
private collapsedCount;
|
|
26
|
+
private snapshot?;
|
|
27
|
+
private llmCache;
|
|
28
|
+
constructor(system: string);
|
|
29
|
+
getEstimatedTokens(): number;
|
|
30
|
+
add(msg: SessionMessage): void;
|
|
31
|
+
toLLM(): LLMMessage[];
|
|
32
|
+
export(): SessionMessage[];
|
|
33
|
+
import(messages: SessionMessage[]): void;
|
|
34
|
+
normalizeInterruptedToolCalls(): void;
|
|
35
|
+
clear(): void;
|
|
36
|
+
compact(summary: string): void;
|
|
37
|
+
private resetMessages;
|
|
38
|
+
collapseSkills(): void;
|
|
39
|
+
private collapseOne;
|
|
40
|
+
createSnapshot(): void;
|
|
41
|
+
restoreFromSnapshot(): void;
|
|
42
|
+
clearSnapshot(): void;
|
|
43
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { toText } from "../llm/messages.js";
|
|
2
|
+
import { INTERRUPTED_TOOL_CONTENT } from "../util/constants.js";
|
|
3
|
+
function estimateTokens(text) {
|
|
4
|
+
if (!text)
|
|
5
|
+
return 0;
|
|
6
|
+
let tokens = 0;
|
|
7
|
+
for (let i = 0; i < text.length; i++) {
|
|
8
|
+
tokens += text.charCodeAt(i) < 0x80 ? 1 : 4;
|
|
9
|
+
}
|
|
10
|
+
return Math.round(tokens / 4);
|
|
11
|
+
}
|
|
12
|
+
export function lastAssistantText(messages) {
|
|
13
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
14
|
+
const m = messages[i];
|
|
15
|
+
if (m.role !== "assistant")
|
|
16
|
+
continue;
|
|
17
|
+
const text = toText(m.content);
|
|
18
|
+
if (text)
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
function messageText(msg) {
|
|
24
|
+
const parts = [];
|
|
25
|
+
const t = toText(msg.content);
|
|
26
|
+
if (t)
|
|
27
|
+
parts.push(t);
|
|
28
|
+
if ("tool_calls" in msg && msg.tool_calls) {
|
|
29
|
+
for (const tc of msg.tool_calls) {
|
|
30
|
+
if (tc.function?.name)
|
|
31
|
+
parts.push(tc.function.name);
|
|
32
|
+
if (tc.function?.arguments)
|
|
33
|
+
parts.push(tc.function.arguments);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if ("thinking" in msg && msg.thinking) {
|
|
37
|
+
for (const t of msg.thinking) {
|
|
38
|
+
if (t.type === "thinking")
|
|
39
|
+
parts.push(t.thinking);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return parts.join(" ");
|
|
43
|
+
}
|
|
44
|
+
function toLLMMessage(m) {
|
|
45
|
+
if (m.role === "tool")
|
|
46
|
+
return { role: "tool", tool_call_id: m.tool_call_id, content: m.content };
|
|
47
|
+
if (m.role === "skill")
|
|
48
|
+
return { role: "user", name: m.name, content: m.content };
|
|
49
|
+
return m;
|
|
50
|
+
}
|
|
51
|
+
export class SessionMessages {
|
|
52
|
+
system;
|
|
53
|
+
systemEstimateTokens;
|
|
54
|
+
messages = [];
|
|
55
|
+
estimatedTokens = 0;
|
|
56
|
+
collapsedCount = 0;
|
|
57
|
+
snapshot;
|
|
58
|
+
llmCache = null;
|
|
59
|
+
constructor(system) {
|
|
60
|
+
this.system = system;
|
|
61
|
+
this.systemEstimateTokens = estimateTokens(system);
|
|
62
|
+
this.estimatedTokens = this.systemEstimateTokens;
|
|
63
|
+
}
|
|
64
|
+
getEstimatedTokens() {
|
|
65
|
+
return this.estimatedTokens;
|
|
66
|
+
}
|
|
67
|
+
add(msg) {
|
|
68
|
+
this.messages.push(msg);
|
|
69
|
+
this.estimatedTokens += estimateTokens(messageText(msg));
|
|
70
|
+
if (this.llmCache) {
|
|
71
|
+
this.llmCache.push(toLLMMessage(msg));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
toLLM() {
|
|
75
|
+
if (this.llmCache) {
|
|
76
|
+
return this.llmCache.slice();
|
|
77
|
+
}
|
|
78
|
+
const result = new Array(this.messages.length + 1);
|
|
79
|
+
result[0] = { role: "system", content: this.system };
|
|
80
|
+
for (let i = 0; i < this.messages.length; i++) {
|
|
81
|
+
result[i + 1] = toLLMMessage(this.messages[i]);
|
|
82
|
+
}
|
|
83
|
+
this.llmCache = result;
|
|
84
|
+
return result.slice();
|
|
85
|
+
}
|
|
86
|
+
export() {
|
|
87
|
+
return this.messages.slice();
|
|
88
|
+
}
|
|
89
|
+
import(messages) {
|
|
90
|
+
this.resetMessages(messages.slice(), messages.reduce((sum, m) => sum + estimateTokens(messageText(m)), 0));
|
|
91
|
+
this.normalizeInterruptedToolCalls();
|
|
92
|
+
}
|
|
93
|
+
normalizeInterruptedToolCalls() {
|
|
94
|
+
const out = [];
|
|
95
|
+
let changed = false;
|
|
96
|
+
let addedTokens = 0;
|
|
97
|
+
for (let i = 0; i < this.messages.length; i++) {
|
|
98
|
+
const m = this.messages[i];
|
|
99
|
+
out.push(m);
|
|
100
|
+
if (m.role !== "assistant" || !m.tool_calls?.length)
|
|
101
|
+
continue;
|
|
102
|
+
const satisfied = new Set();
|
|
103
|
+
while (i + 1 < this.messages.length) {
|
|
104
|
+
const next = this.messages[i + 1];
|
|
105
|
+
if (next.role !== "tool")
|
|
106
|
+
break;
|
|
107
|
+
i++;
|
|
108
|
+
out.push(next);
|
|
109
|
+
satisfied.add(next.tool_call_id);
|
|
110
|
+
}
|
|
111
|
+
for (const tc of m.tool_calls) {
|
|
112
|
+
if (satisfied.has(tc.id))
|
|
113
|
+
continue;
|
|
114
|
+
out.push({ role: "tool", tool_call_id: tc.id, content: INTERRUPTED_TOOL_CONTENT });
|
|
115
|
+
changed = true;
|
|
116
|
+
addedTokens += estimateTokens(INTERRUPTED_TOOL_CONTENT);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (changed) {
|
|
120
|
+
this.messages = out;
|
|
121
|
+
this.estimatedTokens += addedTokens;
|
|
122
|
+
this.llmCache = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
clear() {
|
|
126
|
+
this.resetMessages([], 0);
|
|
127
|
+
}
|
|
128
|
+
compact(summary) {
|
|
129
|
+
this.resetMessages([{ role: "assistant", content: summary }], estimateTokens(summary), true);
|
|
130
|
+
}
|
|
131
|
+
resetMessages(messages, extraTokens, keepSnapshot = false) {
|
|
132
|
+
this.messages = messages;
|
|
133
|
+
this.estimatedTokens = this.systemEstimateTokens + extraTokens;
|
|
134
|
+
this.collapsedCount = 0;
|
|
135
|
+
if (!keepSnapshot)
|
|
136
|
+
this.clearSnapshot();
|
|
137
|
+
this.llmCache = null;
|
|
138
|
+
}
|
|
139
|
+
collapseSkills() {
|
|
140
|
+
for (let i = this.collapsedCount; i < this.messages.length; i++) {
|
|
141
|
+
const m = this.messages[i];
|
|
142
|
+
if (m.role === "skill")
|
|
143
|
+
this.collapseOne(i, m);
|
|
144
|
+
}
|
|
145
|
+
this.collapsedCount = this.messages.length;
|
|
146
|
+
}
|
|
147
|
+
collapseOne(index, m) {
|
|
148
|
+
const before = estimateTokens(messageText(m));
|
|
149
|
+
const collapsed = `<skill "${m.name}" invoked - its instructions were followed above>`;
|
|
150
|
+
this.messages[index] = { ...m, content: collapsed };
|
|
151
|
+
this.estimatedTokens += estimateTokens(collapsed) - before;
|
|
152
|
+
this.llmCache = null;
|
|
153
|
+
}
|
|
154
|
+
createSnapshot() {
|
|
155
|
+
this.snapshot = {
|
|
156
|
+
messages: this.messages.slice(),
|
|
157
|
+
estimatedTokens: this.estimatedTokens,
|
|
158
|
+
collapsedCount: this.collapsedCount,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
restoreFromSnapshot() {
|
|
162
|
+
const snap = this.snapshot;
|
|
163
|
+
if (snap) {
|
|
164
|
+
this.messages = snap.messages.slice();
|
|
165
|
+
this.estimatedTokens = snap.estimatedTokens;
|
|
166
|
+
this.collapsedCount = snap.collapsedCount;
|
|
167
|
+
this.clearSnapshot();
|
|
168
|
+
this.llmCache = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
clearSnapshot() {
|
|
172
|
+
this.snapshot = undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|