@deepstrike/sdk 0.1.12 → 0.1.14
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 +82 -52
- package/dist/collaboration/harness.js +6 -1
- package/dist/collaboration/modes/creator-verifier.d.ts +2 -2
- package/dist/collaboration/modes/creator-verifier.js +2 -2
- package/dist/collaboration/pool.d.ts +4 -35
- package/dist/collaboration/pool.js +18 -44
- package/dist/harness/harness.d.ts +19 -7
- package/dist/harness/harness.js +27 -22
- package/dist/index.d.ts +19 -7
- package/dist/index.js +15 -3
- package/dist/kernel.d.ts +1 -0
- package/dist/kernel.js +10 -1
- package/dist/memory/protocols.d.ts +0 -4
- package/dist/providers/anthropic.d.ts +7 -2
- package/dist/providers/anthropic.js +48 -9
- package/dist/providers/base.d.ts +1 -0
- package/dist/providers/base.js +6 -0
- package/dist/providers/deepseek.d.ts +4 -2
- package/dist/providers/deepseek.js +49 -1
- package/dist/providers/gemini.d.ts +4 -2
- package/dist/providers/gemini.js +24 -5
- package/dist/providers/kimi.d.ts +3 -1
- package/dist/providers/kimi.js +10 -0
- package/dist/providers/minimax.d.ts +3 -1
- package/dist/providers/minimax.js +8 -0
- package/dist/providers/ollama.d.ts +5 -2
- package/dist/providers/ollama.js +63 -7
- package/dist/providers/openai-responses.d.ts +4 -2
- package/dist/providers/openai-responses.js +23 -3
- package/dist/providers/openai.d.ts +4 -2
- package/dist/providers/openai.js +44 -3
- package/dist/providers/profiles.d.ts +502 -6
- package/dist/providers/profiles.js +225 -82
- package/dist/providers/qwen.d.ts +5 -2
- package/dist/providers/qwen.js +59 -11
- package/dist/runtime/credential-vault.d.ts +18 -0
- package/dist/runtime/credential-vault.js +33 -0
- package/dist/runtime/execution-plane.d.ts +38 -0
- package/dist/runtime/execution-plane.js +146 -0
- package/dist/runtime/mcp-proxy-plane.d.ts +51 -0
- package/dist/runtime/mcp-proxy-plane.js +204 -0
- package/dist/runtime/process-sandbox-plane.d.ts +32 -0
- package/dist/runtime/process-sandbox-plane.js +109 -0
- package/dist/runtime/remote-vpc-plane.d.ts +48 -0
- package/dist/runtime/remote-vpc-plane.js +82 -0
- package/dist/runtime/runner.d.ts +49 -0
- package/dist/runtime/runner.js +364 -0
- package/dist/runtime/session-log.d.ts +67 -0
- package/dist/runtime/session-log.js +84 -0
- package/dist/tools/index.d.ts +7 -2
- package/dist/tools/index.js +83 -0
- package/dist/types.d.ts +52 -1
- package/package.json +3 -2
- package/dist/agent.d.ts +0 -88
- package/dist/agent.js +0 -430
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { LLMProvider, StreamEvent, ToolSuspendEvent } from "../types.js";
|
|
2
|
+
import type { DreamStore, DreamResult } from "../memory/protocols.js";
|
|
3
|
+
import type { KnowledgeSource } from "../knowledge/source.js";
|
|
4
|
+
import type { SignalSource } from "../signals/types.js";
|
|
5
|
+
import type { SessionLog } from "./session-log.js";
|
|
6
|
+
import type { ExecutionPlane } from "./execution-plane.js";
|
|
7
|
+
export interface RuntimeOptions {
|
|
8
|
+
provider: LLMProvider;
|
|
9
|
+
sessionLog: SessionLog;
|
|
10
|
+
executionPlane: ExecutionPlane;
|
|
11
|
+
maxTokens: number;
|
|
12
|
+
maxTurns?: number;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
agentId?: string;
|
|
15
|
+
systemPrompt?: string;
|
|
16
|
+
initialMemory?: string[];
|
|
17
|
+
skillDir?: string;
|
|
18
|
+
dreamStore?: DreamStore;
|
|
19
|
+
knowledgeSource?: KnowledgeSource;
|
|
20
|
+
signalSource?: SignalSource;
|
|
21
|
+
extensions?: Record<string, unknown>;
|
|
22
|
+
governance?: {
|
|
23
|
+
setTime?(nowMs: bigint): void;
|
|
24
|
+
evaluate(name: string, argsJson: string): {
|
|
25
|
+
kind: string;
|
|
26
|
+
reason?: string;
|
|
27
|
+
retryAfterMs?: number;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
onToolSuspend?: (event: ToolSuspendEvent) => Promise<unknown> | unknown;
|
|
31
|
+
}
|
|
32
|
+
export declare class RuntimeRunner {
|
|
33
|
+
private readonly opts;
|
|
34
|
+
private interrupted;
|
|
35
|
+
constructor(opts: RuntimeOptions);
|
|
36
|
+
interrupt(): void;
|
|
37
|
+
run(req: {
|
|
38
|
+
sessionId: string;
|
|
39
|
+
goal: string;
|
|
40
|
+
criteria?: string[];
|
|
41
|
+
extensions?: Record<string, unknown>;
|
|
42
|
+
}): AsyncIterable<StreamEvent>;
|
|
43
|
+
wake(sessionId: string, extensions?: Record<string, unknown>): AsyncIterable<StreamEvent>;
|
|
44
|
+
dream(agentId: string, nowMs?: number): Promise<DreamResult>;
|
|
45
|
+
private execute;
|
|
46
|
+
private appendObservations;
|
|
47
|
+
}
|
|
48
|
+
/** Collect all text_delta events from a run into a single string. */
|
|
49
|
+
export declare function collectText(stream: AsyncIterable<StreamEvent>): Promise<string>;
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { getKernel } from "../kernel.js";
|
|
2
|
+
export class RuntimeRunner {
|
|
3
|
+
opts;
|
|
4
|
+
interrupted = false;
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.opts = opts;
|
|
7
|
+
}
|
|
8
|
+
interrupt() { this.interrupted = true; }
|
|
9
|
+
async *run(req) {
|
|
10
|
+
const prior = await this.opts.sessionLog.read(req.sessionId);
|
|
11
|
+
const midRun = isMidRun(prior);
|
|
12
|
+
if (!midRun) {
|
|
13
|
+
await this.opts.sessionLog.append(req.sessionId, {
|
|
14
|
+
kind: "run_started",
|
|
15
|
+
run_id: crypto.randomUUID(),
|
|
16
|
+
goal: req.goal,
|
|
17
|
+
criteria: req.criteria ?? [],
|
|
18
|
+
agent_id: this.opts.agentId,
|
|
19
|
+
system_prompt: this.opts.systemPrompt,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun);
|
|
23
|
+
}
|
|
24
|
+
async *wake(sessionId, extensions) {
|
|
25
|
+
const events = await this.opts.sessionLog.read(sessionId);
|
|
26
|
+
if (events.some(e => e.event.kind === "run_terminal"))
|
|
27
|
+
return;
|
|
28
|
+
const startEntry = [...events].reverse().find(e => e.event.kind === "run_started");
|
|
29
|
+
if (!startEntry)
|
|
30
|
+
throw new Error(`No run_started event for session: ${sessionId}`);
|
|
31
|
+
const start = startEntry.event;
|
|
32
|
+
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true);
|
|
33
|
+
}
|
|
34
|
+
async dream(agentId, nowMs = Date.now()) {
|
|
35
|
+
if (!this.opts.dreamStore)
|
|
36
|
+
throw new Error("dreamStore not configured");
|
|
37
|
+
const kernel = getKernel();
|
|
38
|
+
const sessions = await this.opts.dreamStore.loadSessions(agentId);
|
|
39
|
+
const existingMemories = await this.opts.dreamStore.loadMemories(agentId);
|
|
40
|
+
if (!sessions.length)
|
|
41
|
+
return { sessionsProcessed: 0, insightsExtracted: 0, entriesAdded: 0, entriesRemoved: 0 };
|
|
42
|
+
const pipeline = new kernel.IdlePipeline(agentId);
|
|
43
|
+
const action1 = pipeline.feedTrigger(sessions.map(s => ({
|
|
44
|
+
sessionId: s.sessionId, agentId: s.agentId,
|
|
45
|
+
messages: s.messages.map(m => ({
|
|
46
|
+
role: m.role, content: m.content, tokenCount: m.tokenCount,
|
|
47
|
+
toolCalls: (m.toolCalls ?? []).map(tc => ({ id: tc.id, name: tc.name, arguments: tc.arguments })),
|
|
48
|
+
})),
|
|
49
|
+
metadata: JSON.stringify(s.metadata ?? null),
|
|
50
|
+
createdAtMs: s.createdAtMs, updatedAtMs: s.updatedAtMs,
|
|
51
|
+
})), existingMemories.map(e => ({ text: e.text, score: e.score, metadata: JSON.stringify(e.metadata ?? null) })), nowMs);
|
|
52
|
+
if (action1.kind === "noop" || action1.kind === "aborted") {
|
|
53
|
+
return { sessionsProcessed: 0, insightsExtracted: 0, entriesAdded: 0, entriesRemoved: 0 };
|
|
54
|
+
}
|
|
55
|
+
if (action1.kind !== "synthesize_insights")
|
|
56
|
+
throw new Error(`unexpected: ${action1.kind}`);
|
|
57
|
+
let synthesisText = "";
|
|
58
|
+
const providerState = this.opts.provider.createRunState?.();
|
|
59
|
+
const synthMsgs = (action1.messages ?? []);
|
|
60
|
+
const synthContext = {
|
|
61
|
+
systemText: synthMsgs.filter(m => m.role === "system").map(m => m.content).join("\n\n"),
|
|
62
|
+
turns: synthMsgs.filter(m => m.role !== "system"),
|
|
63
|
+
};
|
|
64
|
+
for await (const evt of this.opts.provider.stream(synthContext, [], undefined, providerState)) {
|
|
65
|
+
if (evt.type === "text_delta")
|
|
66
|
+
synthesisText += evt.delta;
|
|
67
|
+
}
|
|
68
|
+
const action2 = pipeline.feedSynthesisResult(synthesisText);
|
|
69
|
+
if (action2.kind !== "commit_memories")
|
|
70
|
+
throw new Error(`unexpected: ${action2.kind}`);
|
|
71
|
+
const cr = action2.curationResult;
|
|
72
|
+
const rr = action2.runResult;
|
|
73
|
+
const dsResult = {
|
|
74
|
+
toAdd: (cr.toAdd ?? []).map((e) => ({
|
|
75
|
+
text: e.text, score: e.score, metadata: tryParseJson(e.metadata),
|
|
76
|
+
})),
|
|
77
|
+
toRemoveIndices: (cr.toRemoveIndices ?? []).map(Number),
|
|
78
|
+
stats: {
|
|
79
|
+
insightsProcessed: cr.stats?.insightsProcessed ?? 0,
|
|
80
|
+
duplicatesRemoved: cr.stats?.duplicatesRemoved ?? 0,
|
|
81
|
+
conflictsResolved: cr.stats?.conflictsResolved ?? 0,
|
|
82
|
+
entriesAdded: cr.stats?.entriesAdded ?? 0,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
await this.opts.dreamStore.commit(agentId, dsResult, existingMemories);
|
|
86
|
+
return {
|
|
87
|
+
sessionsProcessed: rr.sessionsProcessed,
|
|
88
|
+
insightsExtracted: rr.insightsExtracted,
|
|
89
|
+
entriesAdded: cr.stats?.entriesAdded ?? 0,
|
|
90
|
+
entriesRemoved: (cr.toRemoveIndices ?? []).length,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false) {
|
|
94
|
+
this.interrupted = false;
|
|
95
|
+
const kernel = getKernel();
|
|
96
|
+
const ext = { ...this.opts.extensions, ...(extensions ?? {}) };
|
|
97
|
+
const providerState = this.opts.provider.createRunState?.();
|
|
98
|
+
let nextCompressedArchiveStart = nextArchivedSeqStart(priorEvents);
|
|
99
|
+
// Three-layer policy merge: explicit RuntimeOptions > provider.runtimePolicy() > defaults
|
|
100
|
+
const providerPolicy = this.opts.provider.runtimePolicy?.() ?? {};
|
|
101
|
+
const effectiveMaxTurns = this.opts.maxTurns ?? providerPolicy.maxTurns ?? 25;
|
|
102
|
+
const effectiveTimeoutMs = this.opts.timeoutMs ?? providerPolicy.timeoutMs;
|
|
103
|
+
const sm = new kernel.LoopStateMachine({
|
|
104
|
+
maxTokens: this.opts.maxTokens,
|
|
105
|
+
maxTurns: effectiveMaxTurns,
|
|
106
|
+
timeoutMs: effectiveTimeoutMs !== undefined ? BigInt(effectiveTimeoutMs) : undefined,
|
|
107
|
+
});
|
|
108
|
+
const router = new kernel.SignalRouter(256);
|
|
109
|
+
sm.setTools(this.opts.executionPlane.schemas());
|
|
110
|
+
if (this.opts.systemPrompt) {
|
|
111
|
+
sm.addSystemMessage(this.opts.systemPrompt, Math.max(1, Math.ceil(this.opts.systemPrompt.length / 4)));
|
|
112
|
+
}
|
|
113
|
+
if (this.opts.initialMemory) {
|
|
114
|
+
for (const mem of this.opts.initialMemory) {
|
|
115
|
+
sm.addMemoryMessage(mem, Math.max(1, Math.ceil(mem.length / 4)));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (this.opts.skillDir) {
|
|
119
|
+
const { scanSkillDir } = await import("../skills/loader.js");
|
|
120
|
+
const metas = await scanSkillDir(this.opts.skillDir);
|
|
121
|
+
sm.setAvailableSkills(metas.map((m) => ({
|
|
122
|
+
name: m.name, description: m.description, whenToUse: m.whenToUse,
|
|
123
|
+
effort: m.effort, estimatedTokens: m.estimatedTokens ?? 0,
|
|
124
|
+
})));
|
|
125
|
+
}
|
|
126
|
+
if (this.opts.dreamStore && this.opts.agentId)
|
|
127
|
+
sm.setMemoryEnabled(true);
|
|
128
|
+
if (this.opts.knowledgeSource)
|
|
129
|
+
sm.setKnowledgeEnabled(true);
|
|
130
|
+
if (priorEvents && priorEvents.length > 0) {
|
|
131
|
+
sm.preloadHistory(replayMessages(priorEvents));
|
|
132
|
+
}
|
|
133
|
+
const sessionStart = Date.now();
|
|
134
|
+
let action = resumeMidRun
|
|
135
|
+
? sm.resumeAfterPreload()
|
|
136
|
+
: sm.start({ goal, criteria });
|
|
137
|
+
while (!sm.isTerminal()) {
|
|
138
|
+
nextCompressedArchiveStart = await this.appendObservations(sessionId, sm, nextCompressedArchiveStart);
|
|
139
|
+
if (this.interrupted) {
|
|
140
|
+
action = sm.feedTimeout();
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
if (this.opts.signalSource) {
|
|
144
|
+
const sig = await this.opts.signalSource.nextSignal();
|
|
145
|
+
if (sig) {
|
|
146
|
+
const kernelSig = {
|
|
147
|
+
id: crypto.randomUUID(),
|
|
148
|
+
source: sig.source ?? "custom",
|
|
149
|
+
signalType: sig.signalType ?? "event",
|
|
150
|
+
urgency: sig.urgency ?? "normal",
|
|
151
|
+
summary: String(sig.payload?.goal ?? sig.kind ?? "signal"),
|
|
152
|
+
payload: JSON.stringify(sig.payload ?? {}),
|
|
153
|
+
dedupeKey: sig.dedupeKey,
|
|
154
|
+
timestampMs: Date.now(),
|
|
155
|
+
};
|
|
156
|
+
const disposition = router.ingest(kernelSig, action.kind === "execute_tools");
|
|
157
|
+
if (disposition === "interrupt_now") {
|
|
158
|
+
action = sm.feedTimeout();
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
let queued = router.next();
|
|
164
|
+
while (queued) {
|
|
165
|
+
if (queued.urgency === "critical") {
|
|
166
|
+
action = sm.feedTimeout();
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
queued = router.next();
|
|
170
|
+
}
|
|
171
|
+
if (sm.isTerminal())
|
|
172
|
+
break;
|
|
173
|
+
if (action.kind === "call_llm") {
|
|
174
|
+
const finalToolCalls = [];
|
|
175
|
+
let finalText = "";
|
|
176
|
+
const context = action.context;
|
|
177
|
+
const tools = (action.tools ?? []);
|
|
178
|
+
let turnTokens = 0;
|
|
179
|
+
try {
|
|
180
|
+
for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState)) {
|
|
181
|
+
if (evt.type === "usage") {
|
|
182
|
+
turnTokens = evt.totalTokens;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
yield evt;
|
|
186
|
+
if (evt.type === "text_delta")
|
|
187
|
+
finalText += evt.delta;
|
|
188
|
+
else if (evt.type === "tool_call") {
|
|
189
|
+
const tc = evt;
|
|
190
|
+
finalToolCalls.push({ id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
yield { type: "error", message: String(err) };
|
|
196
|
+
action = sm.feedTimeout();
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
action = sm.feedLlmResponse({
|
|
200
|
+
role: "assistant",
|
|
201
|
+
content: finalText,
|
|
202
|
+
toolCalls: finalToolCalls,
|
|
203
|
+
tokenCount: turnTokens || undefined,
|
|
204
|
+
});
|
|
205
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
206
|
+
kind: "llm_completed",
|
|
207
|
+
turn: sm.turn,
|
|
208
|
+
content: finalText,
|
|
209
|
+
token_count: turnTokens || undefined,
|
|
210
|
+
tool_calls: finalToolCalls,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else if (action.kind === "execute_tools") {
|
|
214
|
+
const allCalls = action.calls ?? [];
|
|
215
|
+
await this.opts.sessionLog.append(sessionId, { kind: "tool_requested", turn: sm.turn, calls: allCalls });
|
|
216
|
+
const runCtx = {
|
|
217
|
+
agentId: this.opts.agentId,
|
|
218
|
+
skillDir: this.opts.skillDir,
|
|
219
|
+
dreamStore: this.opts.dreamStore,
|
|
220
|
+
knowledgeSource: this.opts.knowledgeSource,
|
|
221
|
+
governance: this.opts.governance,
|
|
222
|
+
onToolSuspend: this.opts.onToolSuspend,
|
|
223
|
+
};
|
|
224
|
+
const toolResults = [];
|
|
225
|
+
for await (const evt of this.opts.executionPlane.executeAll(allCalls, runCtx)) {
|
|
226
|
+
yield evt;
|
|
227
|
+
if (evt.type === "tool_result") {
|
|
228
|
+
const tre = evt;
|
|
229
|
+
toolResults.push({ callId: tre.callId, output: tre.content, isError: tre.isError });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
233
|
+
kind: "tool_completed",
|
|
234
|
+
turn: sm.turn,
|
|
235
|
+
results: toolResults.map(r => ({
|
|
236
|
+
call_id: r.callId,
|
|
237
|
+
output: r.output,
|
|
238
|
+
is_error: r.isError,
|
|
239
|
+
token_count: r.tokenCount,
|
|
240
|
+
})),
|
|
241
|
+
});
|
|
242
|
+
action = sm.feedToolResults(toolResults);
|
|
243
|
+
}
|
|
244
|
+
else if (action.kind === "done") {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const result = action.result;
|
|
249
|
+
const status = result?.termination ?? "error";
|
|
250
|
+
const turnsUsed = result ? Math.max(1, result.turnsUsed) : 0;
|
|
251
|
+
const totalTokens = result?.totalTokensUsed ? Number(result.totalTokensUsed) : 0;
|
|
252
|
+
nextCompressedArchiveStart = await this.appendObservations(sessionId, sm, nextCompressedArchiveStart);
|
|
253
|
+
await this.opts.sessionLog.append(sessionId, { kind: "run_terminal", reason: status, turns_used: turnsUsed, total_tokens: totalTokens });
|
|
254
|
+
if (this.opts.dreamStore && this.opts.agentId) {
|
|
255
|
+
const newMsgs = sm.drainNewMessages().map(m => ({
|
|
256
|
+
role: m.role,
|
|
257
|
+
content: m.content,
|
|
258
|
+
contentParts: m.contentParts,
|
|
259
|
+
tokenCount: m.tokenCount,
|
|
260
|
+
toolCalls: m.toolCalls?.length ? m.toolCalls : undefined,
|
|
261
|
+
}));
|
|
262
|
+
if (newMsgs.length > 0) {
|
|
263
|
+
try {
|
|
264
|
+
await this.opts.dreamStore.saveSession({
|
|
265
|
+
sessionId: crypto.randomUUID(),
|
|
266
|
+
agentId: this.opts.agentId,
|
|
267
|
+
messages: newMsgs,
|
|
268
|
+
metadata: null,
|
|
269
|
+
createdAtMs: sessionStart,
|
|
270
|
+
updatedAtMs: Date.now(),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
catch { /* non-fatal */ }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
yield { type: "done", iterations: turnsUsed, totalTokens, status };
|
|
277
|
+
}
|
|
278
|
+
async appendObservations(sessionId, sm, nextArchiveStart) {
|
|
279
|
+
const observations = sm.takeObservations();
|
|
280
|
+
for (const obs of observations) {
|
|
281
|
+
if (obs.kind !== "compressed")
|
|
282
|
+
continue;
|
|
283
|
+
const latest = await this.opts.sessionLog.latestSeq(sessionId);
|
|
284
|
+
if (latest < nextArchiveStart)
|
|
285
|
+
continue;
|
|
286
|
+
const end = latest;
|
|
287
|
+
const compressedSeq = await this.opts.sessionLog.append(sessionId, {
|
|
288
|
+
kind: "compressed",
|
|
289
|
+
turn: sm.turn,
|
|
290
|
+
archived_seq_range: [nextArchiveStart, end],
|
|
291
|
+
});
|
|
292
|
+
nextArchiveStart = compressedSeq + 1;
|
|
293
|
+
}
|
|
294
|
+
return nextArchiveStart;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function isMidRun(events) {
|
|
298
|
+
return events.length > 0 && !events.some(e => e.event.kind === "run_terminal");
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Replay session events into Message[] for preloadHistory.
|
|
302
|
+
* run_started → user turn; llm_completed → assistant; tool_completed → tool messages.
|
|
303
|
+
*/
|
|
304
|
+
function replayMessages(events) {
|
|
305
|
+
const messages = [];
|
|
306
|
+
for (const { event: e } of events) {
|
|
307
|
+
if (e.kind === "run_started") {
|
|
308
|
+
const userText = e.criteria.length
|
|
309
|
+
? `${e.goal}\n\nCriteria:\n${e.criteria.map((c, i) => `${i + 1}. ${c}`).join("\n")}`
|
|
310
|
+
: e.goal;
|
|
311
|
+
messages.push({
|
|
312
|
+
role: "user",
|
|
313
|
+
content: userText,
|
|
314
|
+
toolCalls: [],
|
|
315
|
+
tokenCount: Math.max(1, Math.ceil(userText.length / 4)),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
else if (e.kind === "llm_completed") {
|
|
319
|
+
messages.push({
|
|
320
|
+
role: "assistant",
|
|
321
|
+
content: e.content,
|
|
322
|
+
toolCalls: e.tool_calls ?? [],
|
|
323
|
+
tokenCount: e.token_count,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else if (e.kind === "tool_completed") {
|
|
327
|
+
for (const r of e.results) {
|
|
328
|
+
messages.push({
|
|
329
|
+
role: "tool",
|
|
330
|
+
content: "",
|
|
331
|
+
toolCalls: [],
|
|
332
|
+
contentParts: [{ type: "tool_result", callId: r.call_id, output: r.output, isError: r.is_error ?? false }],
|
|
333
|
+
tokenCount: r.token_count,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return messages;
|
|
339
|
+
}
|
|
340
|
+
function nextArchivedSeqStart(events) {
|
|
341
|
+
let next = 0;
|
|
342
|
+
for (const { event } of events ?? []) {
|
|
343
|
+
if (event.kind === "compressed")
|
|
344
|
+
next = Math.max(next, event.archived_seq_range[1] + 1);
|
|
345
|
+
}
|
|
346
|
+
return next;
|
|
347
|
+
}
|
|
348
|
+
function tryParseJson(s) {
|
|
349
|
+
try {
|
|
350
|
+
return JSON.parse(s);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/** Collect all text_delta events from a run into a single string. */
|
|
357
|
+
export async function collectText(stream) {
|
|
358
|
+
let text = "";
|
|
359
|
+
for await (const evt of stream) {
|
|
360
|
+
if (evt.type === "text_delta")
|
|
361
|
+
text += evt.delta;
|
|
362
|
+
}
|
|
363
|
+
return text;
|
|
364
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ToolCall } from "../types.js";
|
|
2
|
+
export type SessionEvent = {
|
|
3
|
+
kind: "run_started";
|
|
4
|
+
run_id: string;
|
|
5
|
+
goal: string;
|
|
6
|
+
criteria: string[];
|
|
7
|
+
agent_id?: string;
|
|
8
|
+
system_prompt?: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "llm_completed";
|
|
11
|
+
turn: number;
|
|
12
|
+
content: string;
|
|
13
|
+
token_count?: number;
|
|
14
|
+
tool_calls: ToolCall[];
|
|
15
|
+
} | {
|
|
16
|
+
kind: "tool_requested";
|
|
17
|
+
turn: number;
|
|
18
|
+
calls: ToolCall[];
|
|
19
|
+
} | {
|
|
20
|
+
kind: "tool_completed";
|
|
21
|
+
turn: number;
|
|
22
|
+
results: Array<{
|
|
23
|
+
call_id: string;
|
|
24
|
+
output: string;
|
|
25
|
+
is_error?: boolean;
|
|
26
|
+
token_count?: number;
|
|
27
|
+
}>;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "compressed";
|
|
30
|
+
turn: number;
|
|
31
|
+
archived_seq_range: [number, number];
|
|
32
|
+
} | {
|
|
33
|
+
kind: "run_terminal";
|
|
34
|
+
reason: string;
|
|
35
|
+
turns_used: number;
|
|
36
|
+
total_tokens: number;
|
|
37
|
+
};
|
|
38
|
+
export interface SessionLog {
|
|
39
|
+
append(sessionId: string, event: SessionEvent): Promise<number>;
|
|
40
|
+
read(sessionId: string, fromSeq?: number): Promise<Array<{
|
|
41
|
+
seq: number;
|
|
42
|
+
event: SessionEvent;
|
|
43
|
+
}>>;
|
|
44
|
+
latestSeq(sessionId: string): Promise<number>;
|
|
45
|
+
}
|
|
46
|
+
export declare class InMemorySessionLog implements SessionLog {
|
|
47
|
+
private store;
|
|
48
|
+
append(sessionId: string, event: SessionEvent): Promise<number>;
|
|
49
|
+
read(sessionId: string, fromSeq?: number): Promise<Array<{
|
|
50
|
+
seq: number;
|
|
51
|
+
event: SessionEvent;
|
|
52
|
+
}>>;
|
|
53
|
+
latestSeq(sessionId: string): Promise<number>;
|
|
54
|
+
}
|
|
55
|
+
export declare class FileSessionLog implements SessionLog {
|
|
56
|
+
private dir;
|
|
57
|
+
private seqCounters;
|
|
58
|
+
constructor(dir: string);
|
|
59
|
+
private path;
|
|
60
|
+
private nextSeq;
|
|
61
|
+
append(sessionId: string, event: SessionEvent): Promise<number>;
|
|
62
|
+
read(sessionId: string, fromSeq?: number): Promise<Array<{
|
|
63
|
+
seq: number;
|
|
64
|
+
event: SessionEvent;
|
|
65
|
+
}>>;
|
|
66
|
+
latestSeq(sessionId: string): Promise<number>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createWriteStream, createReadStream } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
export class InMemorySessionLog {
|
|
6
|
+
store = new Map();
|
|
7
|
+
async append(sessionId, event) {
|
|
8
|
+
if (!this.store.has(sessionId))
|
|
9
|
+
this.store.set(sessionId, []);
|
|
10
|
+
const entries = this.store.get(sessionId);
|
|
11
|
+
const seq = entries.length;
|
|
12
|
+
entries.push({ seq, event });
|
|
13
|
+
return seq;
|
|
14
|
+
}
|
|
15
|
+
async read(sessionId, fromSeq = 0) {
|
|
16
|
+
return (this.store.get(sessionId) ?? []).filter(e => e.seq >= fromSeq);
|
|
17
|
+
}
|
|
18
|
+
async latestSeq(sessionId) {
|
|
19
|
+
const entries = this.store.get(sessionId);
|
|
20
|
+
return entries ? entries.length - 1 : -1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Single-writer per session. Safe for concurrent appends within one instance.
|
|
24
|
+
// Cross-instance (multi-process) safety requires an external lock.
|
|
25
|
+
export class FileSessionLog {
|
|
26
|
+
dir;
|
|
27
|
+
// Lazy-initialized per-session counter. Avoids re-reading the file on every append.
|
|
28
|
+
seqCounters = new Map();
|
|
29
|
+
constructor(dir) {
|
|
30
|
+
this.dir = dir;
|
|
31
|
+
}
|
|
32
|
+
path(sessionId) {
|
|
33
|
+
return join(this.dir, `${sessionId}.jsonl`);
|
|
34
|
+
}
|
|
35
|
+
async nextSeq(sessionId) {
|
|
36
|
+
if (!this.seqCounters.has(sessionId)) {
|
|
37
|
+
const existing = await this.read(sessionId);
|
|
38
|
+
this.seqCounters.set(sessionId, existing.length);
|
|
39
|
+
}
|
|
40
|
+
const seq = this.seqCounters.get(sessionId);
|
|
41
|
+
this.seqCounters.set(sessionId, seq + 1);
|
|
42
|
+
return seq;
|
|
43
|
+
}
|
|
44
|
+
async append(sessionId, event) {
|
|
45
|
+
await mkdir(this.dir, { recursive: true });
|
|
46
|
+
const seq = await this.nextSeq(sessionId);
|
|
47
|
+
await new Promise((resolve, reject) => {
|
|
48
|
+
const ws = createWriteStream(this.path(sessionId), { flags: "a" });
|
|
49
|
+
ws.write(JSON.stringify({ seq, event }) + "\n", err => {
|
|
50
|
+
if (err)
|
|
51
|
+
reject(err);
|
|
52
|
+
else
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
ws.end();
|
|
56
|
+
});
|
|
57
|
+
return seq;
|
|
58
|
+
}
|
|
59
|
+
async read(sessionId, fromSeq = 0) {
|
|
60
|
+
const results = [];
|
|
61
|
+
try {
|
|
62
|
+
const rl = createInterface({
|
|
63
|
+
input: createReadStream(this.path(sessionId)),
|
|
64
|
+
crlfDelay: Infinity,
|
|
65
|
+
});
|
|
66
|
+
for await (const line of rl) {
|
|
67
|
+
if (!line.trim())
|
|
68
|
+
continue;
|
|
69
|
+
const entry = JSON.parse(line);
|
|
70
|
+
if (entry.seq >= fromSeq)
|
|
71
|
+
results.push(entry);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err.code !== "ENOENT")
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
async latestSeq(sessionId) {
|
|
81
|
+
const entries = await this.read(sessionId);
|
|
82
|
+
return entries.length - 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type { ToolSchema, ToolResult } from "../types.js";
|
|
1
|
+
import type { ToolChunk, ToolSchema, ToolResult } from "../types.js";
|
|
2
2
|
export interface RegisteredTool {
|
|
3
3
|
schema: ToolSchema;
|
|
4
|
-
execute(args: Record<string, unknown>): Promise<string>;
|
|
4
|
+
execute(args: Record<string, unknown>): Promise<string> | AsyncIterable<ToolChunk>;
|
|
5
5
|
}
|
|
6
6
|
export declare function tool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown>) => Promise<string> | string): RegisteredTool;
|
|
7
|
+
export declare function streamingTool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown>) => AsyncIterable<ToolChunk>): RegisteredTool;
|
|
8
|
+
export declare function isAsyncIterable<T>(value: unknown): value is AsyncIterable<T>;
|
|
9
|
+
export declare function normalizeToolChunk(chunk: ToolChunk): Exclude<ToolChunk, string>;
|
|
10
|
+
export declare function toolChunkText(chunk: ToolChunk): string;
|
|
11
|
+
export declare function validateToolArguments(schemaJson: string, args: Record<string, unknown>): string | undefined;
|
|
7
12
|
export declare function executeTools(calls: {
|
|
8
13
|
id: string;
|
|
9
14
|
name: string;
|
package/dist/tools/index.js
CHANGED
|
@@ -4,6 +4,80 @@ export function tool(name, description, parameters, fn) {
|
|
|
4
4
|
async execute(args) { return fn(args); },
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
|
+
export function streamingTool(name, description, parameters, fn) {
|
|
8
|
+
return {
|
|
9
|
+
schema: { name, description, parameters: JSON.stringify(parameters) },
|
|
10
|
+
execute(args) { return fn(args); },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function isAsyncIterable(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
15
|
+
}
|
|
16
|
+
export function normalizeToolChunk(chunk) {
|
|
17
|
+
return typeof chunk === "string" ? { type: "text", text: chunk } : chunk;
|
|
18
|
+
}
|
|
19
|
+
export function toolChunkText(chunk) {
|
|
20
|
+
const normalized = normalizeToolChunk(chunk);
|
|
21
|
+
return normalized.type === "text" ? normalized.text : "";
|
|
22
|
+
}
|
|
23
|
+
export function validateToolArguments(schemaJson, args) {
|
|
24
|
+
let schema;
|
|
25
|
+
try {
|
|
26
|
+
schema = JSON.parse(schemaJson);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "invalid tool schema";
|
|
30
|
+
}
|
|
31
|
+
return validateValue(schema, args, "$", true);
|
|
32
|
+
}
|
|
33
|
+
function validateValue(schema, value, path, isRoot = false) {
|
|
34
|
+
const expectedType = schema.type;
|
|
35
|
+
if (typeof expectedType === "string") {
|
|
36
|
+
if (expectedType === "object") {
|
|
37
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
38
|
+
return `${path} must be object`;
|
|
39
|
+
const obj = value;
|
|
40
|
+
for (const required of schema.required ?? []) {
|
|
41
|
+
if (!(required in obj))
|
|
42
|
+
return `${path}.${required} is required`;
|
|
43
|
+
}
|
|
44
|
+
const properties = schema.properties ?? {};
|
|
45
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
46
|
+
if (key in obj) {
|
|
47
|
+
const err = validateValue(child, obj[key], `${path}.${key}`);
|
|
48
|
+
if (err)
|
|
49
|
+
return err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (expectedType === "array") {
|
|
54
|
+
if (!Array.isArray(value))
|
|
55
|
+
return `${path} must be array`;
|
|
56
|
+
}
|
|
57
|
+
else if (expectedType === "string") {
|
|
58
|
+
if (typeof value !== "string")
|
|
59
|
+
return `${path} must be string`;
|
|
60
|
+
}
|
|
61
|
+
else if (expectedType === "number") {
|
|
62
|
+
if (typeof value !== "number" || Number.isNaN(value))
|
|
63
|
+
return `${path} must be number`;
|
|
64
|
+
}
|
|
65
|
+
else if (expectedType === "integer") {
|
|
66
|
+
if (!Number.isInteger(value))
|
|
67
|
+
return `${path} must be integer`;
|
|
68
|
+
}
|
|
69
|
+
else if (expectedType === "boolean") {
|
|
70
|
+
if (typeof value !== "boolean")
|
|
71
|
+
return `${path} must be boolean`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (isRoot && (!value || typeof value !== "object" || Array.isArray(value))) {
|
|
75
|
+
return `${path} must be object`;
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value))
|
|
78
|
+
return `${path} must be one of enum values`;
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
7
81
|
export async function executeTools(calls, registry) {
|
|
8
82
|
return Promise.all(calls.map(async (c) => {
|
|
9
83
|
const t = registry.get(c.name);
|
|
@@ -11,7 +85,16 @@ export async function executeTools(calls, registry) {
|
|
|
11
85
|
return { callId: c.id, output: `unknown tool: ${c.name}`, isError: true };
|
|
12
86
|
try {
|
|
13
87
|
const args = JSON.parse(c.arguments || "{}");
|
|
88
|
+
const validationError = validateToolArguments(t.schema.parameters, args);
|
|
89
|
+
if (validationError)
|
|
90
|
+
return { callId: c.id, output: `invalid arguments: ${validationError}`, isError: true };
|
|
14
91
|
const output = await t.execute(args);
|
|
92
|
+
if (isAsyncIterable(output)) {
|
|
93
|
+
let combined = "";
|
|
94
|
+
for await (const chunk of output)
|
|
95
|
+
combined += toolChunkText(chunk);
|
|
96
|
+
return { callId: c.id, output: combined, isError: false };
|
|
97
|
+
}
|
|
15
98
|
return { callId: c.id, output, isError: false };
|
|
16
99
|
}
|
|
17
100
|
catch (err) {
|