@fabric-harness/sdk 0.1.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 +202 -0
- package/README.md +73 -0
- package/dist/agent-definition.d.ts +61 -0
- package/dist/agent-definition.d.ts.map +1 -0
- package/dist/agent-definition.js +52 -0
- package/dist/agent-definition.js.map +1 -0
- package/dist/agent.d.ts +13 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +99 -0
- package/dist/agent.js.map +1 -0
- package/dist/context-budget.d.ts +20 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +27 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +8 -0
- package/dist/context.js.map +1 -0
- package/dist/error.d.ts +25 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +48 -0
- package/dist/error.js.map +1 -0
- package/dist/filesystem-source.d.ts +69 -0
- package/dist/filesystem-source.d.ts.map +1 -0
- package/dist/filesystem-source.js +109 -0
- package/dist/filesystem-source.js.map +1 -0
- package/dist/history.d.ts +16 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +118 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/lite.d.ts +46 -0
- package/dist/lite.d.ts.map +1 -0
- package/dist/lite.js +38 -0
- package/dist/lite.js.map +1 -0
- package/dist/loop.d.ts +41 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +94 -0
- package/dist/loop.js.map +1 -0
- package/dist/mcp-stdio.d.ts +28 -0
- package/dist/mcp-stdio.d.ts.map +1 -0
- package/dist/mcp-stdio.js +102 -0
- package/dist/mcp-stdio.js.map +1 -0
- package/dist/mcp.d.ts +40 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +153 -0
- package/dist/mcp.js.map +1 -0
- package/dist/model.d.ts +204 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +661 -0
- package/dist/model.js.map +1 -0
- package/dist/otel.d.ts +17 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +25 -0
- package/dist/otel.js.map +1 -0
- package/dist/policy.d.ts +54 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +148 -0
- package/dist/policy.js.map +1 -0
- package/dist/providers.d.ts +37 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +265 -0
- package/dist/providers.js.map +1 -0
- package/dist/redaction.d.ts +11 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +41 -0
- package/dist/redaction.js.map +1 -0
- package/dist/result.d.ts +7 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +105 -0
- package/dist/result.js.map +1 -0
- package/dist/runtime-mode.d.ts +30 -0
- package/dist/runtime-mode.d.ts.map +1 -0
- package/dist/runtime-mode.js +42 -0
- package/dist/runtime-mode.js.map +1 -0
- package/dist/sandbox.d.ts +247 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +922 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/schema.d.ts +37 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +121 -0
- package/dist/schema.js.map +1 -0
- package/dist/session.d.ts +60 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +762 -0
- package/dist/session.js.map +1 -0
- package/dist/store.d.ts +48 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +348 -0
- package/dist/store.js.map +1 -0
- package/dist/telemetry.d.ts +15 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +51 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tools.d.ts +131 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +235 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +71 -0
package/dist/session.js
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { FabricError } from './error.js';
|
|
3
|
+
import { buildModelMessagesFromHistory } from './history.js';
|
|
4
|
+
import { defaultLoopRuntime } from './loop.js';
|
|
5
|
+
import { defaultModelProvider, generateWithRuntime } from './model.js';
|
|
6
|
+
import { clampCommandTimeout, evaluateCommandPolicy, evaluateToolCallPolicy } from './policy.js';
|
|
7
|
+
import { buildResultRetryPrompt, validateResult } from './result.js';
|
|
8
|
+
import { evaluateContextBudget, isContextOverflowError } from './context-budget.js';
|
|
9
|
+
import { createSandboxEnv } from './sandbox.js';
|
|
10
|
+
import { defaultSessionStore } from './store.js';
|
|
11
|
+
import { createBuiltinTools, createCommandTools, shellQuote } from './tools.js';
|
|
12
|
+
let nextTaskId = 0;
|
|
13
|
+
export class StubFabricSession {
|
|
14
|
+
id;
|
|
15
|
+
agent;
|
|
16
|
+
sandbox;
|
|
17
|
+
checkpoint = {
|
|
18
|
+
create: (options) => this.createCheckpoint(options),
|
|
19
|
+
restore: (options) => this.restoreCheckpoint(options),
|
|
20
|
+
};
|
|
21
|
+
options;
|
|
22
|
+
store;
|
|
23
|
+
eventSubscribers = new Set();
|
|
24
|
+
constructor(agent, id, options = {}) {
|
|
25
|
+
this.agent = agent;
|
|
26
|
+
this.id = id;
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.store = options.store ?? defaultSessionStore;
|
|
29
|
+
this.sandbox = this.createSandbox();
|
|
30
|
+
}
|
|
31
|
+
async prompt(text, options = {}) {
|
|
32
|
+
const role = this.resolveRole(options.role ?? this.options.role ?? this.agent.role);
|
|
33
|
+
const promptSubscriber = options.onEvent;
|
|
34
|
+
if (promptSubscriber)
|
|
35
|
+
this.eventSubscribers.add(promptSubscriber);
|
|
36
|
+
try {
|
|
37
|
+
await this.emit('prompt_start', { text, ...(role ? { role: role.name } : {}) });
|
|
38
|
+
const sandbox = await this.sandbox;
|
|
39
|
+
const tools = this.resolveTools(sandbox, options.tools, options.commands);
|
|
40
|
+
await this.recordEntry('user_prompt', { text, ...(role ? { role: role.name } : {}) });
|
|
41
|
+
let messages = buildModelMessagesFromHistory(await this.history(), role);
|
|
42
|
+
messages = await this.autoCompactIfNeeded(messages, role, options);
|
|
43
|
+
if (options.toolCalls?.length) {
|
|
44
|
+
const results = await this.executeToolCalls(options.toolCalls, tools, sandbox, options.policy, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval);
|
|
45
|
+
await this.recordEntry('result', { result: results });
|
|
46
|
+
await this.emit('prompt_end', { text, result: results });
|
|
47
|
+
return validateResult(results, options.result, options.resultExtraction);
|
|
48
|
+
}
|
|
49
|
+
const runtime = options.loopRuntime ?? this.options.loopRuntime ?? defaultLoopRuntime;
|
|
50
|
+
const runRuntime = () => runtime.runPrompt({
|
|
51
|
+
sessionId: this.id,
|
|
52
|
+
text,
|
|
53
|
+
messages,
|
|
54
|
+
tools,
|
|
55
|
+
sandbox,
|
|
56
|
+
...(typeof (options.model ?? this.options.model ?? this.agent.model) === 'string' ? { model: (options.model ?? this.options.model ?? this.agent.model) } : {}),
|
|
57
|
+
...((options.modelProvider ?? this.options.modelProvider) !== undefined ? { modelProvider: options.modelProvider ?? this.options.modelProvider } : {}),
|
|
58
|
+
...(options.maxIterations !== undefined ? { maxIterations: options.maxIterations } : {}),
|
|
59
|
+
...(options.modelTimeoutMs !== undefined ? { modelTimeoutMs: options.modelTimeoutMs } : {}),
|
|
60
|
+
...(options.modelRetries !== undefined ? { modelRetries: options.modelRetries } : {}),
|
|
61
|
+
...(options.modelRetryDelayMs !== undefined ? { modelRetryDelayMs: options.modelRetryDelayMs } : {}),
|
|
62
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
63
|
+
...(options.policy !== undefined ? { policy: options.policy } : {}),
|
|
64
|
+
...((options.approvalTimeoutMs ?? this.options.approvalTimeoutMs) !== undefined ? { approvalTimeoutMs: options.approvalTimeoutMs ?? this.options.approvalTimeoutMs } : {}),
|
|
65
|
+
executeToolCalls: (calls, policy, approvalTimeoutMs) => this.executeToolCalls(calls, tools, sandbox, policy, approvalTimeoutMs, options.onApproval),
|
|
66
|
+
recordEntry: (type, data) => this.recordEntry(type, data),
|
|
67
|
+
emit: (type, data) => this.emit(type, data),
|
|
68
|
+
});
|
|
69
|
+
let result;
|
|
70
|
+
try {
|
|
71
|
+
result = await runRuntime();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (!isContextOverflowError(error) || options.recoverContextOverflow === false)
|
|
75
|
+
throw error;
|
|
76
|
+
await this.emit('error', { error: 'Context overflow detected; compacting and retrying once.' });
|
|
77
|
+
await this.compact({ keepRecentEntries: options.compactionKeepRecentEntries ?? 20, generateSummary: true });
|
|
78
|
+
messages = buildModelMessagesFromHistory(await this.history(), role);
|
|
79
|
+
result = await runRuntime();
|
|
80
|
+
}
|
|
81
|
+
const resultRetries = options.resultRetries ?? (options.result ? 1 : 0);
|
|
82
|
+
for (let attempt = 0; attempt <= resultRetries; attempt += 1) {
|
|
83
|
+
try {
|
|
84
|
+
return await validateResult(result.raw, options.result, options.resultExtraction);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (attempt >= resultRetries)
|
|
88
|
+
throw error;
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
await this.recordEntry('result_retry', { error: message, retryAttempt: attempt + 1, maxRetries: resultRetries });
|
|
91
|
+
await this.emit('result_retry', { error: message, retryAttempt: attempt + 1, maxRetries: resultRetries });
|
|
92
|
+
messages.push({ role: 'user', content: buildResultRetryPrompt(error, options.resultExtraction) });
|
|
93
|
+
result = await runRuntime();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new FabricError({ code: 'RESULT_VALIDATION_FAILED', message: 'Typed result validation failed.', retryable: true });
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
if (promptSubscriber)
|
|
100
|
+
this.eventSubscribers.delete(promptSubscriber);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async *stream(text, options = {}) {
|
|
104
|
+
const events = [];
|
|
105
|
+
let notify;
|
|
106
|
+
let settled = false;
|
|
107
|
+
const wake = () => {
|
|
108
|
+
notify?.();
|
|
109
|
+
notify = undefined;
|
|
110
|
+
};
|
|
111
|
+
const subscriber = (event) => {
|
|
112
|
+
events.push(event);
|
|
113
|
+
wake();
|
|
114
|
+
};
|
|
115
|
+
this.eventSubscribers.add(subscriber);
|
|
116
|
+
let result;
|
|
117
|
+
let failure;
|
|
118
|
+
const resultPromise = this.prompt(text, options)
|
|
119
|
+
.then((value) => {
|
|
120
|
+
result = value;
|
|
121
|
+
})
|
|
122
|
+
.catch((error) => {
|
|
123
|
+
failure = error;
|
|
124
|
+
})
|
|
125
|
+
.finally(() => {
|
|
126
|
+
settled = true;
|
|
127
|
+
this.eventSubscribers.delete(subscriber);
|
|
128
|
+
wake();
|
|
129
|
+
});
|
|
130
|
+
while (!settled || events.length > 0) {
|
|
131
|
+
const event = events.shift();
|
|
132
|
+
if (event) {
|
|
133
|
+
yield event;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
await new Promise((resolve) => {
|
|
137
|
+
notify = resolve;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
await resultPromise;
|
|
141
|
+
if (failure)
|
|
142
|
+
throw failure;
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
async skill(name, options = {}) {
|
|
146
|
+
const skill = this.resolveSkill(name);
|
|
147
|
+
await this.recordEntry('skill_start', { skill: name });
|
|
148
|
+
await this.emit('skill_start', { skill: name });
|
|
149
|
+
if (!skill)
|
|
150
|
+
throw new FabricError({ code: 'SKILL_NOT_FOUND', message: `Skill not found: ${name}`, details: { skill: name } });
|
|
151
|
+
const argsText = options.args ? `\n\nArguments:\n${JSON.stringify(options.args, null, 2)}` : '';
|
|
152
|
+
const result = await this.prompt(`${skill.content ?? ''}${argsText}`, options);
|
|
153
|
+
await this.recordEntry('skill_end', { skill: name });
|
|
154
|
+
await this.emit('skill_end', { skill: name });
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
async task(text, options = {}) {
|
|
158
|
+
const taskId = options.id ?? `${this.id}:task-${++nextTaskId}`;
|
|
159
|
+
const startedAt = new Date().toISOString();
|
|
160
|
+
await this.recordEntry('task_start', { taskId, text, status: 'running', startedAt, ...(options.agent ? { agent: options.agent } : {}), ...(options.metadata ? { metadata: options.metadata } : {}) });
|
|
161
|
+
await this.emit('task_start', { taskId, text, status: 'running', startedAt });
|
|
162
|
+
const checkpointLabel = typeof options.checkpoint === 'string' ? options.checkpoint : `task:${taskId}`;
|
|
163
|
+
try {
|
|
164
|
+
if (options.signal?.aborted)
|
|
165
|
+
throw abortError();
|
|
166
|
+
if (options.checkpoint) {
|
|
167
|
+
const checkpoint = await this.createCheckpoint({ label: `${checkpointLabel}:before`, metadata: { taskId, phase: 'before' } });
|
|
168
|
+
await this.recordEntry('task_checkpoint', { taskId, phase: 'before', checkpoint: checkpoint, ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}) });
|
|
169
|
+
await this.emit('task_checkpoint', { taskId, phase: 'before', ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}) });
|
|
170
|
+
}
|
|
171
|
+
const taskAbort = new AbortController();
|
|
172
|
+
const stopCancellationWatcher = this.watchTaskCancellation(taskId, taskAbort);
|
|
173
|
+
const forwardAbort = () => taskAbort.abort(options.signal?.reason);
|
|
174
|
+
if (options.signal)
|
|
175
|
+
options.signal.addEventListener('abort', forwardAbort, { once: true });
|
|
176
|
+
try {
|
|
177
|
+
const child = new StubFabricSession(this.agent, taskId, {
|
|
178
|
+
...this.options,
|
|
179
|
+
...options,
|
|
180
|
+
store: this.store,
|
|
181
|
+
sandbox: await this.sandbox,
|
|
182
|
+
});
|
|
183
|
+
const result = await child.prompt(text, { ...options, signal: taskAbort.signal });
|
|
184
|
+
if (taskAbort.signal.aborted)
|
|
185
|
+
throw abortError();
|
|
186
|
+
if (options.checkpoint) {
|
|
187
|
+
const checkpoint = await this.createCheckpoint({ label: `${checkpointLabel}:after`, metadata: { taskId, phase: 'after' } });
|
|
188
|
+
await this.recordEntry('task_checkpoint', { taskId, phase: 'after', checkpoint: checkpoint, ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}) });
|
|
189
|
+
await this.emit('task_checkpoint', { taskId, phase: 'after', ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}) });
|
|
190
|
+
}
|
|
191
|
+
const completedAt = new Date().toISOString();
|
|
192
|
+
await this.recordEntry('task_end', { taskId, status: 'completed', completedAt });
|
|
193
|
+
await this.emit('task_end', { taskId, status: 'completed', completedAt });
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
stopCancellationWatcher();
|
|
198
|
+
if (options.signal)
|
|
199
|
+
options.signal.removeEventListener('abort', forwardAbort);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (isAbortError(error) || options.signal?.aborted) {
|
|
204
|
+
const cancelledAt = new Date().toISOString();
|
|
205
|
+
const message = error instanceof Error ? error.message : 'Task cancelled';
|
|
206
|
+
await this.recordEntry('task_cancelled', { taskId, status: 'cancelled', cancelledAt, reason: message });
|
|
207
|
+
await this.emit('task_cancelled', { taskId, status: 'cancelled', cancelledAt, reason: message });
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
const failedAt = new Date().toISOString();
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
await this.recordEntry('task_failed', { taskId, status: 'failed', failedAt, error: message });
|
|
213
|
+
await this.emit('task_failed', { taskId, status: 'failed', failedAt, error: message });
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
watchTaskCancellation(taskId, controller) {
|
|
218
|
+
const interval = setInterval(() => {
|
|
219
|
+
void (async () => {
|
|
220
|
+
if (controller.signal.aborted)
|
|
221
|
+
return;
|
|
222
|
+
const entries = (await this.store.load(this.id))?.entries ?? [];
|
|
223
|
+
if (entries.some((entry) => entry.type === 'task_cancelled' && entry.data?.taskId === taskId))
|
|
224
|
+
controller.abort(abortError());
|
|
225
|
+
})().catch(() => undefined);
|
|
226
|
+
}, 500);
|
|
227
|
+
return () => clearInterval(interval);
|
|
228
|
+
}
|
|
229
|
+
async shell(command, options = {}) {
|
|
230
|
+
const sandbox = await this.sandbox;
|
|
231
|
+
const execOptions = {};
|
|
232
|
+
const policy = options.policy ?? this.options.policy;
|
|
233
|
+
const decision = evaluateCommandPolicy(command, policy);
|
|
234
|
+
if (!decision.allowed) {
|
|
235
|
+
if (decision.approvalRequired) {
|
|
236
|
+
const approved = await this.requestApproval({
|
|
237
|
+
kind: 'command',
|
|
238
|
+
command,
|
|
239
|
+
input: { command, ...(options.cwd ? { cwd: options.cwd } : {}) },
|
|
240
|
+
reason: decision.reason ?? `Approval required for command: ${command}`,
|
|
241
|
+
...(decision.risk ? { risk: decision.risk } : {}),
|
|
242
|
+
...(decision.matchedPattern ? { matchedPattern: decision.matchedPattern } : {}),
|
|
243
|
+
}, policy, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval);
|
|
244
|
+
if (!approved) {
|
|
245
|
+
const message = decision.reason ?? 'Command approval denied or unavailable';
|
|
246
|
+
await this.emit('error', { reason: message, command, approvalRequired: true });
|
|
247
|
+
throw new FabricError({ code: 'COMMAND_DENIED', message, details: { command, approvalRequired: true } });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const message = decision.reason ?? 'Command denied by policy';
|
|
252
|
+
await this.emit('error', { reason: message, command });
|
|
253
|
+
throw new FabricError({ code: 'COMMAND_DENIED', message, details: { command } });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (options.cwd !== undefined)
|
|
257
|
+
execOptions.cwd = options.cwd;
|
|
258
|
+
if (options.env !== undefined)
|
|
259
|
+
execOptions.env = options.env;
|
|
260
|
+
if (options.signal !== undefined)
|
|
261
|
+
execOptions.signal = options.signal;
|
|
262
|
+
const timeout = clampCommandTimeout(options.timeout, policy);
|
|
263
|
+
if (timeout !== undefined)
|
|
264
|
+
execOptions.timeout = timeout;
|
|
265
|
+
await this.recordEntry('shell_command', { command });
|
|
266
|
+
await this.emit('command_start', { command });
|
|
267
|
+
const result = await sandbox.exec(command, execOptions);
|
|
268
|
+
await this.recordEntry('shell_result', { command, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}) });
|
|
269
|
+
await this.emit('command_end', { command, exitCode: result.exitCode, ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}) });
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
async history() {
|
|
273
|
+
return this.ensureData();
|
|
274
|
+
}
|
|
275
|
+
async artifact(name, content, options = {}) {
|
|
276
|
+
const bytes = typeof content === 'string' ? new TextEncoder().encode(content) : content;
|
|
277
|
+
const fallback = {
|
|
278
|
+
id: `${Date.now()}-${name.replace(/[^a-zA-Z0-9._-]/g, '_')}`,
|
|
279
|
+
sessionId: this.id,
|
|
280
|
+
name,
|
|
281
|
+
path: name,
|
|
282
|
+
size: bytes.byteLength,
|
|
283
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
284
|
+
createdAt: new Date().toISOString(),
|
|
285
|
+
...(options.contentType ? { contentType: options.contentType } : {}),
|
|
286
|
+
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
287
|
+
};
|
|
288
|
+
const ref = this.store.putArtifact ? await this.store.putArtifact(this.id, name, bytes, options) : fallback;
|
|
289
|
+
await this.recordEntry('artifact_created', { artifact: ref });
|
|
290
|
+
await this.emit('artifact_created', { artifact: ref });
|
|
291
|
+
return ref;
|
|
292
|
+
}
|
|
293
|
+
async compact(options = {}) {
|
|
294
|
+
const history = await this.history();
|
|
295
|
+
const entries = history.entries ?? [];
|
|
296
|
+
const keepRecentEntries = options.keepRecentEntries ?? 20;
|
|
297
|
+
const compactedEntries = Math.max(0, entries.length - keepRecentEntries);
|
|
298
|
+
const firstKeptEntry = compactedEntries > 0 ? entries[compactedEntries] : undefined;
|
|
299
|
+
const compacted = compactedEntries > 0 ? entries.slice(0, compactedEntries) : [];
|
|
300
|
+
const fileRefs = collectFileReferences(compacted);
|
|
301
|
+
const baseSummary = options.summary ?? (options.generateSummary ? await this.generateCompactionSummary(compacted) : summarizeEntries(compacted));
|
|
302
|
+
const summary = appendFileReferenceTags(baseSummary, fileRefs);
|
|
303
|
+
const entry = await this.recordEntry('compaction', {
|
|
304
|
+
summary,
|
|
305
|
+
compactedEntries,
|
|
306
|
+
...(fileRefs.readFiles.length > 0 ? { readFiles: fileRefs.readFiles } : {}),
|
|
307
|
+
...(fileRefs.modifiedFiles.length > 0 ? { modifiedFiles: fileRefs.modifiedFiles } : {}),
|
|
308
|
+
...(firstKeptEntry ? { firstKeptEntryId: firstKeptEntry.id } : {}),
|
|
309
|
+
});
|
|
310
|
+
await this.emit('compaction', { summary, compactedEntries });
|
|
311
|
+
return {
|
|
312
|
+
entryId: entry.id,
|
|
313
|
+
summary,
|
|
314
|
+
compactedEntries,
|
|
315
|
+
...(firstKeptEntry ? { firstKeptEntryId: firstKeptEntry.id } : {}),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async autoCompactIfNeeded(messages, role, options) {
|
|
319
|
+
if (options.autoCompact === false)
|
|
320
|
+
return messages;
|
|
321
|
+
const budget = evaluateContextBudget(messages, {
|
|
322
|
+
...(options.contextWindowTokens !== undefined ? { contextWindowTokens: options.contextWindowTokens } : {}),
|
|
323
|
+
...(options.compactAtTokens !== undefined ? { compactAtTokens: options.compactAtTokens } : {}),
|
|
324
|
+
...(options.reserveTokens !== undefined ? { reserveTokens: options.reserveTokens } : {}),
|
|
325
|
+
});
|
|
326
|
+
if (!budget.shouldCompact)
|
|
327
|
+
return messages;
|
|
328
|
+
await this.emit('compaction', {
|
|
329
|
+
phase: 'auto_start',
|
|
330
|
+
estimatedTokens: budget.estimatedTokens,
|
|
331
|
+
compactAtTokens: budget.compactAtTokens ?? 0,
|
|
332
|
+
});
|
|
333
|
+
await this.compact({ keepRecentEntries: options.compactionKeepRecentEntries ?? 20, generateSummary: true });
|
|
334
|
+
const compacted = buildModelMessagesFromHistory(await this.history(), role);
|
|
335
|
+
const compactedBudget = evaluateContextBudget(compacted, {
|
|
336
|
+
...(options.contextWindowTokens !== undefined ? { contextWindowTokens: options.contextWindowTokens } : {}),
|
|
337
|
+
...(options.compactAtTokens !== undefined ? { compactAtTokens: options.compactAtTokens } : {}),
|
|
338
|
+
...(options.reserveTokens !== undefined ? { reserveTokens: options.reserveTokens } : {}),
|
|
339
|
+
});
|
|
340
|
+
await this.emit('compaction', {
|
|
341
|
+
phase: 'auto_end',
|
|
342
|
+
estimatedTokens: compactedBudget.estimatedTokens,
|
|
343
|
+
compactAtTokens: compactedBudget.compactAtTokens ?? 0,
|
|
344
|
+
});
|
|
345
|
+
return compacted;
|
|
346
|
+
}
|
|
347
|
+
async generateCompactionSummary(entries) {
|
|
348
|
+
if (entries.length === 0)
|
|
349
|
+
return summarizeEntries(entries);
|
|
350
|
+
const deterministic = summarizeEntries(entries);
|
|
351
|
+
const provider = this.options.modelProvider ?? defaultModelProvider;
|
|
352
|
+
try {
|
|
353
|
+
const response = await generateWithRuntime(provider, {
|
|
354
|
+
...(this.agent.model !== undefined ? { model: this.agent.model } : {}),
|
|
355
|
+
messages: [
|
|
356
|
+
{
|
|
357
|
+
role: 'system',
|
|
358
|
+
content: 'Summarize durable agent session history for future context. Preserve user goals, decisions, tool/command effects, files touched, errors, policy/approval events, and pending work. Do not include secrets.',
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
role: 'user',
|
|
362
|
+
content: JSON.stringify(entries.map((entry) => ({ type: entry.type, timestamp: entry.timestamp, data: entry.data ?? {} })).slice(0, 200)),
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
}, { retries: 1, retryDelayMs: 250 });
|
|
366
|
+
return response.message?.content?.trim() || deterministic;
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return deterministic;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async createCheckpoint(options) {
|
|
373
|
+
const input = typeof options === 'string' ? { label: options } : options;
|
|
374
|
+
const sandbox = await this.sandbox;
|
|
375
|
+
const snapshot = sandbox.snapshot ? await sandbox.snapshot() : undefined;
|
|
376
|
+
const entry = await this.recordEntry('checkpoint_created', {
|
|
377
|
+
label: input.label,
|
|
378
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
379
|
+
...(snapshot ? { snapshotId: snapshot.id, snapshot: snapshot } : {}),
|
|
380
|
+
});
|
|
381
|
+
await this.emit('checkpoint_created', { label: input.label, ...(snapshot ? { snapshotId: snapshot.id } : {}) });
|
|
382
|
+
return { label: input.label, entryId: entry.id, ...(snapshot ? { snapshotId: snapshot.id } : {}) };
|
|
383
|
+
}
|
|
384
|
+
async restoreCheckpoint(options) {
|
|
385
|
+
const input = typeof options === 'string' ? { label: options } : options;
|
|
386
|
+
const history = await this.history();
|
|
387
|
+
const checkpoint = [...(history.entries ?? [])]
|
|
388
|
+
.reverse()
|
|
389
|
+
.find((entry) => entry.type === 'checkpoint_created' && entry.data?.label === input.label);
|
|
390
|
+
if (!checkpoint)
|
|
391
|
+
throw new FabricError({ code: 'CHECKPOINT_NOT_FOUND', message: `Checkpoint not found: ${input.label}`, details: { label: input.label } });
|
|
392
|
+
const sandbox = await this.sandbox;
|
|
393
|
+
const snapshot = checkpoint.data?.snapshot;
|
|
394
|
+
if (sandbox.restore && isSnapshot(snapshot))
|
|
395
|
+
await sandbox.restore(snapshot);
|
|
396
|
+
const entry = await this.recordEntry('checkpoint_restored', {
|
|
397
|
+
label: input.label,
|
|
398
|
+
checkpointEntryId: checkpoint.id,
|
|
399
|
+
...(isSnapshot(snapshot) ? { snapshotId: snapshot.id } : {}),
|
|
400
|
+
});
|
|
401
|
+
await this.emit('checkpoint_restored', { label: input.label, checkpointEntryId: checkpoint.id });
|
|
402
|
+
return {
|
|
403
|
+
label: input.label,
|
|
404
|
+
entryId: entry.id,
|
|
405
|
+
...(isSnapshot(snapshot) ? { snapshotId: snapshot.id } : {}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
async createSandbox() {
|
|
409
|
+
const sandbox = this.options.sandbox;
|
|
410
|
+
if (typeof sandbox === 'object' && sandbox !== null && 'exec' in sandbox)
|
|
411
|
+
return sandbox;
|
|
412
|
+
if (typeof sandbox === 'function')
|
|
413
|
+
return sandbox({ sessionId: this.id });
|
|
414
|
+
return createSandboxEnv({ backend: sandbox ?? 'empty', sessionId: this.id });
|
|
415
|
+
}
|
|
416
|
+
async executeToolCalls(calls, tools, sandbox, callPolicy, approvalTimeoutMs, approvalHandler) {
|
|
417
|
+
const results = [];
|
|
418
|
+
const policy = callPolicy ?? this.options.policy;
|
|
419
|
+
for (const call of calls) {
|
|
420
|
+
const tool = tools.get(call.tool);
|
|
421
|
+
const decision = evaluateToolCallPolicy(call, policy);
|
|
422
|
+
if (!decision.allowed) {
|
|
423
|
+
if (decision.approvalRequired) {
|
|
424
|
+
const approved = await this.requestApproval({
|
|
425
|
+
kind: 'tool',
|
|
426
|
+
tool: call.tool,
|
|
427
|
+
input: call.input,
|
|
428
|
+
reason: decision.reason ?? `Approval required for tool: ${call.tool}`,
|
|
429
|
+
...(decision.risk ? { risk: decision.risk } : {}),
|
|
430
|
+
...(decision.matchedPattern ? { matchedPattern: decision.matchedPattern } : {}),
|
|
431
|
+
affectedPaths: affectedPathsFromInput(call.input),
|
|
432
|
+
}, policy, approvalTimeoutMs, approvalHandler);
|
|
433
|
+
if (approved) {
|
|
434
|
+
// Approved by caller/operator; continue to normal execution path.
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
const error = decision.reason ?? `Approval required for tool call: ${call.tool}`;
|
|
438
|
+
results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
|
|
439
|
+
await this.recordEntry('error', { tool: call.tool, error, approvalRequired: true });
|
|
440
|
+
await this.emit('error', { tool: call.tool, error, approvalRequired: true });
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
const error = decision.reason ?? `Tool call denied by policy: ${call.tool}`;
|
|
446
|
+
results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
|
|
447
|
+
await this.recordEntry('error', { tool: call.tool, error, approvalRequired: false });
|
|
448
|
+
await this.emit('error', { tool: call.tool, error, approvalRequired: false });
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const commandDecision = this.evaluateScopedCommandToolCall(tool, call.input, policy);
|
|
453
|
+
if (!commandDecision.allowed) {
|
|
454
|
+
if (commandDecision.approvalRequired) {
|
|
455
|
+
const command = this.commandTextForApproval(tool, call.input);
|
|
456
|
+
const approved = await this.requestApproval({
|
|
457
|
+
kind: 'command',
|
|
458
|
+
tool: call.tool,
|
|
459
|
+
...(command ? { command } : {}),
|
|
460
|
+
input: call.input,
|
|
461
|
+
reason: commandDecision.reason ?? `Approval required for command: ${call.tool}`,
|
|
462
|
+
...(commandDecision.risk ? { risk: commandDecision.risk } : {}),
|
|
463
|
+
...(commandDecision.matchedPattern ? { matchedPattern: commandDecision.matchedPattern } : {}),
|
|
464
|
+
envKeys: commandEnvKeys(tool),
|
|
465
|
+
affectedPaths: affectedPathsFromInput(call.input),
|
|
466
|
+
}, policy, approvalTimeoutMs, approvalHandler);
|
|
467
|
+
if (!approved) {
|
|
468
|
+
const error = commandDecision.reason ?? `Approval required for command: ${call.tool}`;
|
|
469
|
+
results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
|
|
470
|
+
await this.recordEntry('error', { tool: call.tool, error, approvalRequired: true });
|
|
471
|
+
await this.emit('error', { tool: call.tool, error, approvalRequired: true });
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
const error = commandDecision.reason ?? `Command denied by policy: ${call.tool}`;
|
|
477
|
+
results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
|
|
478
|
+
await this.recordEntry('error', { tool: call.tool, error, approvalRequired: false });
|
|
479
|
+
await this.emit('error', { tool: call.tool, error, approvalRequired: false });
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
await this.recordEntry('tool_call', { tool: call.tool, input: call.input, ...(tool?.metadata?.effect ? { effect: tool.metadata.effect } : {}), ...(call.id ? { toolCallId: call.id } : {}) });
|
|
484
|
+
await this.emit('tool_start', { tool: call.tool });
|
|
485
|
+
if (!tool?.execute) {
|
|
486
|
+
const error = `Tool is not available: ${call.tool}`;
|
|
487
|
+
results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
|
|
488
|
+
await this.recordEntry('error', { tool: call.tool, error, approvalRequired: false });
|
|
489
|
+
await this.emit('tool_end', { tool: call.tool, error });
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
const input = call.tool === 'bash' ? this.clampBashToolInput(call.input, policy) : call.input;
|
|
494
|
+
const startedAt = Date.now();
|
|
495
|
+
const output = await tool.execute(input, { sandbox });
|
|
496
|
+
const durationMs = Date.now() - startedAt;
|
|
497
|
+
results.push({ tool: call.tool, input, output, ...(call.id ? { id: call.id } : {}) });
|
|
498
|
+
await this.recordEntry('tool_result', { tool: call.tool, output: output, durationMs, ...(call.id ? { toolCallId: call.id } : {}) });
|
|
499
|
+
await this.emit('tool_end', { tool: call.tool, durationMs });
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
503
|
+
results.push({ tool: call.tool, input: call.input, error: message, ...(call.id ? { id: call.id } : {}) });
|
|
504
|
+
await this.recordEntry('error', { tool: call.tool, error: message });
|
|
505
|
+
await this.emit('tool_end', { tool: call.tool, error: message });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return results;
|
|
509
|
+
}
|
|
510
|
+
async requestApproval(input, callPolicy, approvalTimeoutMs, approvalHandler) {
|
|
511
|
+
const id = `${this.id}-approval-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
512
|
+
const request = {
|
|
513
|
+
id,
|
|
514
|
+
sessionId: this.id,
|
|
515
|
+
reason: input.reason,
|
|
516
|
+
subject: input.command ?? input.tool ?? input.kind,
|
|
517
|
+
kind: input.kind,
|
|
518
|
+
createdAt: new Date().toISOString(),
|
|
519
|
+
...(input.risk ? { risk: input.risk } : {}),
|
|
520
|
+
...(callPolicy?.approvals?.requiredApprovals ? { requiredApprovals: callPolicy.approvals.requiredApprovals } : {}),
|
|
521
|
+
...(callPolicy?.approvals?.escalation ? { escalation: callPolicy.approvals.escalation } : {}),
|
|
522
|
+
...(input.matchedPattern ? { matchedPattern: input.matchedPattern } : {}),
|
|
523
|
+
...(input.envKeys && input.envKeys.length > 0 ? { envKeys: input.envKeys } : {}),
|
|
524
|
+
...(input.affectedPaths && input.affectedPaths.length > 0 ? { affectedPaths: input.affectedPaths } : {}),
|
|
525
|
+
...(input.tool ? { tool: input.tool } : {}),
|
|
526
|
+
...(input.command ? { command: input.command } : {}),
|
|
527
|
+
...(isJsonValue(input.input) ? { input: input.input } : {}),
|
|
528
|
+
...(this.options.actor ? { actor: this.options.actor } : {}),
|
|
529
|
+
};
|
|
530
|
+
await this.recordEntry('approval_requested', request);
|
|
531
|
+
await this.emit('approval_requested', request);
|
|
532
|
+
const handler = approvalHandler ?? this.options.onApproval;
|
|
533
|
+
const autonomy = this.effectiveAutonomy();
|
|
534
|
+
const waitTimeout = approvalTimeoutMs ?? callPolicy?.approvals?.defaultTimeoutMs;
|
|
535
|
+
const hasStoredApprovalBackend = typeof this.store.waitForApproval === 'function';
|
|
536
|
+
const response = handler
|
|
537
|
+
? normalizeApprovalResponse(await handler(request))
|
|
538
|
+
: hasStoredApprovalBackend
|
|
539
|
+
? await this.waitForStoredApproval(request, waitTimeout)
|
|
540
|
+
: undefined;
|
|
541
|
+
if (!response) {
|
|
542
|
+
const unavailableReason = !handler && !hasStoredApprovalBackend
|
|
543
|
+
? `Approval unavailable in ${autonomy.mode} mode: no approval handler or store waiter is configured (${autonomy.onApprovalUnavailable}).`
|
|
544
|
+
: waitTimeout !== undefined
|
|
545
|
+
? `Approval timed out after ${waitTimeout}ms`
|
|
546
|
+
: 'No approval response was received';
|
|
547
|
+
if (!handler && !hasStoredApprovalBackend && autonomy.onApprovalUnavailable === 'deny') {
|
|
548
|
+
await this.recordEntry('approval_denied', { approvalId: id, reason: unavailableReason });
|
|
549
|
+
await this.emit('approval_denied', { approvalId: id, reason: unavailableReason });
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
await this.recordEntry('approval_expired', { approvalId: id, reason: unavailableReason });
|
|
553
|
+
await this.emit('approval_expired', { approvalId: id, reason: unavailableReason });
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
if (response.decision === 'approved') {
|
|
558
|
+
await this.recordEntry('approval_granted', { approvalId: id, reason: response.reason ?? 'Approved by handler' });
|
|
559
|
+
await this.emit('approval_granted', { approvalId: id, reason: response.reason ?? 'Approved by handler' });
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
await this.recordEntry('approval_denied', { approvalId: id, reason: response.reason ?? 'Denied by handler' });
|
|
563
|
+
await this.emit('approval_denied', { approvalId: id, reason: response.reason ?? 'Denied by handler' });
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
async waitForStoredApproval(request, timeoutMs) {
|
|
567
|
+
if (!this.store.waitForApproval)
|
|
568
|
+
return undefined;
|
|
569
|
+
return this.store.waitForApproval(this.id, request.id, timeoutMs);
|
|
570
|
+
}
|
|
571
|
+
effectiveAutonomy() {
|
|
572
|
+
return {
|
|
573
|
+
mode: this.options.autonomy?.mode ?? 'background',
|
|
574
|
+
onMissingInput: this.options.autonomy?.onMissingInput ?? 'assume',
|
|
575
|
+
onApprovalUnavailable: this.options.autonomy?.onApprovalUnavailable ?? 'fail',
|
|
576
|
+
onCredentialMissing: this.options.autonomy?.onCredentialMissing ?? 'fail',
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
evaluateScopedCommandToolCall(tool, input, policy) {
|
|
580
|
+
if (tool?.metadata?.kind !== 'command' || typeof tool.metadata.command !== 'string')
|
|
581
|
+
return { allowed: true };
|
|
582
|
+
const args = typeof input === 'object' && input !== null && Array.isArray(input.args)
|
|
583
|
+
? input.args.filter((value) => typeof value === 'string')
|
|
584
|
+
: [];
|
|
585
|
+
const command = [tool.metadata.command, ...args.map(shellQuote)].join(' ');
|
|
586
|
+
return evaluateCommandPolicy(command, policy);
|
|
587
|
+
}
|
|
588
|
+
commandTextForApproval(tool, input) {
|
|
589
|
+
if (tool?.metadata?.kind !== 'command' || typeof tool.metadata.command !== 'string')
|
|
590
|
+
return undefined;
|
|
591
|
+
const args = typeof input === 'object' && input !== null && Array.isArray(input.args)
|
|
592
|
+
? input.args.filter((value) => typeof value === 'string')
|
|
593
|
+
: [];
|
|
594
|
+
return [tool.metadata.command, ...args].join(' ');
|
|
595
|
+
}
|
|
596
|
+
clampBashToolInput(input, policy) {
|
|
597
|
+
if (!policy?.maxCommandTimeoutMs || typeof input !== 'object' || input === null)
|
|
598
|
+
return input;
|
|
599
|
+
const record = input;
|
|
600
|
+
const timeout = typeof record.timeout === 'number' ? record.timeout : undefined;
|
|
601
|
+
return { ...record, timeout: clampCommandTimeout(timeout, policy) };
|
|
602
|
+
}
|
|
603
|
+
resolveTools(sandbox, promptTools = [], promptCommands = []) {
|
|
604
|
+
const tools = new Map();
|
|
605
|
+
const commandToolOptions = this.options.resolveSecret ? { resolveSecret: this.options.resolveSecret } : {};
|
|
606
|
+
for (const tool of createBuiltinTools(sandbox))
|
|
607
|
+
tools.set(tool.name, tool);
|
|
608
|
+
for (const tool of createCommandTools(this.options.commands ?? [], commandToolOptions))
|
|
609
|
+
tools.set(tool.name, tool);
|
|
610
|
+
for (const tool of this.options.tools ?? [])
|
|
611
|
+
tools.set(tool.name, tool);
|
|
612
|
+
for (const tool of createCommandTools(promptCommands ?? [], commandToolOptions))
|
|
613
|
+
tools.set(tool.name, tool);
|
|
614
|
+
for (const tool of promptTools)
|
|
615
|
+
tools.set(tool.name, tool);
|
|
616
|
+
return tools;
|
|
617
|
+
}
|
|
618
|
+
resolveSkill(name) {
|
|
619
|
+
return this.options.skills?.find((skill) => skill.name === name);
|
|
620
|
+
}
|
|
621
|
+
resolveRole(role) {
|
|
622
|
+
if (!role)
|
|
623
|
+
return undefined;
|
|
624
|
+
if (typeof role !== 'string')
|
|
625
|
+
return role;
|
|
626
|
+
return this.options.roles?.find((candidate) => candidate.name === role);
|
|
627
|
+
}
|
|
628
|
+
async ensureData() {
|
|
629
|
+
const existing = await this.store.load(this.id);
|
|
630
|
+
if (existing)
|
|
631
|
+
return existing;
|
|
632
|
+
const now = new Date().toISOString();
|
|
633
|
+
const data = { id: this.id, agentId: this.agent.id, createdAt: now, updatedAt: now, events: [], entries: [] };
|
|
634
|
+
if (this.options.metadata !== undefined)
|
|
635
|
+
data.metadata = this.options.metadata;
|
|
636
|
+
await this.store.save(data);
|
|
637
|
+
return data;
|
|
638
|
+
}
|
|
639
|
+
async recordEntry(type, data) {
|
|
640
|
+
await this.ensureData();
|
|
641
|
+
const current = await this.store.load(this.id);
|
|
642
|
+
const entry = {
|
|
643
|
+
id: `${this.id}-entry-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
644
|
+
type,
|
|
645
|
+
timestamp: new Date().toISOString(),
|
|
646
|
+
...(current?.leafId ? { parentId: current.leafId } : {}),
|
|
647
|
+
...(this.options.actor ? { actor: this.options.actor } : {}),
|
|
648
|
+
...(data ? { data } : {}),
|
|
649
|
+
};
|
|
650
|
+
await this.store.appendEntry?.(this.id, entry);
|
|
651
|
+
return entry;
|
|
652
|
+
}
|
|
653
|
+
async emit(type, data) {
|
|
654
|
+
await this.ensureData();
|
|
655
|
+
const event = {
|
|
656
|
+
id: `${this.id}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
657
|
+
type,
|
|
658
|
+
timestamp: new Date().toISOString(),
|
|
659
|
+
sessionId: this.id,
|
|
660
|
+
...(data ? { data } : {}),
|
|
661
|
+
};
|
|
662
|
+
await this.store.appendEvent?.(this.id, event);
|
|
663
|
+
await this.options.onEvent?.(event);
|
|
664
|
+
for (const subscriber of this.eventSubscribers)
|
|
665
|
+
await subscriber(event);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function affectedPathsFromInput(input) {
|
|
669
|
+
if (typeof input !== 'object' || input === null)
|
|
670
|
+
return [];
|
|
671
|
+
const record = input;
|
|
672
|
+
const paths = [record.path, record.cwd].filter((value) => typeof value === 'string');
|
|
673
|
+
return [...new Set(paths)];
|
|
674
|
+
}
|
|
675
|
+
function commandEnvKeys(tool) {
|
|
676
|
+
const value = tool?.metadata?.envKeys;
|
|
677
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
|
|
678
|
+
}
|
|
679
|
+
function normalizeApprovalResponse(value) {
|
|
680
|
+
if (typeof value === 'boolean')
|
|
681
|
+
return { decision: value ? 'approved' : 'denied' };
|
|
682
|
+
if (typeof value === 'string')
|
|
683
|
+
return { decision: value };
|
|
684
|
+
return value;
|
|
685
|
+
}
|
|
686
|
+
function isJsonValue(value) {
|
|
687
|
+
if (value === null)
|
|
688
|
+
return true;
|
|
689
|
+
if (['string', 'number', 'boolean'].includes(typeof value))
|
|
690
|
+
return true;
|
|
691
|
+
if (Array.isArray(value))
|
|
692
|
+
return value.every(isJsonValue);
|
|
693
|
+
if (typeof value !== 'object')
|
|
694
|
+
return false;
|
|
695
|
+
return Object.values(value).every(isJsonValue);
|
|
696
|
+
}
|
|
697
|
+
function summarizeEntries(entries) {
|
|
698
|
+
if (entries.length === 0)
|
|
699
|
+
return 'No entries to compact.';
|
|
700
|
+
const counts = new Map();
|
|
701
|
+
for (const entry of entries)
|
|
702
|
+
counts.set(entry.type, (counts.get(entry.type) ?? 0) + 1);
|
|
703
|
+
const summary = [...counts.entries()].map(([type, count]) => `${type}: ${count}`).join(', ');
|
|
704
|
+
const first = entries[0];
|
|
705
|
+
const last = entries.at(-1);
|
|
706
|
+
return `Compacted ${entries.length} entries (${summary}). Range: ${first?.timestamp ?? 'unknown'} to ${last?.timestamp ?? 'unknown'}.`;
|
|
707
|
+
}
|
|
708
|
+
function collectFileReferences(entries) {
|
|
709
|
+
const readFiles = new Set();
|
|
710
|
+
const modifiedFiles = new Set();
|
|
711
|
+
for (const entry of entries) {
|
|
712
|
+
if (entry.type === 'tool_call') {
|
|
713
|
+
const tool = typeof entry.data?.tool === 'string' ? entry.data.tool : undefined;
|
|
714
|
+
const effect = typeof entry.data?.effect === 'string' ? entry.data.effect : undefined;
|
|
715
|
+
const input = entry.data?.input;
|
|
716
|
+
for (const path of filePathsFromToolInput(input)) {
|
|
717
|
+
if (effect === 'read' || (!effect && isReadOnlyFileTool(tool)))
|
|
718
|
+
readFiles.add(path);
|
|
719
|
+
if (effect === 'write' || (!effect && isMutatingFileTool(tool)))
|
|
720
|
+
modifiedFiles.add(path);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (entry.type === 'shell_command' && typeof entry.data?.cwd === 'string')
|
|
724
|
+
readFiles.add(entry.data.cwd);
|
|
725
|
+
}
|
|
726
|
+
return { readFiles: [...readFiles].sort(), modifiedFiles: [...modifiedFiles].sort() };
|
|
727
|
+
}
|
|
728
|
+
function appendFileReferenceTags(summary, refs) {
|
|
729
|
+
const sections = [summary];
|
|
730
|
+
if (refs.readFiles.length > 0)
|
|
731
|
+
sections.push(`<read-files>\n${refs.readFiles.join('\n')}\n</read-files>`);
|
|
732
|
+
if (refs.modifiedFiles.length > 0)
|
|
733
|
+
sections.push(`<modified-files>\n${refs.modifiedFiles.join('\n')}\n</modified-files>`);
|
|
734
|
+
return sections.join('\n\n');
|
|
735
|
+
}
|
|
736
|
+
function isReadOnlyFileTool(tool) {
|
|
737
|
+
return tool !== undefined && ['read', 'read_buffer', 'stat', 'readdir', 'exists', 'grep', 'glob'].includes(tool);
|
|
738
|
+
}
|
|
739
|
+
function isMutatingFileTool(tool) {
|
|
740
|
+
return tool !== undefined && ['write', 'edit', 'mkdir', 'rm'].includes(tool);
|
|
741
|
+
}
|
|
742
|
+
function filePathsFromToolInput(input) {
|
|
743
|
+
if (typeof input !== 'object' || input === null)
|
|
744
|
+
return [];
|
|
745
|
+
const record = input;
|
|
746
|
+
const values = [record.path, record.cwd, record.pattern];
|
|
747
|
+
if (Array.isArray(record.paths))
|
|
748
|
+
values.push(...record.paths);
|
|
749
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
|
|
750
|
+
}
|
|
751
|
+
function isSnapshot(value) {
|
|
752
|
+
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
|
|
753
|
+
}
|
|
754
|
+
function abortError() {
|
|
755
|
+
const error = new Error('Task cancelled');
|
|
756
|
+
error.name = 'AbortError';
|
|
757
|
+
return error;
|
|
758
|
+
}
|
|
759
|
+
function isAbortError(error) {
|
|
760
|
+
return error instanceof Error && (error.name === 'AbortError' || /aborted|cancelled|canceled/i.test(error.message));
|
|
761
|
+
}
|
|
762
|
+
//# sourceMappingURL=session.js.map
|