@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,102 @@
|
|
|
1
|
+
import type { LLMClient, LLMConfig } from "../llm/types.js";
|
|
2
|
+
import type { MCPServerManager } from "../mcp/manager.js";
|
|
3
|
+
import type { MCPServerConfig, MCPServerInfo } from "../mcp/types.js";
|
|
4
|
+
import type { Skill } from "../skills/loader.js";
|
|
5
|
+
import { type BuiltinToolsOptions, type ToolRegistry } from "../tools/registry.js";
|
|
6
|
+
import type { Todo, Tool } from "../tools/types.js";
|
|
7
|
+
import { type SessionEvent, type TimelineEvent } from "./events.js";
|
|
8
|
+
import type { MCPClientInfo } from "../mcp/types.js";
|
|
9
|
+
import { type RunStatus } from "./agent.js";
|
|
10
|
+
import { type SessionMessage } from "./session-messages.js";
|
|
11
|
+
export interface SessionOptions {
|
|
12
|
+
systemPrompt: string;
|
|
13
|
+
llm: LLMConfig;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
tools?: Tool[];
|
|
16
|
+
skills?: Skill[];
|
|
17
|
+
mcpServers?: Record<string, MCPServerConfig>;
|
|
18
|
+
builtInTools?: BuiltinToolsOptions | false;
|
|
19
|
+
clientInfo?: MCPClientInfo;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
maxTurns?: number;
|
|
22
|
+
stallThreshold?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SessionDeps extends Omit<SessionOptions, "llm" | "tools" | "mcpServers"> {
|
|
25
|
+
llm: LLMClient;
|
|
26
|
+
tools: ToolRegistry;
|
|
27
|
+
mcp: MCPServerManager;
|
|
28
|
+
contextLimit: number;
|
|
29
|
+
}
|
|
30
|
+
export interface SessionView {
|
|
31
|
+
timeline: readonly TimelineEvent[];
|
|
32
|
+
todos: readonly Todo[];
|
|
33
|
+
}
|
|
34
|
+
export interface SessionState {
|
|
35
|
+
messages: SessionMessage[];
|
|
36
|
+
todos: Todo[];
|
|
37
|
+
}
|
|
38
|
+
export interface PromptResult {
|
|
39
|
+
status: RunStatus;
|
|
40
|
+
reply: string;
|
|
41
|
+
}
|
|
42
|
+
export declare class SessionBusyError extends Error {
|
|
43
|
+
constructor();
|
|
44
|
+
}
|
|
45
|
+
export declare class Session {
|
|
46
|
+
private agent;
|
|
47
|
+
private mcp;
|
|
48
|
+
private skillsMap;
|
|
49
|
+
private resolveSkill;
|
|
50
|
+
private timelineStore;
|
|
51
|
+
private todoStore;
|
|
52
|
+
private stream;
|
|
53
|
+
private questionQueue;
|
|
54
|
+
private runTimer;
|
|
55
|
+
private runMetrics;
|
|
56
|
+
private abortController;
|
|
57
|
+
private timer;
|
|
58
|
+
private conversation;
|
|
59
|
+
private tools;
|
|
60
|
+
readonly cwd: string;
|
|
61
|
+
readonly sessionId: string;
|
|
62
|
+
private viewCache;
|
|
63
|
+
private eventListeners;
|
|
64
|
+
subscribe: (listener: () => void) => (() => void);
|
|
65
|
+
getSnapshot: () => SessionView;
|
|
66
|
+
onEvent: (listener: (e: SessionEvent) => void) => (() => void);
|
|
67
|
+
addNotice: (text: string) => void;
|
|
68
|
+
addError: (text: string) => void;
|
|
69
|
+
runSkill: (name: string) => Promise<boolean>;
|
|
70
|
+
private emit;
|
|
71
|
+
get pendingQuestion(): Extract<TimelineEvent, {
|
|
72
|
+
type: "question";
|
|
73
|
+
}> | undefined;
|
|
74
|
+
get running(): boolean;
|
|
75
|
+
get contextTokens(): number;
|
|
76
|
+
get model(): string;
|
|
77
|
+
get thinkingEffort(): import("../llm/types.js").LLMThinkingEffort;
|
|
78
|
+
get contextLimit(): number;
|
|
79
|
+
get mcpServers(): readonly MCPServerInfo[];
|
|
80
|
+
get skills(): readonly Skill[];
|
|
81
|
+
constructor(deps: SessionDeps);
|
|
82
|
+
private start;
|
|
83
|
+
private run;
|
|
84
|
+
private clearCompletedTodos;
|
|
85
|
+
private emitRunMetrics;
|
|
86
|
+
private handleEvent;
|
|
87
|
+
private flushStreaming;
|
|
88
|
+
private flushThinking;
|
|
89
|
+
connectMCP(servers: Record<string, MCPServerConfig>): Promise<void>;
|
|
90
|
+
dispose(): void;
|
|
91
|
+
private rejectIfBusy;
|
|
92
|
+
clear(): void;
|
|
93
|
+
export(): SessionMessage[];
|
|
94
|
+
exportState(): SessionState;
|
|
95
|
+
private rebuildTimeline;
|
|
96
|
+
importState(state: SessionState): void;
|
|
97
|
+
compact(): Promise<RunStatus>;
|
|
98
|
+
abort(): void;
|
|
99
|
+
submitAnswer(id: string, answer: string): void;
|
|
100
|
+
prompt(text: string): Promise<PromptResult>;
|
|
101
|
+
private ask;
|
|
102
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isAbortError } from "../util/async.js";
|
|
3
|
+
import { toErrorMessage } from "../util/text.js";
|
|
4
|
+
import { DEFAULT_MAX_TURNS, DEFAULT_STALL_THRESHOLD } from "../util/constants.js";
|
|
5
|
+
import { registerBuiltinTools } from "../tools/registry.js";
|
|
6
|
+
import { INITIAL_RUN_METRICS } from "./events.js";
|
|
7
|
+
import { Agent } from "./agent.js";
|
|
8
|
+
import { SessionMessages } from "./session-messages.js";
|
|
9
|
+
import { Emitter } from "../util/emitter.js";
|
|
10
|
+
import { TimelineStore, toTimelineEntries } from "./timeline.js";
|
|
11
|
+
import { TodoStore } from "./todo-store.js";
|
|
12
|
+
import { createSubAgentRunner } from "./sub-agent-runner.js";
|
|
13
|
+
class StreamBuffer {
|
|
14
|
+
streamingText = "";
|
|
15
|
+
thinkingText = "";
|
|
16
|
+
replyStart = null;
|
|
17
|
+
lastReplyText = "";
|
|
18
|
+
get reply() {
|
|
19
|
+
return this.lastReplyText;
|
|
20
|
+
}
|
|
21
|
+
get firstReplyAt() {
|
|
22
|
+
return this.replyStart;
|
|
23
|
+
}
|
|
24
|
+
begin() {
|
|
25
|
+
this.streamingText = "";
|
|
26
|
+
this.thinkingText = "";
|
|
27
|
+
this.replyStart = null;
|
|
28
|
+
this.lastReplyText = "";
|
|
29
|
+
}
|
|
30
|
+
push(text) {
|
|
31
|
+
if (this.replyStart === null)
|
|
32
|
+
this.replyStart = Date.now();
|
|
33
|
+
this.streamingText += text;
|
|
34
|
+
}
|
|
35
|
+
pushThinking(text) {
|
|
36
|
+
this.thinkingText += text;
|
|
37
|
+
}
|
|
38
|
+
flush() {
|
|
39
|
+
const assistant = this.flushAssistant();
|
|
40
|
+
const thinkingCleared = this.flushThinking();
|
|
41
|
+
return { assistant, thinkingCleared };
|
|
42
|
+
}
|
|
43
|
+
flushForRetry() {
|
|
44
|
+
this.streamingText = "";
|
|
45
|
+
return { thinkingCleared: this.flushThinking() };
|
|
46
|
+
}
|
|
47
|
+
interrupt() {
|
|
48
|
+
this.lastReplyText = this.streamingText;
|
|
49
|
+
this.streamingText = "";
|
|
50
|
+
return this.flushThinking();
|
|
51
|
+
}
|
|
52
|
+
discardStreamedText() {
|
|
53
|
+
this.streamingText = "";
|
|
54
|
+
}
|
|
55
|
+
flushThinking() {
|
|
56
|
+
if (!this.thinkingText)
|
|
57
|
+
return false;
|
|
58
|
+
this.thinkingText = "";
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
flushAssistant() {
|
|
62
|
+
if (!this.streamingText)
|
|
63
|
+
return null;
|
|
64
|
+
this.lastReplyText = this.streamingText;
|
|
65
|
+
const text = this.streamingText;
|
|
66
|
+
this.streamingText = "";
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
class QuestionQueue {
|
|
71
|
+
questionSeq = 0;
|
|
72
|
+
resolvers = new Map();
|
|
73
|
+
ask() {
|
|
74
|
+
const id = `q${++this.questionSeq}`;
|
|
75
|
+
const promise = new Promise((resolve) => {
|
|
76
|
+
this.resolvers.set(id, resolve);
|
|
77
|
+
});
|
|
78
|
+
return { id, promise };
|
|
79
|
+
}
|
|
80
|
+
submit(id, answer) {
|
|
81
|
+
const resolve = this.resolvers.get(id);
|
|
82
|
+
if (resolve) {
|
|
83
|
+
this.resolvers.delete(id);
|
|
84
|
+
resolve(answer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
resolveAll(answer) {
|
|
88
|
+
const ids = [...this.resolvers.keys()];
|
|
89
|
+
for (const id of ids) {
|
|
90
|
+
this.submit(id, answer);
|
|
91
|
+
}
|
|
92
|
+
return ids;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
class RunTimer {
|
|
96
|
+
startTime = 0;
|
|
97
|
+
begin() {
|
|
98
|
+
this.startTime = Date.now();
|
|
99
|
+
}
|
|
100
|
+
metrics(usage, firstReplyAt, running) {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
const elapsed = Math.floor((now - this.startTime) / 1000);
|
|
103
|
+
if (firstReplyAt === null) {
|
|
104
|
+
return { running, elapsed, thinkingElapsed: elapsed, replyElapsed: 0, ...usage };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
running,
|
|
108
|
+
elapsed,
|
|
109
|
+
thinkingElapsed: Math.floor((firstReplyAt - this.startTime) / 1000),
|
|
110
|
+
replyElapsed: Math.floor((now - firstReplyAt) / 1000),
|
|
111
|
+
...usage,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export class SessionBusyError extends Error {
|
|
116
|
+
constructor() {
|
|
117
|
+
super("session is busy; another run is in progress");
|
|
118
|
+
this.name = "SessionBusyError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export class Session {
|
|
122
|
+
agent;
|
|
123
|
+
mcp;
|
|
124
|
+
skillsMap = new Map();
|
|
125
|
+
resolveSkill = (name) => this.skillsMap.get(name);
|
|
126
|
+
timelineStore = new TimelineStore();
|
|
127
|
+
todoStore = new TodoStore();
|
|
128
|
+
stream = new StreamBuffer();
|
|
129
|
+
questionQueue = new QuestionQueue();
|
|
130
|
+
runTimer = new RunTimer();
|
|
131
|
+
runMetrics = INITIAL_RUN_METRICS;
|
|
132
|
+
abortController = null;
|
|
133
|
+
timer;
|
|
134
|
+
conversation;
|
|
135
|
+
tools;
|
|
136
|
+
cwd;
|
|
137
|
+
sessionId;
|
|
138
|
+
viewCache = null;
|
|
139
|
+
eventListeners = new Emitter();
|
|
140
|
+
subscribe = (listener) => {
|
|
141
|
+
const on = () => { this.viewCache = null; listener(); };
|
|
142
|
+
const unsubscribeTimeline = this.timelineStore.subscribe(on);
|
|
143
|
+
const unsubscribeTodos = this.todoStore.subscribe(on);
|
|
144
|
+
return () => { unsubscribeTimeline(); unsubscribeTodos(); };
|
|
145
|
+
};
|
|
146
|
+
getSnapshot = () => {
|
|
147
|
+
if (!this.viewCache) {
|
|
148
|
+
this.viewCache = { timeline: this.timelineStore.all, todos: this.todoStore.all };
|
|
149
|
+
}
|
|
150
|
+
return this.viewCache;
|
|
151
|
+
};
|
|
152
|
+
onEvent = (listener) => this.eventListeners.subscribe(listener);
|
|
153
|
+
addNotice = (text) => {
|
|
154
|
+
this.emit({ type: "notice", text });
|
|
155
|
+
};
|
|
156
|
+
addError = (text) => {
|
|
157
|
+
this.emit({ type: "error", text });
|
|
158
|
+
};
|
|
159
|
+
runSkill = async (name) => {
|
|
160
|
+
this.rejectIfBusy();
|
|
161
|
+
const skill = this.skillsMap.get(name);
|
|
162
|
+
if (!skill)
|
|
163
|
+
return false;
|
|
164
|
+
await this.start({ type: "skill", name: skill.name }, (signal) => this.agent.runSkill(skill, this.handleEvent, signal));
|
|
165
|
+
return true;
|
|
166
|
+
};
|
|
167
|
+
emit = (e) => {
|
|
168
|
+
this.timelineStore.applyEvent(e);
|
|
169
|
+
this.eventListeners.notify(e);
|
|
170
|
+
};
|
|
171
|
+
get pendingQuestion() {
|
|
172
|
+
return this.timelineStore.latestUnansweredQuestion;
|
|
173
|
+
}
|
|
174
|
+
get running() {
|
|
175
|
+
return this.abortController !== null;
|
|
176
|
+
}
|
|
177
|
+
get contextTokens() {
|
|
178
|
+
return this.agent.contextTokens;
|
|
179
|
+
}
|
|
180
|
+
get model() {
|
|
181
|
+
return this.agent.model;
|
|
182
|
+
}
|
|
183
|
+
get thinkingEffort() {
|
|
184
|
+
return this.agent.thinkingEffort;
|
|
185
|
+
}
|
|
186
|
+
get contextLimit() {
|
|
187
|
+
return this.agent.contextLimit;
|
|
188
|
+
}
|
|
189
|
+
get mcpServers() {
|
|
190
|
+
return this.mcp.list();
|
|
191
|
+
}
|
|
192
|
+
get skills() {
|
|
193
|
+
return [...this.skillsMap.values()];
|
|
194
|
+
}
|
|
195
|
+
constructor(deps) {
|
|
196
|
+
this.conversation = new SessionMessages(deps.systemPrompt);
|
|
197
|
+
this.tools = deps.tools;
|
|
198
|
+
this.cwd = deps.cwd ?? process.cwd();
|
|
199
|
+
this.sessionId = deps.sessionId ?? randomUUID();
|
|
200
|
+
for (const s of deps.skills ?? [])
|
|
201
|
+
this.skillsMap.set(s.name, s);
|
|
202
|
+
registerBuiltinTools(this.tools, deps.builtInTools, {
|
|
203
|
+
ask: (q, o) => this.ask(q, o),
|
|
204
|
+
setTodos: (t) => this.todoStore.set(t),
|
|
205
|
+
resolveSkill: deps.skills?.length ? this.resolveSkill : undefined,
|
|
206
|
+
subAgent: {
|
|
207
|
+
runSubAgent: (systemPrompt, task, signal) => createSubAgentRunner({
|
|
208
|
+
llm: deps.llm,
|
|
209
|
+
tools: this.tools,
|
|
210
|
+
cwd: this.cwd,
|
|
211
|
+
maxTurns: deps.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
212
|
+
stallThreshold: deps.stallThreshold ?? DEFAULT_STALL_THRESHOLD,
|
|
213
|
+
contextLimit: deps.contextLimit,
|
|
214
|
+
})(systemPrompt, task, signal),
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
this.agent = new Agent({
|
|
218
|
+
llm: deps.llm,
|
|
219
|
+
conversation: this.conversation,
|
|
220
|
+
tools: this.tools,
|
|
221
|
+
cwd: this.cwd,
|
|
222
|
+
setTodos: (t) => this.todoStore.set(t),
|
|
223
|
+
getTodos: () => this.todoStore.all,
|
|
224
|
+
stallThreshold: deps.stallThreshold ?? DEFAULT_STALL_THRESHOLD,
|
|
225
|
+
maxTurns: deps.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
226
|
+
contextLimit: deps.contextLimit,
|
|
227
|
+
resolveSkill: this.resolveSkill,
|
|
228
|
+
onCompact: () => {
|
|
229
|
+
this.stream.discardStreamedText();
|
|
230
|
+
this.rebuildTimeline();
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
this.mcp = deps.mcp;
|
|
234
|
+
}
|
|
235
|
+
start(event, runFn) {
|
|
236
|
+
this.emit(event);
|
|
237
|
+
return this.run(runFn);
|
|
238
|
+
}
|
|
239
|
+
async run(runFn) {
|
|
240
|
+
this.stream.begin();
|
|
241
|
+
this.runTimer.begin();
|
|
242
|
+
this.abortController = new AbortController();
|
|
243
|
+
this.runMetrics = { ...INITIAL_RUN_METRICS, running: true };
|
|
244
|
+
this.agent.resetUsage();
|
|
245
|
+
this.emitRunMetrics();
|
|
246
|
+
this.timer = setInterval(() => {
|
|
247
|
+
this.runMetrics = this.runTimer.metrics(this.agent.usage, this.stream.firstReplyAt, true);
|
|
248
|
+
this.emitRunMetrics();
|
|
249
|
+
}, 1000);
|
|
250
|
+
let status = "ok";
|
|
251
|
+
try {
|
|
252
|
+
status = await runFn(this.abortController.signal);
|
|
253
|
+
this.flushStreaming();
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
status = isAbortError(e) ? "aborted" : "error";
|
|
257
|
+
this.flushStreaming();
|
|
258
|
+
if (status !== "aborted") {
|
|
259
|
+
this.emit({ type: "error", text: toErrorMessage(e) });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
clearInterval(this.timer);
|
|
264
|
+
this.timer = undefined;
|
|
265
|
+
this.abortController = null;
|
|
266
|
+
this.timelineStore.markPendingToolsAborted();
|
|
267
|
+
this.runMetrics = this.runTimer.metrics(this.agent.usage, this.stream.firstReplyAt, false);
|
|
268
|
+
this.emitRunMetrics();
|
|
269
|
+
this.flushThinking();
|
|
270
|
+
this.clearCompletedTodos();
|
|
271
|
+
}
|
|
272
|
+
return { status, reply: this.stream.reply };
|
|
273
|
+
}
|
|
274
|
+
clearCompletedTodos() {
|
|
275
|
+
if (this.todoStore.all.length > 0 && this.todoStore.all.every((t) => t.status === "completed")) {
|
|
276
|
+
this.todoStore.set([]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
emitRunMetrics() {
|
|
280
|
+
this.emit({ type: "run_metrics", ...this.runMetrics });
|
|
281
|
+
}
|
|
282
|
+
handleEvent = (e) => {
|
|
283
|
+
switch (e.type) {
|
|
284
|
+
case "assistant_delta":
|
|
285
|
+
this.stream.push(e.text);
|
|
286
|
+
break;
|
|
287
|
+
case "thinking_delta":
|
|
288
|
+
this.stream.pushThinking(e.text);
|
|
289
|
+
break;
|
|
290
|
+
case "retry": {
|
|
291
|
+
const { thinkingCleared } = this.stream.flushForRetry();
|
|
292
|
+
if (thinkingCleared)
|
|
293
|
+
this.emit({ type: "thinking_cleared" });
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case "tool_start":
|
|
297
|
+
case "error":
|
|
298
|
+
this.flushStreaming();
|
|
299
|
+
break;
|
|
300
|
+
case "interrupted":
|
|
301
|
+
if (this.stream.interrupt())
|
|
302
|
+
this.emit({ type: "thinking_cleared" });
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
this.emit(e);
|
|
306
|
+
};
|
|
307
|
+
flushStreaming() {
|
|
308
|
+
const { assistant, thinkingCleared } = this.stream.flush();
|
|
309
|
+
if (assistant !== null)
|
|
310
|
+
this.emit({ type: "assistant", text: assistant });
|
|
311
|
+
if (thinkingCleared)
|
|
312
|
+
this.emit({ type: "thinking_cleared" });
|
|
313
|
+
}
|
|
314
|
+
flushThinking() {
|
|
315
|
+
if (this.stream.flushThinking())
|
|
316
|
+
this.emit({ type: "thinking_cleared" });
|
|
317
|
+
}
|
|
318
|
+
async connectMCP(servers) {
|
|
319
|
+
await this.mcp.connect(servers);
|
|
320
|
+
}
|
|
321
|
+
dispose() {
|
|
322
|
+
this.abort();
|
|
323
|
+
this.mcp.kill();
|
|
324
|
+
}
|
|
325
|
+
rejectIfBusy() {
|
|
326
|
+
if (this.abortController !== null)
|
|
327
|
+
throw new SessionBusyError();
|
|
328
|
+
}
|
|
329
|
+
clear() {
|
|
330
|
+
this.rejectIfBusy();
|
|
331
|
+
this.agent.clear();
|
|
332
|
+
this.timelineStore.clear();
|
|
333
|
+
this.todoStore.set([]);
|
|
334
|
+
}
|
|
335
|
+
export() {
|
|
336
|
+
return this.agent.export();
|
|
337
|
+
}
|
|
338
|
+
exportState() {
|
|
339
|
+
return { messages: this.conversation.export(), todos: [...this.todoStore.all] };
|
|
340
|
+
}
|
|
341
|
+
rebuildTimeline() {
|
|
342
|
+
this.timelineStore.rebuild(toTimelineEntries(this.conversation.export(), (n, a) => this.tools.summarizeArgs(n, a)));
|
|
343
|
+
this.viewCache = null;
|
|
344
|
+
}
|
|
345
|
+
importState(state) {
|
|
346
|
+
this.rejectIfBusy();
|
|
347
|
+
this.conversation.import(state.messages);
|
|
348
|
+
this.todoStore.set(state.todos);
|
|
349
|
+
this.rebuildTimeline();
|
|
350
|
+
}
|
|
351
|
+
async compact() {
|
|
352
|
+
this.rejectIfBusy();
|
|
353
|
+
const { status } = await this.run((signal) => this.agent.compact(this.handleEvent, signal));
|
|
354
|
+
return status;
|
|
355
|
+
}
|
|
356
|
+
abort() {
|
|
357
|
+
this.abortController?.abort();
|
|
358
|
+
for (const id of this.questionQueue.resolveAll("")) {
|
|
359
|
+
this.timelineStore.setAnswer(id, "");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
submitAnswer(id, answer) {
|
|
363
|
+
this.questionQueue.submit(id, answer);
|
|
364
|
+
this.timelineStore.setAnswer(id, answer);
|
|
365
|
+
}
|
|
366
|
+
async prompt(text) {
|
|
367
|
+
this.rejectIfBusy();
|
|
368
|
+
return this.start({ type: "user", text }, (signal) => this.agent.run(text, this.handleEvent, signal));
|
|
369
|
+
}
|
|
370
|
+
ask(text, options) {
|
|
371
|
+
const { id, promise } = this.questionQueue.ask();
|
|
372
|
+
this.emit({ type: "question", id, text, options, answer: null });
|
|
373
|
+
return promise;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SessionMessage } from "./session-messages.js";
|
|
2
|
+
import { type RunStatus } from "./agent.js";
|
|
3
|
+
import type { LLMClient } from "../llm/types.js";
|
|
4
|
+
import { ToolRegistry } from "../tools/registry.js";
|
|
5
|
+
export interface SubAgentRunOptions {
|
|
6
|
+
llm: LLMClient;
|
|
7
|
+
tools: ToolRegistry;
|
|
8
|
+
cwd: string;
|
|
9
|
+
maxTurns: number;
|
|
10
|
+
stallThreshold: number;
|
|
11
|
+
contextLimit: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SubAgentRunResult {
|
|
14
|
+
status: RunStatus;
|
|
15
|
+
reply: string;
|
|
16
|
+
messages: SessionMessage[];
|
|
17
|
+
}
|
|
18
|
+
export declare function createSubAgentRunner(opts: SubAgentRunOptions): (systemPrompt: string, task: string, signal?: AbortSignal) => Promise<SubAgentRunResult>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SessionMessages, lastAssistantText } from "./session-messages.js";
|
|
2
|
+
import { Agent } from "./agent.js";
|
|
3
|
+
import { TOOL_USE_PROMPT } from "./prompts.js";
|
|
4
|
+
import { ToolRegistry } from "../tools/registry.js";
|
|
5
|
+
export function createSubAgentRunner(opts) {
|
|
6
|
+
return async (systemPrompt, task, signal) => {
|
|
7
|
+
const conversation = new SessionMessages([systemPrompt, TOOL_USE_PROMPT, `- Turn budget: ${opts.maxTurns} tool-calling turns per run.`].join("\n\n"));
|
|
8
|
+
const subTools = new ToolRegistry();
|
|
9
|
+
subTools.registerAll(opts.tools.filter((t) => t.readOnly === true));
|
|
10
|
+
const subAgent = new Agent({
|
|
11
|
+
llm: opts.llm,
|
|
12
|
+
conversation,
|
|
13
|
+
tools: subTools,
|
|
14
|
+
cwd: opts.cwd,
|
|
15
|
+
setTodos: () => { },
|
|
16
|
+
getTodos: () => [],
|
|
17
|
+
stallThreshold: opts.stallThreshold,
|
|
18
|
+
maxTurns: opts.maxTurns,
|
|
19
|
+
contextLimit: opts.contextLimit,
|
|
20
|
+
});
|
|
21
|
+
const status = await subAgent.run(task, undefined, signal);
|
|
22
|
+
const messages = conversation.export();
|
|
23
|
+
const reply = lastAssistantText(messages) || `(sub-agent produced no final text; status ${status})`;
|
|
24
|
+
return { status, reply, messages };
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SessionMessage } from "./session-messages.js";
|
|
2
|
+
import type { SessionEvent, TimelineEvent } from "./events.js";
|
|
3
|
+
export declare class TimelineStore {
|
|
4
|
+
private listeners;
|
|
5
|
+
private entries;
|
|
6
|
+
private pendingTools;
|
|
7
|
+
private pendingQuestions;
|
|
8
|
+
get all(): readonly TimelineEvent[];
|
|
9
|
+
subscribe(listener: () => void): () => void;
|
|
10
|
+
applyEvent(e: SessionEvent): void;
|
|
11
|
+
private append;
|
|
12
|
+
setResult(id: string, result: string, isError?: boolean, resultSummary?: string): void;
|
|
13
|
+
setAnswer(id: string, answer: string): void;
|
|
14
|
+
get latestUnansweredQuestion(): Extract<TimelineEvent, {
|
|
15
|
+
type: "question";
|
|
16
|
+
}> | undefined;
|
|
17
|
+
markPendingToolsAborted(): void;
|
|
18
|
+
clear(): void;
|
|
19
|
+
rebuild(entries: TimelineEvent[]): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function toTimelineEntries(messages: SessionMessage[], summarizeArgs: (name: string, args: Record<string, unknown>) => string): TimelineEvent[];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { parseToolArgs, toText } from "../llm/messages.js";
|
|
2
|
+
import { Emitter } from "../util/emitter.js";
|
|
3
|
+
export class TimelineStore {
|
|
4
|
+
listeners = new Emitter();
|
|
5
|
+
entries = [];
|
|
6
|
+
pendingTools = new Map();
|
|
7
|
+
pendingQuestions = new Map();
|
|
8
|
+
get all() {
|
|
9
|
+
return this.entries;
|
|
10
|
+
}
|
|
11
|
+
subscribe(listener) {
|
|
12
|
+
return this.listeners.subscribe(listener);
|
|
13
|
+
}
|
|
14
|
+
applyEvent(e) {
|
|
15
|
+
switch (e.type) {
|
|
16
|
+
case "tool_start":
|
|
17
|
+
this.append({ type: "tool", id: e.id, name: e.name, argsSummary: e.argsSummary, result: null });
|
|
18
|
+
break;
|
|
19
|
+
case "tool_end":
|
|
20
|
+
this.setResult(e.id, e.result, e.isError, e.resultSummary);
|
|
21
|
+
break;
|
|
22
|
+
case "question":
|
|
23
|
+
this.pendingQuestions.set(e.id, this.entries.length);
|
|
24
|
+
this.append(e);
|
|
25
|
+
break;
|
|
26
|
+
case "assistant_delta":
|
|
27
|
+
case "thinking_delta":
|
|
28
|
+
case "thinking_cleared":
|
|
29
|
+
case "run_metrics":
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
this.append(e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
append(entry) {
|
|
36
|
+
this.entries.push(entry);
|
|
37
|
+
if (entry.type === "tool" && entry.result === null) {
|
|
38
|
+
this.pendingTools.set(entry.id, this.entries.length - 1);
|
|
39
|
+
}
|
|
40
|
+
this.listeners.notify();
|
|
41
|
+
}
|
|
42
|
+
setResult(id, result, isError, resultSummary) {
|
|
43
|
+
const idx = this.pendingTools.get(id);
|
|
44
|
+
if (idx === undefined)
|
|
45
|
+
return;
|
|
46
|
+
this.pendingTools.delete(id);
|
|
47
|
+
const entry = this.entries[idx];
|
|
48
|
+
if (entry.type !== "tool" || entry.result !== null)
|
|
49
|
+
return;
|
|
50
|
+
this.entries[idx] = { ...entry, result, isError, resultSummary };
|
|
51
|
+
this.listeners.notify();
|
|
52
|
+
}
|
|
53
|
+
setAnswer(id, answer) {
|
|
54
|
+
const index = this.pendingQuestions.get(id);
|
|
55
|
+
if (index === undefined)
|
|
56
|
+
return;
|
|
57
|
+
this.pendingQuestions.delete(id);
|
|
58
|
+
const entry = this.entries[index];
|
|
59
|
+
if (entry.type !== "question" || entry.answer !== null)
|
|
60
|
+
return;
|
|
61
|
+
this.entries[index] = { ...entry, answer };
|
|
62
|
+
this.listeners.notify();
|
|
63
|
+
}
|
|
64
|
+
get latestUnansweredQuestion() {
|
|
65
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
66
|
+
const e = this.entries[i];
|
|
67
|
+
if (e.type === "question" && e.answer === null)
|
|
68
|
+
return e;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
markPendingToolsAborted() {
|
|
73
|
+
for (const [, idx] of this.pendingTools) {
|
|
74
|
+
const entry = this.entries[idx];
|
|
75
|
+
if (entry.type === "tool" && entry.result === null) {
|
|
76
|
+
this.entries[idx] = { ...entry, result: "aborted", isError: true, resultSummary: "aborted" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.pendingTools.clear();
|
|
80
|
+
this.listeners.notify();
|
|
81
|
+
}
|
|
82
|
+
clear() {
|
|
83
|
+
this.entries = [];
|
|
84
|
+
this.pendingTools.clear();
|
|
85
|
+
this.pendingQuestions.clear();
|
|
86
|
+
this.listeners.notify();
|
|
87
|
+
}
|
|
88
|
+
rebuild(entries) {
|
|
89
|
+
this.entries = entries;
|
|
90
|
+
this.pendingTools.clear();
|
|
91
|
+
this.pendingQuestions.clear();
|
|
92
|
+
entries.forEach((entry, i) => {
|
|
93
|
+
if (entry.type === "tool" && entry.result === null) {
|
|
94
|
+
this.pendingTools.set(entry.id, i);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
this.listeners.notify();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function toTimelineEntries(messages, summarizeArgs) {
|
|
101
|
+
const toolResults = new Map();
|
|
102
|
+
for (const m of messages) {
|
|
103
|
+
if (m.role === "tool") {
|
|
104
|
+
const result = { content: m.content };
|
|
105
|
+
if (m.resultSummary !== undefined || m.isError !== undefined) {
|
|
106
|
+
result.resultSummary = m.resultSummary;
|
|
107
|
+
result.isError = m.isError;
|
|
108
|
+
}
|
|
109
|
+
toolResults.set(m.tool_call_id, result);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const entries = [];
|
|
113
|
+
for (const m of messages) {
|
|
114
|
+
if (m.role === "user") {
|
|
115
|
+
entries.push({ type: "user", text: m.content });
|
|
116
|
+
}
|
|
117
|
+
else if (m.role === "skill") {
|
|
118
|
+
entries.push({ type: "skill", name: m.name });
|
|
119
|
+
}
|
|
120
|
+
else if (m.role === "assistant") {
|
|
121
|
+
const text = toText(m.content);
|
|
122
|
+
if (text)
|
|
123
|
+
entries.push({ type: "assistant", text });
|
|
124
|
+
if (m.tool_calls) {
|
|
125
|
+
for (const tc of m.tool_calls) {
|
|
126
|
+
const parsed = parseToolArgs(tc.function.arguments);
|
|
127
|
+
const entry = {
|
|
128
|
+
type: "tool",
|
|
129
|
+
id: tc.id,
|
|
130
|
+
name: tc.function.name,
|
|
131
|
+
argsSummary: summarizeArgs(tc.function.name, parsed.ok ? parsed.args : {}),
|
|
132
|
+
result: null,
|
|
133
|
+
};
|
|
134
|
+
const result = toolResults.get(tc.id);
|
|
135
|
+
if (result) {
|
|
136
|
+
entries.push({ ...entry, result: result.content, isError: result.isError, resultSummary: result.resultSummary });
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
entries.push(entry);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return entries;
|
|
146
|
+
}
|