@shareai-lab/kode-sdk 1.0.0-beta.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 +312 -0
- package/dist/core/agent.d.ts +85 -0
- package/dist/core/agent.js +687 -0
- package/dist/core/events.d.ts +19 -0
- package/dist/core/events.js +121 -0
- package/dist/core/hooks.d.ts +23 -0
- package/dist/core/hooks.js +71 -0
- package/dist/core/pool.d.ts +33 -0
- package/dist/core/pool.js +91 -0
- package/dist/core/room.d.ts +15 -0
- package/dist/core/room.js +57 -0
- package/dist/core/scheduler.d.ts +26 -0
- package/dist/core/scheduler.js +184 -0
- package/dist/core/types.d.ts +192 -0
- package/dist/core/types.js +13 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +57 -0
- package/dist/infra/provider.d.ts +58 -0
- package/dist/infra/provider.js +118 -0
- package/dist/infra/sandbox.d.ts +39 -0
- package/dist/infra/sandbox.js +77 -0
- package/dist/infra/store.d.ts +32 -0
- package/dist/infra/store.js +132 -0
- package/dist/tools/bash.d.ts +63 -0
- package/dist/tools/bash.js +99 -0
- package/dist/tools/builtin.d.ts +15 -0
- package/dist/tools/builtin.js +96 -0
- package/dist/tools/fs.d.ts +96 -0
- package/dist/tools/fs.js +96 -0
- package/dist/tools/task.d.ts +38 -0
- package/dist/tools/task.js +45 -0
- package/dist/utils/session-id.d.ts +21 -0
- package/dist/utils/session-id.js +64 -0
- package/package.json +47 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Agent = void 0;
|
|
4
|
+
const events_1 = require("./events");
|
|
5
|
+
const hooks_1 = require("./hooks");
|
|
6
|
+
const scheduler_1 = require("./scheduler");
|
|
7
|
+
class Agent {
|
|
8
|
+
constructor(templateOrOpts, overrides) {
|
|
9
|
+
this.tools = new Map();
|
|
10
|
+
this.messages = [];
|
|
11
|
+
this.state = 'READY';
|
|
12
|
+
this.lastSfpIndex = -1;
|
|
13
|
+
this.stepCount = 0;
|
|
14
|
+
this.events = new events_1.EventBus();
|
|
15
|
+
this.hooks = new hooks_1.HookManager();
|
|
16
|
+
this.pendingPermissions = new Map();
|
|
17
|
+
this.interrupted = false;
|
|
18
|
+
let opts;
|
|
19
|
+
if ('sessionId' in templateOrOpts) {
|
|
20
|
+
opts = templateOrOpts;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
if (!overrides)
|
|
24
|
+
throw new Error('overrides required when using template');
|
|
25
|
+
opts = {
|
|
26
|
+
sessionId: overrides.sessionId,
|
|
27
|
+
provider: overrides.provider,
|
|
28
|
+
store: overrides.store,
|
|
29
|
+
sandbox: overrides.sandbox,
|
|
30
|
+
tools: templateOrOpts.tools || [],
|
|
31
|
+
system: templateOrOpts.system,
|
|
32
|
+
templateId: templateOrOpts.id,
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
this.sessionId = opts.sessionId;
|
|
37
|
+
this.provider = opts.provider;
|
|
38
|
+
this.store = opts.store;
|
|
39
|
+
this.sandbox = opts.sandbox;
|
|
40
|
+
this.system = opts.system;
|
|
41
|
+
this.maxTokens = opts.maxTokens || 4096;
|
|
42
|
+
this.temperature = opts.temperature ?? 0.7;
|
|
43
|
+
this.maxConcurrency = opts.maxConcurrency || 3;
|
|
44
|
+
this.templateId = opts.templateId || 'default';
|
|
45
|
+
// Connect EventBus to Store for event persistence
|
|
46
|
+
this.events.setStore(this.store, this.sessionId);
|
|
47
|
+
if (opts.tools) {
|
|
48
|
+
for (const tool of opts.tools) {
|
|
49
|
+
this.tools.set(tool.name, tool);
|
|
50
|
+
if (tool.hooks) {
|
|
51
|
+
this.hooks.register(tool.hooks);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async send(text) {
|
|
57
|
+
const messageId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
58
|
+
this.messages.push({
|
|
59
|
+
role: 'user',
|
|
60
|
+
content: [{ type: 'text', text }],
|
|
61
|
+
});
|
|
62
|
+
this.stepCount++;
|
|
63
|
+
await this.persistMessages();
|
|
64
|
+
this.events.emitEvent({
|
|
65
|
+
type: 'messages_update',
|
|
66
|
+
messageCount: this.messages.length,
|
|
67
|
+
lastSfpIndex: this.lastSfpIndex,
|
|
68
|
+
added: 1,
|
|
69
|
+
});
|
|
70
|
+
// Start processing in background (non-blocking)
|
|
71
|
+
this.step().catch((err) => {
|
|
72
|
+
this.events.emitEvent({
|
|
73
|
+
type: 'error',
|
|
74
|
+
kind: 'ProviderError',
|
|
75
|
+
message: err.message,
|
|
76
|
+
hint: err.stack,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
return messageId;
|
|
80
|
+
}
|
|
81
|
+
subscribe(opts) {
|
|
82
|
+
return this.events.subscribe(opts);
|
|
83
|
+
}
|
|
84
|
+
async *chat(text) {
|
|
85
|
+
const since = this.events.getCursor();
|
|
86
|
+
await this.send(text);
|
|
87
|
+
for await (const event of this.subscribe({ since })) {
|
|
88
|
+
yield event;
|
|
89
|
+
if (event.type === 'state' && event.state === 'READY')
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async reply(text) {
|
|
94
|
+
let fullText = '';
|
|
95
|
+
for await (const event of this.chat(text)) {
|
|
96
|
+
if (event.type === 'text') {
|
|
97
|
+
fullText = event.text;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return fullText;
|
|
101
|
+
}
|
|
102
|
+
async askLLM(text, opts) {
|
|
103
|
+
const provider = opts?.provider || this.provider;
|
|
104
|
+
const messages = [{ role: 'user', content: [{ type: 'text', text }] }];
|
|
105
|
+
const response = await provider.complete(messages, {
|
|
106
|
+
tools: opts?.useTools ? this.getToolSchemas() : undefined,
|
|
107
|
+
system: opts?.system || this.system,
|
|
108
|
+
maxTokens: this.maxTokens,
|
|
109
|
+
temperature: this.temperature,
|
|
110
|
+
});
|
|
111
|
+
const textBlock = response.content.find((c) => c.type === 'text');
|
|
112
|
+
return {
|
|
113
|
+
text: textBlock ? textBlock.text : '',
|
|
114
|
+
sessionId: opts?.sessionId || this.sessionId,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async interrupt(opts) {
|
|
118
|
+
this.interrupted = true;
|
|
119
|
+
// Find pending tool_use blocks that haven't received results yet
|
|
120
|
+
const lastMsg = this.messages[this.messages.length - 1];
|
|
121
|
+
if (lastMsg?.role === 'assistant') {
|
|
122
|
+
const toolUses = lastMsg.content.filter(c => c.type === 'tool_use');
|
|
123
|
+
if (toolUses.length > 0) {
|
|
124
|
+
// Collect all existing tool_result IDs
|
|
125
|
+
const resultIds = new Set();
|
|
126
|
+
for (const msg of this.messages) {
|
|
127
|
+
for (const block of msg.content) {
|
|
128
|
+
if (block.type === 'tool_result') {
|
|
129
|
+
resultIds.add(block.tool_use_id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Generate cancelled results for pending tools
|
|
134
|
+
const cancelledResults = [];
|
|
135
|
+
for (const tu of toolUses) {
|
|
136
|
+
if (!resultIds.has(tu.id)) {
|
|
137
|
+
cancelledResults.push({
|
|
138
|
+
type: 'tool_result',
|
|
139
|
+
tool_use_id: tu.id,
|
|
140
|
+
content: { error: opts?.note || 'Interrupted by user' },
|
|
141
|
+
is_error: true,
|
|
142
|
+
});
|
|
143
|
+
this.events.emitEvent({
|
|
144
|
+
type: 'tool_result',
|
|
145
|
+
id: tu.id,
|
|
146
|
+
name: tu.name,
|
|
147
|
+
ok: false,
|
|
148
|
+
content: { error: opts?.note || 'Interrupted by user' },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Add cancelled results to message history
|
|
153
|
+
if (cancelledResults.length > 0) {
|
|
154
|
+
this.messages.push({
|
|
155
|
+
role: 'user',
|
|
156
|
+
content: cancelledResults,
|
|
157
|
+
});
|
|
158
|
+
this.stepCount++;
|
|
159
|
+
this.lastSfpIndex = this.messages.length - 1;
|
|
160
|
+
await this.persistMessages();
|
|
161
|
+
this.events.emitEvent({
|
|
162
|
+
type: 'messages_update',
|
|
163
|
+
messageCount: this.messages.length,
|
|
164
|
+
lastSfpIndex: this.lastSfpIndex,
|
|
165
|
+
added: 1,
|
|
166
|
+
});
|
|
167
|
+
this.events.emitEvent({
|
|
168
|
+
type: 'commit',
|
|
169
|
+
sfpIndex: this.lastSfpIndex,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.state = 'READY';
|
|
175
|
+
this.events.emitEvent({ type: 'state', state: 'READY' });
|
|
176
|
+
// Note is only for events/audit, not written to message stream
|
|
177
|
+
if (opts?.note) {
|
|
178
|
+
this.events.emitEvent({
|
|
179
|
+
type: 'error',
|
|
180
|
+
kind: 'PolicyViolation',
|
|
181
|
+
message: `Interrupted: ${opts.note}`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async decide(permId, decision, note) {
|
|
186
|
+
const resolver = this.pendingPermissions.get(permId);
|
|
187
|
+
if (!resolver) {
|
|
188
|
+
throw new Error(`Permission not found: ${permId}`);
|
|
189
|
+
}
|
|
190
|
+
resolver(decision, note);
|
|
191
|
+
this.pendingPermissions.delete(permId);
|
|
192
|
+
this.events.emitEvent({ type: 'permission_decision', id: permId, decision, by: 'api' });
|
|
193
|
+
if (decision === 'allow') {
|
|
194
|
+
this.state = 'BUSY';
|
|
195
|
+
this.step().catch((err) => {
|
|
196
|
+
this.events.emitEvent({
|
|
197
|
+
type: 'error',
|
|
198
|
+
kind: 'ProviderError',
|
|
199
|
+
message: err.message,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async snapshot(label) {
|
|
205
|
+
const id = label || `sfp:${this.lastSfpIndex}`;
|
|
206
|
+
const snapshot = {
|
|
207
|
+
id,
|
|
208
|
+
messages: JSON.parse(JSON.stringify(this.messages)),
|
|
209
|
+
lastSfpIndex: this.lastSfpIndex,
|
|
210
|
+
createdAt: new Date().toISOString(),
|
|
211
|
+
};
|
|
212
|
+
await this.store.saveSnapshot(this.sessionId, snapshot);
|
|
213
|
+
return id;
|
|
214
|
+
}
|
|
215
|
+
async fork(sel) {
|
|
216
|
+
// 1. Load snapshot (or use current state if no selector)
|
|
217
|
+
let snapshot;
|
|
218
|
+
if (!sel) {
|
|
219
|
+
// Fork from current state
|
|
220
|
+
snapshot = {
|
|
221
|
+
id: `sfp:${this.lastSfpIndex}`,
|
|
222
|
+
messages: JSON.parse(JSON.stringify(this.messages)),
|
|
223
|
+
lastSfpIndex: this.lastSfpIndex,
|
|
224
|
+
createdAt: new Date().toISOString(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
else if (typeof sel === 'string') {
|
|
228
|
+
// Load snapshot by ID
|
|
229
|
+
const loaded = await this.store.loadSnapshot(this.sessionId, sel);
|
|
230
|
+
if (!loaded) {
|
|
231
|
+
throw new Error(`Snapshot not found: ${sel}`);
|
|
232
|
+
}
|
|
233
|
+
snapshot = loaded;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Load snapshot by selector
|
|
237
|
+
const snapshotId = sel.at || `sfp:${this.lastSfpIndex}`;
|
|
238
|
+
const loaded = await this.store.loadSnapshot(this.sessionId, snapshotId);
|
|
239
|
+
if (!loaded) {
|
|
240
|
+
throw new Error(`Snapshot not found: ${snapshotId}`);
|
|
241
|
+
}
|
|
242
|
+
snapshot = loaded;
|
|
243
|
+
}
|
|
244
|
+
// 2. Generate new sessionId for forked agent
|
|
245
|
+
const forkId = `fork:${Date.now()}`;
|
|
246
|
+
const newSessionId = `${this.sessionId}/${forkId}`;
|
|
247
|
+
// 3. Create new agent with same configuration
|
|
248
|
+
const forked = new Agent({
|
|
249
|
+
sessionId: newSessionId,
|
|
250
|
+
provider: this.provider,
|
|
251
|
+
store: this.store,
|
|
252
|
+
sandbox: this.sandbox,
|
|
253
|
+
tools: Array.from(this.tools.values()),
|
|
254
|
+
system: this.system,
|
|
255
|
+
maxTokens: this.maxTokens,
|
|
256
|
+
temperature: this.temperature,
|
|
257
|
+
maxConcurrency: this.maxConcurrency,
|
|
258
|
+
templateId: this.templateId,
|
|
259
|
+
});
|
|
260
|
+
// 4. Restore messages from snapshot
|
|
261
|
+
forked.messages = snapshot.messages;
|
|
262
|
+
forked.lastSfpIndex = snapshot.lastSfpIndex;
|
|
263
|
+
forked.stepCount = snapshot.messages.filter(m => m.role === 'user').length;
|
|
264
|
+
// 5. Persist forked state
|
|
265
|
+
await forked.persistMessages();
|
|
266
|
+
// 6. Emit forked event
|
|
267
|
+
this.events.emitEvent({
|
|
268
|
+
type: 'forked',
|
|
269
|
+
childSessionId: newSessionId,
|
|
270
|
+
from: snapshot.id,
|
|
271
|
+
});
|
|
272
|
+
return forked;
|
|
273
|
+
}
|
|
274
|
+
static async resume(sessionId, opts) {
|
|
275
|
+
const { autoRun = false, strategy = 'manual', store, ...agentOpts } = opts;
|
|
276
|
+
// Load messages from store
|
|
277
|
+
const messages = await store.loadMessages(sessionId);
|
|
278
|
+
if (messages.length === 0) {
|
|
279
|
+
throw new Error(`Session has no messages: ${sessionId}`);
|
|
280
|
+
}
|
|
281
|
+
// Create agent instance
|
|
282
|
+
const agent = new Agent({
|
|
283
|
+
...agentOpts,
|
|
284
|
+
sessionId,
|
|
285
|
+
store,
|
|
286
|
+
});
|
|
287
|
+
// Restore messages
|
|
288
|
+
agent.messages = messages;
|
|
289
|
+
// Find last SFP
|
|
290
|
+
agent.lastSfpIndex = agent.findLastSfp();
|
|
291
|
+
// Restore step count
|
|
292
|
+
agent.stepCount = messages.filter((m) => m.role === 'user').length;
|
|
293
|
+
// Handle crash recovery: generate sealed results for pending tools
|
|
294
|
+
if (strategy === 'crash') {
|
|
295
|
+
const sealedTools = agent.findSealedTools();
|
|
296
|
+
if (sealedTools.length > 0) {
|
|
297
|
+
const sealedResults = sealedTools.map((tool) => ({
|
|
298
|
+
type: 'tool_result',
|
|
299
|
+
tool_use_id: tool.tool_use_id,
|
|
300
|
+
content: {
|
|
301
|
+
error: `Sealed due to crash: ${tool.note}`,
|
|
302
|
+
sealed: true,
|
|
303
|
+
},
|
|
304
|
+
is_error: true,
|
|
305
|
+
}));
|
|
306
|
+
agent.messages.push({
|
|
307
|
+
role: 'user',
|
|
308
|
+
content: sealedResults,
|
|
309
|
+
});
|
|
310
|
+
agent.stepCount++;
|
|
311
|
+
agent.lastSfpIndex = agent.messages.length - 1;
|
|
312
|
+
await agent.persistMessages();
|
|
313
|
+
agent.events.emitEvent({
|
|
314
|
+
type: 'resume',
|
|
315
|
+
from: 'crash',
|
|
316
|
+
sealed: sealedTools,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
agent.events.emitEvent({
|
|
322
|
+
type: 'resume',
|
|
323
|
+
from: 'manual',
|
|
324
|
+
sealed: [],
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// AutoRun: continue execution if there are pending tools
|
|
328
|
+
if (autoRun) {
|
|
329
|
+
const lastMessage = messages[messages.length - 1];
|
|
330
|
+
if (lastMessage.role === 'assistant') {
|
|
331
|
+
const pendingTools = lastMessage.content.filter((c) => c.type === 'tool_use');
|
|
332
|
+
if (pendingTools.length > 0) {
|
|
333
|
+
agent.step().catch((err) => {
|
|
334
|
+
agent.events.emitEvent({
|
|
335
|
+
type: 'error',
|
|
336
|
+
kind: 'ProviderError',
|
|
337
|
+
message: err.message,
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
agent.state = 'READY';
|
|
344
|
+
agent.events.emitEvent({ type: 'state', state: 'READY' });
|
|
345
|
+
return agent;
|
|
346
|
+
}
|
|
347
|
+
findLastSfp() {
|
|
348
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
349
|
+
const msg = this.messages[i];
|
|
350
|
+
// User message is SFP
|
|
351
|
+
if (msg.role === 'user') {
|
|
352
|
+
return i;
|
|
353
|
+
}
|
|
354
|
+
// Assistant text-only message is SFP
|
|
355
|
+
if (msg.role === 'assistant') {
|
|
356
|
+
const hasToolUse = msg.content.some((c) => c.type === 'tool_use');
|
|
357
|
+
if (!hasToolUse) {
|
|
358
|
+
return i;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return -1;
|
|
363
|
+
}
|
|
364
|
+
findSealedTools() {
|
|
365
|
+
const sealed = [];
|
|
366
|
+
const toolUseMap = new Map();
|
|
367
|
+
const toolResultSet = new Set();
|
|
368
|
+
// Collect all tool_use and tool_result
|
|
369
|
+
for (const msg of this.messages) {
|
|
370
|
+
for (const block of msg.content) {
|
|
371
|
+
if (block.type === 'tool_use') {
|
|
372
|
+
const tu = block;
|
|
373
|
+
toolUseMap.set(tu.id, { name: tu.name, args: tu.input });
|
|
374
|
+
}
|
|
375
|
+
else if (block.type === 'tool_result') {
|
|
376
|
+
const tr = block;
|
|
377
|
+
toolResultSet.add(tr.tool_use_id);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Find tool_use without results
|
|
382
|
+
for (const [toolId, tool] of toolUseMap.entries()) {
|
|
383
|
+
if (!toolResultSet.has(toolId)) {
|
|
384
|
+
sealed.push({
|
|
385
|
+
tool_use_id: toolId,
|
|
386
|
+
name: tool.name,
|
|
387
|
+
args: tool.args,
|
|
388
|
+
note: 'No result found, likely crashed during execution',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return sealed;
|
|
393
|
+
}
|
|
394
|
+
async history(opts) {
|
|
395
|
+
const timeline = this.events.getTimeline(opts?.since);
|
|
396
|
+
const limited = opts?.limit ? timeline.slice(0, opts.limit) : timeline;
|
|
397
|
+
return limited.map((t) => t.event);
|
|
398
|
+
}
|
|
399
|
+
async status() {
|
|
400
|
+
return {
|
|
401
|
+
state: this.state,
|
|
402
|
+
sessionId: this.sessionId,
|
|
403
|
+
messageCount: this.messages.length,
|
|
404
|
+
lastSfpIndex: this.lastSfpIndex,
|
|
405
|
+
cursor: this.events.getCursor(),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
async info() {
|
|
409
|
+
return {
|
|
410
|
+
sessionId: this.sessionId,
|
|
411
|
+
templateId: this.templateId,
|
|
412
|
+
createdAt: new Date().toISOString(),
|
|
413
|
+
lineage: [],
|
|
414
|
+
messageCount: this.messages.length,
|
|
415
|
+
lastSfpIndex: this.lastSfpIndex,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
use(hooks) {
|
|
419
|
+
this.hooks.register(hooks, 'agent');
|
|
420
|
+
return this;
|
|
421
|
+
}
|
|
422
|
+
getHooks() {
|
|
423
|
+
return this.hooks.getRegistered();
|
|
424
|
+
}
|
|
425
|
+
registerTools(tools) {
|
|
426
|
+
for (const tool of tools) {
|
|
427
|
+
this.tools.set(tool.name, tool);
|
|
428
|
+
if (tool.hooks) {
|
|
429
|
+
this.hooks.register(tool.hooks, 'toolTune');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return this;
|
|
433
|
+
}
|
|
434
|
+
schedule() {
|
|
435
|
+
if (!this.scheduler) {
|
|
436
|
+
this.scheduler = new scheduler_1.Scheduler();
|
|
437
|
+
// Connect step events to scheduler
|
|
438
|
+
this.on('messages_update', (event) => {
|
|
439
|
+
if (event.added && event.added > 0) {
|
|
440
|
+
this.scheduler.notifyStep();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return this.scheduler;
|
|
445
|
+
}
|
|
446
|
+
on(event, handler) {
|
|
447
|
+
this.events.on(event, handler);
|
|
448
|
+
return this;
|
|
449
|
+
}
|
|
450
|
+
async step() {
|
|
451
|
+
if (this.state !== 'READY')
|
|
452
|
+
return;
|
|
453
|
+
if (this.interrupted) {
|
|
454
|
+
this.interrupted = false;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
this.state = 'BUSY';
|
|
458
|
+
this.events.emitEvent({ type: 'state', state: 'BUSY' });
|
|
459
|
+
try {
|
|
460
|
+
await this.hooks.runPreModel(this.messages);
|
|
461
|
+
const response = await this.provider.complete(this.messages, {
|
|
462
|
+
tools: this.getToolSchemas(),
|
|
463
|
+
system: this.system,
|
|
464
|
+
maxTokens: this.maxTokens,
|
|
465
|
+
temperature: this.temperature,
|
|
466
|
+
});
|
|
467
|
+
await this.hooks.runPostModel(response);
|
|
468
|
+
this.messages.push({
|
|
469
|
+
role: 'assistant',
|
|
470
|
+
content: response.content,
|
|
471
|
+
});
|
|
472
|
+
// Emit text events
|
|
473
|
+
const textBlocks = response.content.filter((c) => c.type === 'text');
|
|
474
|
+
for (const block of textBlocks) {
|
|
475
|
+
this.events.emitEvent({ type: 'text', text: block.text });
|
|
476
|
+
}
|
|
477
|
+
// Emit usage
|
|
478
|
+
if (response.usage) {
|
|
479
|
+
this.events.emitEvent({
|
|
480
|
+
type: 'usage',
|
|
481
|
+
data: {
|
|
482
|
+
input_tokens: response.usage.input_tokens,
|
|
483
|
+
output_tokens: response.usage.output_tokens,
|
|
484
|
+
total_tokens: response.usage.input_tokens + response.usage.output_tokens,
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const toolUses = response.content.filter((c) => c.type === 'tool_use');
|
|
489
|
+
if (toolUses.length > 0) {
|
|
490
|
+
const results = await this.executeTools(toolUses);
|
|
491
|
+
this.messages.push({
|
|
492
|
+
role: 'user',
|
|
493
|
+
content: results,
|
|
494
|
+
});
|
|
495
|
+
this.stepCount++;
|
|
496
|
+
this.lastSfpIndex = this.messages.length - 1;
|
|
497
|
+
this.events.emitEvent({ type: 'commit', sfpIndex: this.lastSfpIndex });
|
|
498
|
+
await this.persistMessages();
|
|
499
|
+
this.events.emitEvent({
|
|
500
|
+
type: 'messages_update',
|
|
501
|
+
messageCount: this.messages.length,
|
|
502
|
+
lastSfpIndex: this.lastSfpIndex,
|
|
503
|
+
added: 1,
|
|
504
|
+
});
|
|
505
|
+
// Continue next step
|
|
506
|
+
this.state = 'READY';
|
|
507
|
+
return this.step();
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
// No tools, this is SFP
|
|
511
|
+
this.lastSfpIndex = this.messages.length - 1;
|
|
512
|
+
this.events.emitEvent({ type: 'commit', sfpIndex: this.lastSfpIndex });
|
|
513
|
+
await this.persistMessages();
|
|
514
|
+
this.events.emitEvent({
|
|
515
|
+
type: 'messages_update',
|
|
516
|
+
messageCount: this.messages.length,
|
|
517
|
+
lastSfpIndex: this.lastSfpIndex,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
this.events.emitEvent({
|
|
523
|
+
type: 'error',
|
|
524
|
+
kind: 'ProviderError',
|
|
525
|
+
message: error.message,
|
|
526
|
+
hint: error.stack,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
this.state = 'READY';
|
|
531
|
+
this.events.emitEvent({ type: 'state', state: 'READY' });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async executeTools(toolUses) {
|
|
535
|
+
const results = [];
|
|
536
|
+
for (const use of toolUses) {
|
|
537
|
+
if (use.type !== 'tool_use')
|
|
538
|
+
continue;
|
|
539
|
+
const tu = use;
|
|
540
|
+
const tool = this.tools.get(tu.name);
|
|
541
|
+
this.events.emitEvent({ type: 'tool_use', id: tu.id, name: tu.name, input: tu.input });
|
|
542
|
+
if (!tool) {
|
|
543
|
+
results.push({
|
|
544
|
+
type: 'tool_result',
|
|
545
|
+
tool_use_id: tu.id,
|
|
546
|
+
content: { error: `Tool not found: ${tu.name}` },
|
|
547
|
+
is_error: true,
|
|
548
|
+
});
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const call = {
|
|
552
|
+
id: tu.id,
|
|
553
|
+
name: tu.name,
|
|
554
|
+
args: tu.input,
|
|
555
|
+
sessionId: this.sessionId,
|
|
556
|
+
};
|
|
557
|
+
const ctx = {
|
|
558
|
+
sessionId: this.sessionId,
|
|
559
|
+
sandbox: this.sandbox,
|
|
560
|
+
agent: this,
|
|
561
|
+
};
|
|
562
|
+
// Run preToolUse hooks
|
|
563
|
+
const hookDecision = await this.hooks.runPreToolUse(call, ctx);
|
|
564
|
+
if (hookDecision) {
|
|
565
|
+
if ('decision' in hookDecision) {
|
|
566
|
+
if (hookDecision.decision === 'ask') {
|
|
567
|
+
// Pause and wait for permission
|
|
568
|
+
await this.requestPermission(call, hookDecision.meta);
|
|
569
|
+
// After permission granted, continue
|
|
570
|
+
}
|
|
571
|
+
else if (hookDecision.decision === 'deny') {
|
|
572
|
+
const result = {
|
|
573
|
+
type: 'tool_result',
|
|
574
|
+
tool_use_id: tu.id,
|
|
575
|
+
content: hookDecision.toolResult || { error: hookDecision.reason || 'Denied by policy' },
|
|
576
|
+
is_error: true,
|
|
577
|
+
};
|
|
578
|
+
results.push(result);
|
|
579
|
+
this.events.emitEvent({
|
|
580
|
+
type: 'tool_result',
|
|
581
|
+
id: tu.id,
|
|
582
|
+
name: tu.name,
|
|
583
|
+
ok: false,
|
|
584
|
+
content: result.content,
|
|
585
|
+
});
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else if ('result' in hookDecision) {
|
|
590
|
+
// Pre-computed result
|
|
591
|
+
const result = {
|
|
592
|
+
type: 'tool_result',
|
|
593
|
+
tool_use_id: tu.id,
|
|
594
|
+
content: hookDecision.result,
|
|
595
|
+
};
|
|
596
|
+
results.push(result);
|
|
597
|
+
this.events.emitEvent({
|
|
598
|
+
type: 'tool_result',
|
|
599
|
+
id: tu.id,
|
|
600
|
+
name: tu.name,
|
|
601
|
+
ok: true,
|
|
602
|
+
content: result.content,
|
|
603
|
+
});
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Execute tool
|
|
608
|
+
try {
|
|
609
|
+
const startTime = Date.now();
|
|
610
|
+
const res = await tool.exec(call.args, ctx);
|
|
611
|
+
const duration = Date.now() - startTime;
|
|
612
|
+
let outcome = {
|
|
613
|
+
id: tu.id,
|
|
614
|
+
name: tu.name,
|
|
615
|
+
ok: true,
|
|
616
|
+
content: res,
|
|
617
|
+
duration_ms: duration,
|
|
618
|
+
};
|
|
619
|
+
// Run postToolUse hooks
|
|
620
|
+
outcome = await this.hooks.runPostToolUse(outcome, ctx);
|
|
621
|
+
const result = {
|
|
622
|
+
type: 'tool_result',
|
|
623
|
+
tool_use_id: tu.id,
|
|
624
|
+
content: outcome.content,
|
|
625
|
+
};
|
|
626
|
+
results.push(result);
|
|
627
|
+
this.events.emitEvent({
|
|
628
|
+
type: 'tool_result',
|
|
629
|
+
id: tu.id,
|
|
630
|
+
name: tu.name,
|
|
631
|
+
ok: true,
|
|
632
|
+
content: outcome.content,
|
|
633
|
+
duration_ms: outcome.duration_ms,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
const result = {
|
|
638
|
+
type: 'tool_result',
|
|
639
|
+
tool_use_id: tu.id,
|
|
640
|
+
content: { error: error.message },
|
|
641
|
+
is_error: true,
|
|
642
|
+
};
|
|
643
|
+
results.push(result);
|
|
644
|
+
this.events.emitEvent({
|
|
645
|
+
type: 'tool_result',
|
|
646
|
+
id: tu.id,
|
|
647
|
+
name: tu.name,
|
|
648
|
+
ok: false,
|
|
649
|
+
content: result.content,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return results;
|
|
654
|
+
}
|
|
655
|
+
async requestPermission(call, meta) {
|
|
656
|
+
return new Promise((resolve) => {
|
|
657
|
+
const respondFn = async (decision, note) => {
|
|
658
|
+
this.events.emitEvent({ type: 'permission_decision', id: call.id, decision, by: 'respond' });
|
|
659
|
+
resolve();
|
|
660
|
+
};
|
|
661
|
+
this.pendingPermissions.set(call.id, (decision, note) => {
|
|
662
|
+
respondFn(decision, note);
|
|
663
|
+
});
|
|
664
|
+
this.state = 'PAUSED';
|
|
665
|
+
this.events.emitEvent({
|
|
666
|
+
type: 'permission_ask',
|
|
667
|
+
id: call.id,
|
|
668
|
+
tool: call.name,
|
|
669
|
+
args: call.args,
|
|
670
|
+
meta,
|
|
671
|
+
respond: respondFn,
|
|
672
|
+
});
|
|
673
|
+
this.events.emitEvent({ type: 'state', state: 'PAUSED' });
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
getToolSchemas() {
|
|
677
|
+
return Array.from(this.tools.values()).map((tool) => ({
|
|
678
|
+
name: tool.name,
|
|
679
|
+
description: tool.description,
|
|
680
|
+
input_schema: tool.input_schema,
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
async persistMessages() {
|
|
684
|
+
await this.store.saveMessages(this.sessionId, this.messages);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
exports.Agent = Agent;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AgentEvent, AgentEventKind, Timeline } from '../core/types';
|
|
2
|
+
import { Store } from '../infra/store';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
export declare class EventBus extends EventEmitter {
|
|
5
|
+
private cursor;
|
|
6
|
+
private timeline;
|
|
7
|
+
private subscribers;
|
|
8
|
+
private store?;
|
|
9
|
+
private sessionId?;
|
|
10
|
+
setStore(store: Store, sessionId: string): void;
|
|
11
|
+
emitEvent(event: any): number;
|
|
12
|
+
subscribe(opts?: {
|
|
13
|
+
since?: number;
|
|
14
|
+
kinds?: AgentEventKind[];
|
|
15
|
+
}): AsyncIterable<AgentEvent>;
|
|
16
|
+
getTimeline(since?: number): Timeline[];
|
|
17
|
+
getCursor(): number;
|
|
18
|
+
reset(): void;
|
|
19
|
+
}
|