@fabric-harness/sdk 2.4.1 → 2.6.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 +6 -0
- package/dist/approval.d.ts +26 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +138 -0
- package/dist/approval.js.map +1 -0
- package/dist/events.d.ts +43 -42
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +141 -140
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -71
- package/dist/index.js.map +1 -1
- package/dist/model-pricing.d.ts.map +1 -1
- package/dist/model-pricing.js +5 -0
- package/dist/model-pricing.js.map +1 -1
- package/dist/model.js +2 -2
- package/dist/model.js.map +1 -1
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +32 -1
- package/dist/providers.js.map +1 -1
- package/dist/session.d.ts +11 -10
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +664 -303
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +2 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +143 -54
- package/dist/store.js.map +1 -1
- package/dist/strict.d.ts +135 -134
- package/dist/strict.d.ts.map +1 -1
- package/dist/strict.js +70 -69
- package/dist/strict.js.map +1 -1
- package/dist/tools.d.ts +13 -9
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +248 -92
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +60 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/session.js
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
import { createHash } from
|
|
2
|
-
import { CostBudgetTracker } from
|
|
3
|
-
import { inMemorySessionMemory } from
|
|
4
|
-
import { FabricError } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CostBudgetTracker } from "./cost-budget.js";
|
|
3
|
+
import { inMemorySessionMemory } from "./session-memory.js";
|
|
4
|
+
import { FabricError } from "./error.js";
|
|
5
|
+
import { approvalGrantFromState, approvalInputDigest, createApprovalGrant } from "./approval.js";
|
|
6
|
+
import { SessionHistory, buildModelMessagesFromHistory, findTrailingDanglingToolCalls, findTrailingUnfinishedTasks, } from "./history.js";
|
|
7
|
+
import { getLogger } from "./logger.js";
|
|
8
|
+
import { defaultLoopRuntime } from "./loop.js";
|
|
9
|
+
import { defaultModelProvider, generateWithRuntime } from "./model.js";
|
|
10
|
+
import { clampCommandTimeout, evaluateCommandPolicy, evaluateToolCallPolicy } from "./policy.js";
|
|
11
|
+
import { buildResultRetryPrompt, validateResult } from "./result.js";
|
|
12
|
+
import { estimateSessionEntryTokens, evaluateContextBudget, isContextOverflowError, } from "./context-budget.js";
|
|
13
|
+
import { policiedSandboxEnv, runWithSandboxPolicyApproval } from "./policied-sandbox.js";
|
|
14
|
+
import { currentSubmissionContext } from "./submission-context.js";
|
|
15
|
+
import { createSandboxEnv, createScopedSandboxEnv } from "./sandbox.js";
|
|
16
|
+
import { createFabricFs } from "./filesystem.js";
|
|
17
|
+
import { registerSandbox, serializeSandboxRef, unregisterSandbox } from "./sandbox-ref.js";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { defaultSessionStore } from "./store.js";
|
|
20
|
+
import { createActivateSkillTool, createBuiltinTools, createCommandTools, shellQuote, } from "./tools.js";
|
|
21
|
+
import { formatPackagedSkillResourceList, readPackagedSkillFile } from "./skills.js";
|
|
22
|
+
import { MAX_TASK_DEPTH } from "./types.js";
|
|
22
23
|
let nextTaskId = 0;
|
|
23
|
-
const CROCKFORD_ALPHABET =
|
|
24
|
+
const CROCKFORD_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
24
25
|
const AFFINITY_KEY_REGEX = /^aff_[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
|
|
25
26
|
function encodeBase32(buf, length) {
|
|
26
|
-
let result =
|
|
27
|
+
let result = "";
|
|
27
28
|
let bitIndex = 0; // 0 = MSB of buf[0]
|
|
28
29
|
for (let i = 0; i < length; i++) {
|
|
29
30
|
let value = 0;
|
|
@@ -46,7 +47,7 @@ function encodeBase32(buf, length) {
|
|
|
46
47
|
* with overwhelming probability.
|
|
47
48
|
*/
|
|
48
49
|
export function generateAffinityKey(agentId, sessionId) {
|
|
49
|
-
const hash = createHash(
|
|
50
|
+
const hash = createHash("sha256").update(agentId).update("\0").update(sessionId).digest();
|
|
50
51
|
// Build a 128-bit buffer from the hash, clearing the top 5 bits of the
|
|
51
52
|
// first byte so the first Crockford base32 char is in [0-7].
|
|
52
53
|
const combined = Buffer.alloc(16);
|
|
@@ -57,7 +58,7 @@ export function generateAffinityKey(agentId, sessionId) {
|
|
|
57
58
|
export function isValidAffinityKey(key) {
|
|
58
59
|
return AFFINITY_KEY_REGEX.test(key);
|
|
59
60
|
}
|
|
60
|
-
const COMPACTION_SUMMARIZATION_SYSTEM_PROMPT =
|
|
61
|
+
const COMPACTION_SUMMARIZATION_SYSTEM_PROMPT = "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.";
|
|
61
62
|
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
|
|
62
63
|
|
|
63
64
|
Summarize the prefix to provide context for the retained suffix:
|
|
@@ -126,20 +127,22 @@ export class StubFabricSession {
|
|
|
126
127
|
}
|
|
127
128
|
async prompt(text, promptOptions = {}) {
|
|
128
129
|
const ambientSignal = promptOptions.signal === undefined ? currentSubmissionContext()?.signal : undefined;
|
|
129
|
-
const options = ambientSignal
|
|
130
|
+
const options = ambientSignal
|
|
131
|
+
? { ...promptOptions, signal: ambientSignal }
|
|
132
|
+
: promptOptions;
|
|
130
133
|
const durable = await this.getDurableRuntime();
|
|
131
134
|
if (durable) {
|
|
132
135
|
// Delegate to the durable runtime; the runtime is responsible for
|
|
133
136
|
// recording entries / events on its own side. We still emit a single
|
|
134
137
|
// local prompt_start/end pair so local subscribers see activity.
|
|
135
|
-
await this.emit(
|
|
138
|
+
await this.emit("prompt_start", { text });
|
|
136
139
|
try {
|
|
137
140
|
const result = await durable.prompt({ text, ...(options ? { options } : {}) });
|
|
138
|
-
await this.emit(
|
|
141
|
+
await this.emit("prompt_end", { text });
|
|
139
142
|
return result;
|
|
140
143
|
}
|
|
141
144
|
catch (error) {
|
|
142
|
-
await this.emit(
|
|
145
|
+
await this.emit("error", { error: error instanceof Error ? error.message : String(error) });
|
|
143
146
|
throw error;
|
|
144
147
|
}
|
|
145
148
|
}
|
|
@@ -149,7 +152,7 @@ export class StubFabricSession {
|
|
|
149
152
|
this.eventSubscribers.add(promptSubscriber);
|
|
150
153
|
try {
|
|
151
154
|
await this.repairDanglingToolCalls();
|
|
152
|
-
await this.emit(
|
|
155
|
+
await this.emit("prompt_start", { text, ...(role ? { role: role.name } : {}) });
|
|
153
156
|
const sandbox = createScopedSandboxEnv(await this.sandbox, options.cwd);
|
|
154
157
|
const tools = this.resolveTools(sandbox, options.tools, options.commands);
|
|
155
158
|
// Submission-correlated prompts are idempotent by submission id: a
|
|
@@ -159,10 +162,11 @@ export class StubFabricSession {
|
|
|
159
162
|
if (options.submission) {
|
|
160
163
|
const prior = await this.history();
|
|
161
164
|
const submissionId = options.submission.submissionId;
|
|
162
|
-
inputAlreadyApplied = (prior.entries ?? []).some((entry) => entry.type ===
|
|
165
|
+
inputAlreadyApplied = (prior.entries ?? []).some((entry) => entry.type === "user_prompt" &&
|
|
166
|
+
entry.data?.submissionId === submissionId);
|
|
163
167
|
}
|
|
164
168
|
if (!inputAlreadyApplied) {
|
|
165
|
-
await this.recordEntry(
|
|
169
|
+
await this.recordEntry("user_prompt", {
|
|
166
170
|
text,
|
|
167
171
|
...(role ? { role: role.name } : {}),
|
|
168
172
|
...(options.submission ? { submissionId: options.submission.submissionId } : {}),
|
|
@@ -175,13 +179,13 @@ export class StubFabricSession {
|
|
|
175
179
|
await options.submission?.onInputApplied?.();
|
|
176
180
|
const sessionData = await this.history();
|
|
177
181
|
let messages = buildModelMessagesFromHistory(sessionData, role);
|
|
178
|
-
if (!messages.some((message) => message.role ===
|
|
179
|
-
messages.push({ role:
|
|
182
|
+
if (!messages.some((message) => message.role === "user" && message.content === text))
|
|
183
|
+
messages.push({ role: "user", content: text });
|
|
180
184
|
messages = await this.autoCompactIfNeeded(messages, role, options);
|
|
181
185
|
if (options.toolCalls?.length) {
|
|
182
186
|
const results = await this.executeToolCalls(options.toolCalls, tools, sandbox, options.policy, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval, undefined, options.signal);
|
|
183
|
-
await this.recordEntry(
|
|
184
|
-
await this.emit(
|
|
187
|
+
await this.recordEntry("result", { result: results });
|
|
188
|
+
await this.emit("prompt_end", { text, result: results });
|
|
185
189
|
return validateResult(results, options.result, options.resultExtraction);
|
|
186
190
|
}
|
|
187
191
|
// Use tool-based result extraction when a validator is provided and the caller
|
|
@@ -190,25 +194,44 @@ export class StubFabricSession {
|
|
|
190
194
|
const runtime = options.loopRuntime ?? this.options.loopRuntime ?? defaultLoopRuntime;
|
|
191
195
|
const runRuntime = () => runtime.runPrompt({
|
|
192
196
|
sessionId: this.id,
|
|
193
|
-
...(sessionData.affinityKey !== undefined
|
|
197
|
+
...(sessionData.affinityKey !== undefined
|
|
198
|
+
? { affinityKey: sessionData.affinityKey }
|
|
199
|
+
: {}),
|
|
194
200
|
text,
|
|
195
201
|
messages,
|
|
196
202
|
tools,
|
|
197
203
|
sandbox,
|
|
198
|
-
...(typeof (options.model ?? this.options.model ?? this.agent.model) ===
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
...(typeof (options.model ?? this.options.model ?? this.agent.model) === "string"
|
|
205
|
+
? { model: (options.model ?? this.options.model ?? this.agent.model) }
|
|
206
|
+
: {}),
|
|
207
|
+
...((options.thinkingLevel ?? this.options.thinkingLevel ?? this.agent.thinkingLevel) !==
|
|
208
|
+
undefined
|
|
209
|
+
? {
|
|
210
|
+
thinkingLevel: options.thinkingLevel ?? this.options.thinkingLevel ?? this.agent.thinkingLevel,
|
|
211
|
+
}
|
|
212
|
+
: {}),
|
|
213
|
+
...((options.modelProvider ?? this.options.modelProvider) !== undefined
|
|
214
|
+
? { modelProvider: options.modelProvider ?? this.options.modelProvider }
|
|
215
|
+
: {}),
|
|
201
216
|
...(options.maxIterations !== undefined ? { maxIterations: options.maxIterations } : {}),
|
|
202
|
-
...(options.modelTimeoutMs !== undefined
|
|
217
|
+
...(options.modelTimeoutMs !== undefined
|
|
218
|
+
? { modelTimeoutMs: options.modelTimeoutMs }
|
|
219
|
+
: {}),
|
|
203
220
|
...(options.modelRetries !== undefined ? { modelRetries: options.modelRetries } : {}),
|
|
204
|
-
...(options.modelRetryDelayMs !== undefined
|
|
221
|
+
...(options.modelRetryDelayMs !== undefined
|
|
222
|
+
? { modelRetryDelayMs: options.modelRetryDelayMs }
|
|
223
|
+
: {}),
|
|
205
224
|
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
206
225
|
...(options.policy !== undefined ? { policy: options.policy } : {}),
|
|
207
|
-
...((options.approvalTimeoutMs ?? this.options.approvalTimeoutMs) !== undefined
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
226
|
+
...((options.approvalTimeoutMs ?? this.options.approvalTimeoutMs) !== undefined
|
|
227
|
+
? { approvalTimeoutMs: options.approvalTimeoutMs ?? this.options.approvalTimeoutMs }
|
|
228
|
+
: {}),
|
|
229
|
+
...(this.costBudget.enabled()
|
|
230
|
+
? {
|
|
231
|
+
costBudget: this.costBudget,
|
|
232
|
+
requestCostLimitApproval: (ctx) => this.requestCostLimitApproval(ctx, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval),
|
|
233
|
+
}
|
|
234
|
+
: {}),
|
|
212
235
|
...(useResultTools ? { resultValidator: options.result } : {}),
|
|
213
236
|
agentId: this.agent.id,
|
|
214
237
|
...(this.options.tenantId !== undefined ? { tenantId: this.options.tenantId } : {}),
|
|
@@ -225,11 +248,13 @@ export class StubFabricSession {
|
|
|
225
248
|
const recoverFromOverflow = options.recoverContextOverflow ?? compactionSettings?.recoverFromOverflow ?? true;
|
|
226
249
|
if (!isContextOverflowError(error) || !recoverFromOverflow)
|
|
227
250
|
throw error;
|
|
228
|
-
await this.emit(
|
|
251
|
+
await this.emit("error", {
|
|
252
|
+
error: "Context overflow detected; compacting and retrying once.",
|
|
253
|
+
});
|
|
229
254
|
await this.compact({
|
|
230
255
|
keepRecentEntries: options.compactionKeepRecentEntries ?? compactionSettings?.keepRecentEntries ?? 20,
|
|
231
256
|
generateSummary: true,
|
|
232
|
-
reason:
|
|
257
|
+
reason: "overflow",
|
|
233
258
|
});
|
|
234
259
|
messages = buildModelMessagesFromHistory(await this.history(), role);
|
|
235
260
|
result = await runRuntime();
|
|
@@ -247,13 +272,28 @@ export class StubFabricSession {
|
|
|
247
272
|
if (attempt >= resultRetries)
|
|
248
273
|
throw error;
|
|
249
274
|
const message = error instanceof Error ? error.message : String(error);
|
|
250
|
-
await this.recordEntry(
|
|
251
|
-
|
|
252
|
-
|
|
275
|
+
await this.recordEntry("result_retry", {
|
|
276
|
+
error: message,
|
|
277
|
+
retryAttempt: attempt + 1,
|
|
278
|
+
maxRetries: resultRetries,
|
|
279
|
+
});
|
|
280
|
+
await this.emit("result_retry", {
|
|
281
|
+
error: message,
|
|
282
|
+
retryAttempt: attempt + 1,
|
|
283
|
+
maxRetries: resultRetries,
|
|
284
|
+
});
|
|
285
|
+
messages.push({
|
|
286
|
+
role: "user",
|
|
287
|
+
content: buildResultRetryPrompt(error, options.resultExtraction),
|
|
288
|
+
});
|
|
253
289
|
result = await runRuntime();
|
|
254
290
|
}
|
|
255
291
|
}
|
|
256
|
-
throw new FabricError({
|
|
292
|
+
throw new FabricError({
|
|
293
|
+
code: "RESULT_VALIDATION_FAILED",
|
|
294
|
+
message: "Typed result validation failed.",
|
|
295
|
+
retryable: true,
|
|
296
|
+
});
|
|
257
297
|
}
|
|
258
298
|
finally {
|
|
259
299
|
if (promptSubscriber)
|
|
@@ -304,34 +344,44 @@ export class StubFabricSession {
|
|
|
304
344
|
}
|
|
305
345
|
async skill(name, options = {}) {
|
|
306
346
|
const skill = this.resolveSkill(name);
|
|
307
|
-
await this.recordEntry(
|
|
308
|
-
await this.emit(
|
|
347
|
+
await this.recordEntry("skill_start", { skill: name });
|
|
348
|
+
await this.emit("skill_start", { skill: name });
|
|
309
349
|
if (!skill)
|
|
310
|
-
throw new FabricError({
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
350
|
+
throw new FabricError({
|
|
351
|
+
code: "SKILL_NOT_FOUND",
|
|
352
|
+
message: `Skill not found: ${name}`,
|
|
353
|
+
details: { skill: name },
|
|
354
|
+
});
|
|
355
|
+
const argsText = options.args ? `\n\nArguments:\n${JSON.stringify(options.args, null, 2)}` : "";
|
|
356
|
+
const packagedText = skill.packaged ? formatPackagedSkillResourceList(skill.packaged) : "";
|
|
357
|
+
const result = await this.prompt(`${skill.content ?? ""}${argsText}${packagedText}`, options);
|
|
358
|
+
await this.recordEntry("skill_end", { skill: name });
|
|
359
|
+
await this.emit("skill_end", { skill: name });
|
|
316
360
|
return result;
|
|
317
361
|
}
|
|
318
362
|
async task(text, options = {}) {
|
|
319
363
|
const durable = await this.getDurableRuntime();
|
|
320
364
|
if (durable) {
|
|
321
365
|
const taskId = options.id ?? `${this.id}:task-${++nextTaskId}`;
|
|
322
|
-
await this.emit(
|
|
366
|
+
await this.emit("task_start", { taskId, depth: 1, parentSessionId: this.id });
|
|
323
367
|
try {
|
|
324
368
|
const { id: _id, ...taskOptions } = options;
|
|
325
369
|
const result = await durable.task({
|
|
326
370
|
text,
|
|
327
371
|
taskId,
|
|
328
|
-
...(Object.keys(taskOptions).length
|
|
372
|
+
...(Object.keys(taskOptions).length
|
|
373
|
+
? { options: taskOptions }
|
|
374
|
+
: {}),
|
|
329
375
|
});
|
|
330
|
-
await this.emit(
|
|
376
|
+
await this.emit("task_end", { taskId, depth: 1, parentSessionId: this.id });
|
|
331
377
|
return result;
|
|
332
378
|
}
|
|
333
379
|
catch (error) {
|
|
334
|
-
await this.emit(
|
|
380
|
+
await this.emit("task_failed", {
|
|
381
|
+
taskId,
|
|
382
|
+
status: "failed",
|
|
383
|
+
error: error instanceof Error ? error.message : String(error),
|
|
384
|
+
});
|
|
335
385
|
throw error;
|
|
336
386
|
}
|
|
337
387
|
}
|
|
@@ -341,22 +391,42 @@ export class StubFabricSession {
|
|
|
341
391
|
throw new Error(`Task nesting exceeds MAX_TASK_DEPTH (${MAX_TASK_DEPTH}). Refusing to spawn deeper child task at depth ${depth}.`);
|
|
342
392
|
}
|
|
343
393
|
const startedAt = new Date().toISOString();
|
|
344
|
-
await this.recordEntry(
|
|
345
|
-
|
|
346
|
-
|
|
394
|
+
await this.recordEntry("task_start", {
|
|
395
|
+
taskId,
|
|
396
|
+
text,
|
|
397
|
+
status: "running",
|
|
398
|
+
startedAt,
|
|
399
|
+
depth,
|
|
400
|
+
...(options.agent ? { agent: options.agent } : {}),
|
|
401
|
+
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
402
|
+
});
|
|
403
|
+
await this.emit("task_start", { taskId, depth, parentSessionId: this.id });
|
|
404
|
+
const checkpointLabel = typeof options.checkpoint === "string" ? options.checkpoint : `task:${taskId}`;
|
|
347
405
|
try {
|
|
348
406
|
if (options.signal?.aborted)
|
|
349
407
|
throw abortError();
|
|
350
408
|
if (options.checkpoint) {
|
|
351
|
-
const checkpoint = await this.createCheckpoint({
|
|
352
|
-
|
|
353
|
-
|
|
409
|
+
const checkpoint = await this.createCheckpoint({
|
|
410
|
+
label: `${checkpointLabel}:before`,
|
|
411
|
+
metadata: { taskId, phase: "before" },
|
|
412
|
+
});
|
|
413
|
+
await this.recordEntry("task_checkpoint", {
|
|
414
|
+
taskId,
|
|
415
|
+
phase: "before",
|
|
416
|
+
checkpoint: checkpoint,
|
|
417
|
+
...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}),
|
|
418
|
+
});
|
|
419
|
+
await this.emit("task_checkpoint", {
|
|
420
|
+
taskId,
|
|
421
|
+
phase: "before",
|
|
422
|
+
...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}),
|
|
423
|
+
});
|
|
354
424
|
}
|
|
355
425
|
const taskAbort = new AbortController();
|
|
356
426
|
const stopCancellationWatcher = this.watchTaskCancellation(taskId, taskAbort);
|
|
357
427
|
const forwardAbort = () => taskAbort.abort(options.signal?.reason);
|
|
358
428
|
if (options.signal)
|
|
359
|
-
options.signal.addEventListener(
|
|
429
|
+
options.signal.addEventListener("abort", forwardAbort, { once: true });
|
|
360
430
|
try {
|
|
361
431
|
const child = new StubFabricSession(this.agent, taskId, {
|
|
362
432
|
...this.options,
|
|
@@ -364,39 +434,65 @@ export class StubFabricSession {
|
|
|
364
434
|
store: this.store,
|
|
365
435
|
sandbox: await this.sandbox,
|
|
366
436
|
// Propagate depth so nested tasks can enforce MAX_TASK_DEPTH.
|
|
367
|
-
metadata: {
|
|
437
|
+
metadata: {
|
|
438
|
+
...(this.options.metadata ?? {}),
|
|
439
|
+
...(options.metadata ?? {}),
|
|
440
|
+
taskDepth: depth,
|
|
441
|
+
},
|
|
368
442
|
});
|
|
369
443
|
const result = await child.prompt(text, { ...options, signal: taskAbort.signal });
|
|
370
444
|
if (taskAbort.signal.aborted)
|
|
371
445
|
throw abortError();
|
|
372
446
|
if (options.checkpoint) {
|
|
373
|
-
const checkpoint = await this.createCheckpoint({
|
|
374
|
-
|
|
375
|
-
|
|
447
|
+
const checkpoint = await this.createCheckpoint({
|
|
448
|
+
label: `${checkpointLabel}:after`,
|
|
449
|
+
metadata: { taskId, phase: "after" },
|
|
450
|
+
});
|
|
451
|
+
await this.recordEntry("task_checkpoint", {
|
|
452
|
+
taskId,
|
|
453
|
+
phase: "after",
|
|
454
|
+
checkpoint: checkpoint,
|
|
455
|
+
...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}),
|
|
456
|
+
});
|
|
457
|
+
await this.emit("task_checkpoint", {
|
|
458
|
+
taskId,
|
|
459
|
+
phase: "after",
|
|
460
|
+
...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}),
|
|
461
|
+
});
|
|
376
462
|
}
|
|
377
463
|
const completedAt = new Date().toISOString();
|
|
378
|
-
await this.recordEntry(
|
|
379
|
-
await this.emit(
|
|
464
|
+
await this.recordEntry("task_end", { taskId, status: "completed", completedAt, depth });
|
|
465
|
+
await this.emit("task_end", { taskId, depth, parentSessionId: this.id });
|
|
380
466
|
return result;
|
|
381
467
|
}
|
|
382
468
|
finally {
|
|
383
469
|
stopCancellationWatcher();
|
|
384
470
|
if (options.signal)
|
|
385
|
-
options.signal.removeEventListener(
|
|
471
|
+
options.signal.removeEventListener("abort", forwardAbort);
|
|
386
472
|
}
|
|
387
473
|
}
|
|
388
474
|
catch (error) {
|
|
389
475
|
if (isAbortError(error) || options.signal?.aborted) {
|
|
390
476
|
const cancelledAt = new Date().toISOString();
|
|
391
|
-
const message = error instanceof Error ? error.message :
|
|
392
|
-
await this.recordEntry(
|
|
393
|
-
|
|
477
|
+
const message = error instanceof Error ? error.message : "Task cancelled";
|
|
478
|
+
await this.recordEntry("task_cancelled", {
|
|
479
|
+
taskId,
|
|
480
|
+
status: "cancelled",
|
|
481
|
+
cancelledAt,
|
|
482
|
+
reason: message,
|
|
483
|
+
});
|
|
484
|
+
await this.emit("task_cancelled", {
|
|
485
|
+
taskId,
|
|
486
|
+
status: "cancelled",
|
|
487
|
+
cancelledAt,
|
|
488
|
+
reason: message,
|
|
489
|
+
});
|
|
394
490
|
throw error;
|
|
395
491
|
}
|
|
396
492
|
const failedAt = new Date().toISOString();
|
|
397
493
|
const message = error instanceof Error ? error.message : String(error);
|
|
398
|
-
await this.recordEntry(
|
|
399
|
-
await this.emit(
|
|
494
|
+
await this.recordEntry("task_failed", { taskId, status: "failed", failedAt, error: message });
|
|
495
|
+
await this.emit("task_failed", { taskId, status: "failed", failedAt, error: message });
|
|
400
496
|
throw error;
|
|
401
497
|
}
|
|
402
498
|
}
|
|
@@ -419,11 +515,15 @@ export class StubFabricSession {
|
|
|
419
515
|
return;
|
|
420
516
|
try {
|
|
421
517
|
const entries = (await this.store.load(this.id))?.entries ?? [];
|
|
422
|
-
if (entries.some((entry) => entry.type ===
|
|
518
|
+
if (entries.some((entry) => entry.type === "task_cancelled" && entry.data?.taskId === taskId))
|
|
423
519
|
controller.abort(abortError());
|
|
424
520
|
}
|
|
425
521
|
catch (error) {
|
|
426
|
-
logger.warn(
|
|
522
|
+
logger.warn("Task-cancellation poller failed to load session; will retry next interval.", {
|
|
523
|
+
error: error instanceof Error ? error.message : String(error),
|
|
524
|
+
taskId,
|
|
525
|
+
sessionId: this.id,
|
|
526
|
+
});
|
|
427
527
|
}
|
|
428
528
|
})();
|
|
429
529
|
}, 500);
|
|
@@ -432,9 +532,9 @@ export class StubFabricSession {
|
|
|
432
532
|
async shell(command, options = {}) {
|
|
433
533
|
const durable = await this.getDurableRuntime();
|
|
434
534
|
if (durable) {
|
|
435
|
-
await this.emit(
|
|
535
|
+
await this.emit("command_start", { command });
|
|
436
536
|
const result = await durable.shell({ command, ...(options ? { options } : {}) });
|
|
437
|
-
await this.emit(
|
|
537
|
+
await this.emit("command_end", { command, exitCode: result.exitCode });
|
|
438
538
|
return result;
|
|
439
539
|
}
|
|
440
540
|
const sandbox = await this.sandbox;
|
|
@@ -445,24 +545,33 @@ export class StubFabricSession {
|
|
|
445
545
|
if (!decision.allowed) {
|
|
446
546
|
if (decision.approvalRequired) {
|
|
447
547
|
const approved = await this.requestApproval({
|
|
448
|
-
kind:
|
|
548
|
+
kind: "command",
|
|
449
549
|
command,
|
|
450
|
-
input: {
|
|
550
|
+
input: {
|
|
551
|
+
command,
|
|
552
|
+
...((options.cwd ?? this.options.cwd)
|
|
553
|
+
? { cwd: options.cwd ?? this.options.cwd }
|
|
554
|
+
: {}),
|
|
555
|
+
},
|
|
451
556
|
reason: decision.reason ?? `Approval required for command: ${command}`,
|
|
452
557
|
...(decision.risk ? { risk: decision.risk } : {}),
|
|
453
558
|
...(decision.matchedPattern ? { matchedPattern: decision.matchedPattern } : {}),
|
|
454
559
|
}, policy, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval);
|
|
455
560
|
if (!approved) {
|
|
456
|
-
const message = decision.reason ??
|
|
457
|
-
await this.emit(
|
|
458
|
-
throw new FabricError({
|
|
561
|
+
const message = decision.reason ?? "Command approval denied or unavailable";
|
|
562
|
+
await this.emit("error", { reason: message, command, approvalRequired: true });
|
|
563
|
+
throw new FabricError({
|
|
564
|
+
code: "COMMAND_DENIED",
|
|
565
|
+
message,
|
|
566
|
+
details: { command, approvalRequired: true },
|
|
567
|
+
});
|
|
459
568
|
}
|
|
460
569
|
policyApproved = true;
|
|
461
570
|
}
|
|
462
571
|
else {
|
|
463
|
-
const message = decision.reason ??
|
|
464
|
-
await this.emit(
|
|
465
|
-
throw new FabricError({ code:
|
|
572
|
+
const message = decision.reason ?? "Command denied by policy";
|
|
573
|
+
await this.emit("error", { reason: message, command });
|
|
574
|
+
throw new FabricError({ code: "COMMAND_DENIED", message, details: { command } });
|
|
466
575
|
}
|
|
467
576
|
}
|
|
468
577
|
if (options.cwd !== undefined)
|
|
@@ -474,17 +583,22 @@ export class StubFabricSession {
|
|
|
474
583
|
const timeout = clampCommandTimeout(options.timeout, policy);
|
|
475
584
|
if (timeout !== undefined)
|
|
476
585
|
execOptions.timeout = timeout;
|
|
477
|
-
await this.emit(
|
|
586
|
+
await this.emit("command_start", { command });
|
|
478
587
|
const execute = () => sandbox.exec(command, execOptions);
|
|
479
588
|
const result = policyApproved
|
|
480
|
-
? await runWithSandboxPolicyApproval(sandbox, { tool:
|
|
589
|
+
? await runWithSandboxPolicyApproval(sandbox, { tool: "bash", input: { command } }, execute)
|
|
481
590
|
: await execute();
|
|
482
591
|
const redactedCommand = redactEnvValues(command, options.env);
|
|
483
592
|
const toolCallId = `shell-${Date.now()}`;
|
|
484
|
-
await this.recordEntry(
|
|
485
|
-
await this.recordEntry(
|
|
486
|
-
|
|
487
|
-
|
|
593
|
+
await this.recordEntry("user_prompt", { text: `Run shell command: ${redactedCommand}` });
|
|
594
|
+
await this.recordEntry("tool_call", {
|
|
595
|
+
tool: "bash",
|
|
596
|
+
input: { command: redactedCommand },
|
|
597
|
+
toolCallId,
|
|
598
|
+
synthetic: true,
|
|
599
|
+
});
|
|
600
|
+
await this.recordEntry("tool_result", {
|
|
601
|
+
tool: "bash",
|
|
488
602
|
output: {
|
|
489
603
|
stdout: redactEnvValues(result.stdout, options.env),
|
|
490
604
|
stderr: redactEnvValues(result.stderr, options.env),
|
|
@@ -493,47 +607,75 @@ export class StubFabricSession {
|
|
|
493
607
|
toolCallId,
|
|
494
608
|
synthetic: true,
|
|
495
609
|
});
|
|
496
|
-
await this.emit(
|
|
610
|
+
await this.emit("command_end", {
|
|
611
|
+
command,
|
|
612
|
+
exitCode: result.exitCode,
|
|
613
|
+
...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
|
|
614
|
+
});
|
|
497
615
|
return result;
|
|
498
616
|
}
|
|
499
617
|
async mount(mountAt, source, options = {}) {
|
|
500
|
-
if (!mountAt || typeof mountAt !==
|
|
501
|
-
throw new FabricError({
|
|
618
|
+
if (!mountAt || typeof mountAt !== "string") {
|
|
619
|
+
throw new FabricError({
|
|
620
|
+
code: "MOUNT_INVALID_PATH",
|
|
621
|
+
message: "mount() requires a non-empty mountAt path.",
|
|
622
|
+
});
|
|
502
623
|
}
|
|
503
624
|
const sandbox = await this.sandbox;
|
|
504
625
|
const root = resolveMountPath(sandbox.cwd, mountAt);
|
|
505
|
-
if (!await sandbox.exists(root))
|
|
626
|
+
if (!(await sandbox.exists(root)))
|
|
506
627
|
await sandbox.mkdir(root, { recursive: true });
|
|
507
|
-
const mode = options.mode ??
|
|
628
|
+
const mode = options.mode ?? "read";
|
|
508
629
|
let files = 0;
|
|
509
630
|
let bytes = 0;
|
|
510
631
|
for await (const entry of source.entries()) {
|
|
511
|
-
const relative = entry.path.replace(/^\/+/,
|
|
632
|
+
const relative = entry.path.replace(/^\/+/, "");
|
|
512
633
|
const target = path.posix.join(root, relative);
|
|
513
634
|
const parent = path.posix.dirname(target);
|
|
514
|
-
if (parent && parent !==
|
|
515
|
-
if (!await sandbox.exists(parent))
|
|
635
|
+
if (parent && parent !== "." && parent !== "/") {
|
|
636
|
+
if (!(await sandbox.exists(parent)))
|
|
516
637
|
await sandbox.mkdir(parent, { recursive: true });
|
|
517
638
|
}
|
|
518
639
|
await sandbox.writeFile(target, entry.content);
|
|
519
640
|
files += 1;
|
|
520
|
-
bytes +=
|
|
641
|
+
bytes +=
|
|
642
|
+
typeof entry.content === "string"
|
|
643
|
+
? Buffer.byteLength(entry.content, "utf8")
|
|
644
|
+
: entry.content.byteLength;
|
|
521
645
|
}
|
|
522
646
|
this.mounts.push({ mountAt: root, mode });
|
|
523
|
-
const result = {
|
|
524
|
-
|
|
525
|
-
|
|
647
|
+
const result = {
|
|
648
|
+
mountAt: root,
|
|
649
|
+
files,
|
|
650
|
+
bytes,
|
|
651
|
+
mode,
|
|
652
|
+
...(source.name ? { source: source.name } : {}),
|
|
653
|
+
};
|
|
654
|
+
await this.recordEntry("mount", {
|
|
655
|
+
mountAt: root,
|
|
656
|
+
files,
|
|
657
|
+
bytes,
|
|
658
|
+
mode,
|
|
659
|
+
...(source.name ? { source: source.name } : {}),
|
|
660
|
+
});
|
|
661
|
+
await this.emit("mount", {
|
|
662
|
+
mountAt: root,
|
|
663
|
+
files,
|
|
664
|
+
bytes,
|
|
665
|
+
mode,
|
|
666
|
+
...(source.name ? { source: source.name } : {}),
|
|
667
|
+
});
|
|
526
668
|
return result;
|
|
527
669
|
}
|
|
528
670
|
checkMountWriteAccess(call) {
|
|
529
|
-
const writeTools = new Set([
|
|
671
|
+
const writeTools = new Set(["write", "edit", "mkdir", "rm"]);
|
|
530
672
|
if (!writeTools.has(call.tool) || this.mounts.length === 0)
|
|
531
673
|
return undefined;
|
|
532
|
-
const targetPath = readStringPropertyFromInput(call.input,
|
|
674
|
+
const targetPath = readStringPropertyFromInput(call.input, "path");
|
|
533
675
|
if (!targetPath)
|
|
534
676
|
return undefined;
|
|
535
677
|
for (const mount of this.mounts) {
|
|
536
|
-
if (mount.mode !==
|
|
678
|
+
if (mount.mode !== "read")
|
|
537
679
|
continue;
|
|
538
680
|
if (targetPath === mount.mountAt || targetPath.startsWith(`${mount.mountAt}/`)) {
|
|
539
681
|
return {
|
|
@@ -549,7 +691,7 @@ export class StubFabricSession {
|
|
|
549
691
|
return this.ensureData();
|
|
550
692
|
}
|
|
551
693
|
async createSignalEntry(data) {
|
|
552
|
-
return this.recordEntry(
|
|
694
|
+
return this.recordEntry("signal", data);
|
|
553
695
|
}
|
|
554
696
|
async sandboxRef(options) {
|
|
555
697
|
const sandbox = await this.sandbox;
|
|
@@ -566,7 +708,11 @@ export class StubFabricSession {
|
|
|
566
708
|
await originalCleanup();
|
|
567
709
|
};
|
|
568
710
|
}
|
|
569
|
-
await this.recordEntry(
|
|
711
|
+
await this.recordEntry("metric", {
|
|
712
|
+
event: "sandbox_ref_created",
|
|
713
|
+
refId: ref.id,
|
|
714
|
+
...(options?.portable ? { portable: true } : {}),
|
|
715
|
+
});
|
|
570
716
|
if (options?.portable) {
|
|
571
717
|
return serializeSandboxRef(ref, this.id, this.options.tenantId);
|
|
572
718
|
}
|
|
@@ -576,27 +722,31 @@ export class StubFabricSession {
|
|
|
576
722
|
const sandbox = await this.sandbox;
|
|
577
723
|
if (!sandbox.snapshot || !sandbox.fork) {
|
|
578
724
|
throw new FabricError({
|
|
579
|
-
code:
|
|
580
|
-
message:
|
|
725
|
+
code: "SANDBOX_UNAVAILABLE",
|
|
726
|
+
message: "Sandbox does not support fork. Required capabilities: snapshot + fork.",
|
|
581
727
|
});
|
|
582
728
|
}
|
|
583
729
|
const snapshot = await sandbox.snapshot();
|
|
584
730
|
const sandboxFork = sandbox.fork;
|
|
585
731
|
const agent = this.agent;
|
|
586
732
|
const baseOptions = this.options;
|
|
587
|
-
await this.recordEntry(
|
|
733
|
+
await this.recordEntry("checkpoint_created", {
|
|
588
734
|
label: label ?? `fork:${snapshot.id}`,
|
|
589
735
|
...(label ? { metadata: { fork: true } } : { metadata: { fork: true } }),
|
|
590
736
|
snapshotId: snapshot.id,
|
|
591
737
|
snapshot: snapshot,
|
|
592
738
|
});
|
|
593
|
-
await this.emit(
|
|
739
|
+
await this.emit("checkpoint_created", {
|
|
740
|
+
label: label ?? `fork:${snapshot.id}`,
|
|
741
|
+
snapshotId: snapshot.id,
|
|
742
|
+
});
|
|
594
743
|
return {
|
|
595
744
|
snapshotId: snapshot.id,
|
|
596
745
|
...(label !== undefined ? { label } : {}),
|
|
597
746
|
async attach(attachOptions = {}) {
|
|
598
747
|
const newSandbox = await sandboxFork.call(sandbox, snapshot);
|
|
599
|
-
const newId = attachOptions.id ??
|
|
748
|
+
const newId = attachOptions.id ??
|
|
749
|
+
`${baseOptions.metadata?.parentSessionId ?? "fork"}-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
600
750
|
const merged = {
|
|
601
751
|
...baseOptions,
|
|
602
752
|
...attachOptions,
|
|
@@ -613,21 +763,23 @@ export class StubFabricSession {
|
|
|
613
763
|
};
|
|
614
764
|
}
|
|
615
765
|
async artifact(name, content, options = {}) {
|
|
616
|
-
const bytes = typeof content ===
|
|
766
|
+
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
617
767
|
const fallback = {
|
|
618
|
-
id: `${Date.now()}-${name.replace(/[^a-zA-Z0-9._-]/g,
|
|
768
|
+
id: `${Date.now()}-${name.replace(/[^a-zA-Z0-9._-]/g, "_")}`,
|
|
619
769
|
sessionId: this.id,
|
|
620
770
|
name,
|
|
621
771
|
path: name,
|
|
622
772
|
size: bytes.byteLength,
|
|
623
|
-
sha256: createHash(
|
|
773
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
624
774
|
createdAt: new Date().toISOString(),
|
|
625
775
|
...(options.contentType ? { contentType: options.contentType } : {}),
|
|
626
776
|
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
627
777
|
};
|
|
628
|
-
const ref = this.store.putArtifact
|
|
629
|
-
|
|
630
|
-
|
|
778
|
+
const ref = this.store.putArtifact
|
|
779
|
+
? await this.store.putArtifact(this.id, name, bytes, options)
|
|
780
|
+
: fallback;
|
|
781
|
+
await this.recordEntry("artifact_created", { artifact: ref });
|
|
782
|
+
await this.emit("artifact_created", { artifact: ref });
|
|
631
783
|
return ref;
|
|
632
784
|
}
|
|
633
785
|
async compact(options = {}) {
|
|
@@ -661,12 +813,13 @@ export class StubFabricSession {
|
|
|
661
813
|
isSplitTurn: false,
|
|
662
814
|
fileRefs,
|
|
663
815
|
};
|
|
664
|
-
const baseSummary = options.summary ??
|
|
665
|
-
|
|
666
|
-
|
|
816
|
+
const baseSummary = options.summary ??
|
|
817
|
+
(options.generateSummary
|
|
818
|
+
? await this.generateCompactionSummary(summaryInput)
|
|
819
|
+
: summarizeEntries(compacted));
|
|
667
820
|
const summary = appendFileReferenceTags(baseSummary, fileRefs);
|
|
668
821
|
const compactedEntries = compacted.length;
|
|
669
|
-
const entry = await this.recordEntry(
|
|
822
|
+
const entry = await this.recordEntry("compaction", {
|
|
670
823
|
summary,
|
|
671
824
|
compactedEntries,
|
|
672
825
|
...(fileRefs.readFiles.length > 0 ? { readFiles: fileRefs.readFiles } : {}),
|
|
@@ -674,12 +827,12 @@ export class StubFabricSession {
|
|
|
674
827
|
...(firstKeptEntry ? { firstKeptEntryId: firstKeptEntry.id } : {}),
|
|
675
828
|
});
|
|
676
829
|
const messagesAfter = Math.max(0, entries.length - compactedEntries) + (compactedEntries > 0 ? 1 : 0);
|
|
677
|
-
await this.emit(
|
|
830
|
+
await this.emit("compaction", {
|
|
678
831
|
summary,
|
|
679
832
|
compactedEntries,
|
|
680
833
|
messagesBefore: options.messagesBefore ?? entries.length,
|
|
681
834
|
messagesAfter,
|
|
682
|
-
...(options.reason ? { reason: options.reason } : { reason:
|
|
835
|
+
...(options.reason ? { reason: options.reason } : { reason: "manual" }),
|
|
683
836
|
...(options.tokensBefore !== undefined ? { tokensBefore: options.tokensBefore } : {}),
|
|
684
837
|
});
|
|
685
838
|
return {
|
|
@@ -704,13 +857,20 @@ export class StubFabricSession {
|
|
|
704
857
|
...(contextWindowTokens !== undefined ? { contextWindowTokens } : {}),
|
|
705
858
|
...(compactAtTokens !== undefined ? { compactAtTokens } : {}),
|
|
706
859
|
...(reserveTokens !== undefined ? { reserveTokens } : {}),
|
|
707
|
-
...(settings?.keepRecentTokens !== undefined
|
|
860
|
+
...(settings?.keepRecentTokens !== undefined
|
|
861
|
+
? { keepRecentTokens: settings.keepRecentTokens }
|
|
862
|
+
: {}),
|
|
708
863
|
});
|
|
709
864
|
if (!budget.shouldCompact)
|
|
710
865
|
return messages;
|
|
711
866
|
const messagesBefore = messages.length;
|
|
712
867
|
const tokensBefore = budget.estimatedTokens;
|
|
713
|
-
const compactOpts = {
|
|
868
|
+
const compactOpts = {
|
|
869
|
+
generateSummary: true,
|
|
870
|
+
reason: "threshold",
|
|
871
|
+
tokensBefore,
|
|
872
|
+
messagesBefore,
|
|
873
|
+
};
|
|
714
874
|
if (keepRecentEntries !== undefined) {
|
|
715
875
|
compactOpts.keepRecentEntries = keepRecentEntries;
|
|
716
876
|
}
|
|
@@ -725,7 +885,7 @@ export class StubFabricSession {
|
|
|
725
885
|
const provider = options.modelProvider ?? this.options.modelProvider;
|
|
726
886
|
if (!provider)
|
|
727
887
|
return undefined;
|
|
728
|
-
const model = typeof (options.model ?? this.options.model ?? this.agent.model) ===
|
|
888
|
+
const model = typeof (options.model ?? this.options.model ?? this.agent.model) === "string"
|
|
729
889
|
? (options.model ?? this.options.model ?? this.agent.model)
|
|
730
890
|
: undefined;
|
|
731
891
|
return this.readProviderModelMetadata(provider, model)?.contextWindowTokens;
|
|
@@ -754,13 +914,19 @@ export class StubFabricSession {
|
|
|
754
914
|
const deterministic = summarizeEntries(entries);
|
|
755
915
|
const provider = this.options.modelProvider ?? defaultModelProvider;
|
|
756
916
|
try {
|
|
757
|
-
const serialized = JSON.stringify(entries
|
|
917
|
+
const serialized = JSON.stringify(entries
|
|
918
|
+
.map((entry) => ({
|
|
919
|
+
type: entry.type,
|
|
920
|
+
timestamp: entry.timestamp,
|
|
921
|
+
data: entry.data ?? {},
|
|
922
|
+
}))
|
|
923
|
+
.slice(0, 200));
|
|
758
924
|
const userContent = prefixPrompt ? `${serialized}\n\n${prefixPrompt}` : serialized;
|
|
759
925
|
const response = await generateWithRuntime(provider, {
|
|
760
926
|
...(this.agent.model !== undefined ? { model: this.agent.model } : {}),
|
|
761
927
|
messages: [
|
|
762
|
-
{ role:
|
|
763
|
-
{ role:
|
|
928
|
+
{ role: "system", content: COMPACTION_SUMMARIZATION_SYSTEM_PROMPT },
|
|
929
|
+
{ role: "user", content: userContent },
|
|
764
930
|
],
|
|
765
931
|
}, { retries: 1, retryDelayMs: 250 });
|
|
766
932
|
return response.message?.content?.trim() || deterministic;
|
|
@@ -772,25 +938,25 @@ export class StubFabricSession {
|
|
|
772
938
|
async requestCustomApproval(input) {
|
|
773
939
|
const timeoutMs = input.timeoutMs ?? this.options.approvalTimeoutMs;
|
|
774
940
|
const approvalPromise = this.requestApproval({
|
|
775
|
-
kind:
|
|
941
|
+
kind: "custom",
|
|
776
942
|
reason: input.reason,
|
|
777
|
-
subject: input.subject ??
|
|
943
|
+
subject: input.subject ?? "custom",
|
|
778
944
|
input: undefined,
|
|
779
945
|
...(input.risk ? { risk: input.risk } : {}),
|
|
780
946
|
...(input.idempotencyKey !== undefined ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
781
947
|
}, this.options.policy, timeoutMs, input.onApproval);
|
|
782
|
-
let
|
|
948
|
+
let grant;
|
|
783
949
|
if (timeoutMs !== undefined) {
|
|
784
950
|
let timer;
|
|
785
951
|
const timeoutPromise = new Promise((_, reject) => {
|
|
786
952
|
timer = setTimeout(() => reject(new FabricError({
|
|
787
|
-
code:
|
|
953
|
+
code: "APPROVAL_REQUIRED",
|
|
788
954
|
message: `Approval timed out after ${timeoutMs}ms.`,
|
|
789
955
|
retryable: false,
|
|
790
956
|
})), timeoutMs);
|
|
791
957
|
});
|
|
792
958
|
try {
|
|
793
|
-
|
|
959
|
+
grant = await Promise.race([approvalPromise, timeoutPromise]);
|
|
794
960
|
}
|
|
795
961
|
finally {
|
|
796
962
|
if (timer)
|
|
@@ -798,11 +964,11 @@ export class StubFabricSession {
|
|
|
798
964
|
}
|
|
799
965
|
}
|
|
800
966
|
else {
|
|
801
|
-
|
|
967
|
+
grant = await approvalPromise;
|
|
802
968
|
}
|
|
803
|
-
if (!
|
|
969
|
+
if (!grant) {
|
|
804
970
|
throw new FabricError({
|
|
805
|
-
code:
|
|
971
|
+
code: "APPROVAL_DENIED",
|
|
806
972
|
message: `Custom approval denied: ${input.reason}`,
|
|
807
973
|
retryable: false,
|
|
808
974
|
});
|
|
@@ -813,38 +979,52 @@ export class StubFabricSession {
|
|
|
813
979
|
const durable = await this.getDurableRuntime();
|
|
814
980
|
if (durable)
|
|
815
981
|
return durable.checkpointCreate({ options });
|
|
816
|
-
const input = typeof options ===
|
|
982
|
+
const input = typeof options === "string" ? { label: options } : options;
|
|
817
983
|
const sandbox = await this.sandbox;
|
|
818
984
|
const snapshot = sandbox.snapshot ? await sandbox.snapshot() : undefined;
|
|
819
|
-
const entry = await this.recordEntry(
|
|
985
|
+
const entry = await this.recordEntry("checkpoint_created", {
|
|
820
986
|
label: input.label,
|
|
821
987
|
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
822
988
|
...(snapshot ? { snapshotId: snapshot.id, snapshot: snapshot } : {}),
|
|
823
989
|
});
|
|
824
|
-
await this.emit(
|
|
825
|
-
|
|
990
|
+
await this.emit("checkpoint_created", {
|
|
991
|
+
label: input.label,
|
|
992
|
+
...(snapshot ? { snapshotId: snapshot.id } : {}),
|
|
993
|
+
});
|
|
994
|
+
return {
|
|
995
|
+
label: input.label,
|
|
996
|
+
entryId: entry.id,
|
|
997
|
+
...(snapshot ? { snapshotId: snapshot.id } : {}),
|
|
998
|
+
};
|
|
826
999
|
}
|
|
827
1000
|
async restoreCheckpoint(options) {
|
|
828
1001
|
const durable = await this.getDurableRuntime();
|
|
829
1002
|
if (durable)
|
|
830
1003
|
return durable.checkpointRestore({ options });
|
|
831
|
-
const input = typeof options ===
|
|
1004
|
+
const input = typeof options === "string" ? { label: options } : options;
|
|
832
1005
|
const history = await this.history();
|
|
833
1006
|
const checkpoint = [...(history.entries ?? [])]
|
|
834
1007
|
.reverse()
|
|
835
|
-
.find((entry) => entry.type ===
|
|
1008
|
+
.find((entry) => entry.type === "checkpoint_created" && entry.data?.label === input.label);
|
|
836
1009
|
if (!checkpoint)
|
|
837
|
-
throw new FabricError({
|
|
1010
|
+
throw new FabricError({
|
|
1011
|
+
code: "CHECKPOINT_NOT_FOUND",
|
|
1012
|
+
message: `Checkpoint not found: ${input.label}`,
|
|
1013
|
+
details: { label: input.label },
|
|
1014
|
+
});
|
|
838
1015
|
const sandbox = await this.sandbox;
|
|
839
1016
|
const snapshot = checkpoint.data?.snapshot;
|
|
840
1017
|
if (sandbox.restore && isSnapshot(snapshot))
|
|
841
1018
|
await sandbox.restore(snapshot);
|
|
842
|
-
const entry = await this.recordEntry(
|
|
1019
|
+
const entry = await this.recordEntry("checkpoint_restored", {
|
|
843
1020
|
label: input.label,
|
|
844
1021
|
checkpointEntryId: checkpoint.id,
|
|
845
1022
|
...(isSnapshot(snapshot) ? { snapshotId: snapshot.id } : {}),
|
|
846
1023
|
});
|
|
847
|
-
await this.emit(
|
|
1024
|
+
await this.emit("checkpoint_restored", {
|
|
1025
|
+
label: input.label,
|
|
1026
|
+
checkpointEntryId: checkpoint.id,
|
|
1027
|
+
});
|
|
848
1028
|
return {
|
|
849
1029
|
label: input.label,
|
|
850
1030
|
entryId: entry.id,
|
|
@@ -854,12 +1034,19 @@ export class StubFabricSession {
|
|
|
854
1034
|
async createSandbox() {
|
|
855
1035
|
const sandbox = this.options.sandbox;
|
|
856
1036
|
let env;
|
|
857
|
-
if (typeof sandbox ===
|
|
1037
|
+
if (typeof sandbox === "object" && sandbox !== null && "exec" in sandbox)
|
|
858
1038
|
env = sandbox;
|
|
859
|
-
else if (typeof sandbox ===
|
|
860
|
-
env = await sandbox({
|
|
1039
|
+
else if (typeof sandbox === "function")
|
|
1040
|
+
env = await sandbox({
|
|
1041
|
+
sessionId: this.id,
|
|
1042
|
+
...(this.options.cwd !== undefined ? { cwd: this.options.cwd } : {}),
|
|
1043
|
+
});
|
|
861
1044
|
else
|
|
862
|
-
env = await createSandboxEnv({
|
|
1045
|
+
env = await createSandboxEnv({
|
|
1046
|
+
backend: sandbox ?? "empty",
|
|
1047
|
+
sessionId: this.id,
|
|
1048
|
+
...(this.options.cwd !== undefined ? { cwd: this.options.cwd } : {}),
|
|
1049
|
+
});
|
|
863
1050
|
const scoped = createScopedSandboxEnv(env, this.options.cwd);
|
|
864
1051
|
return wrapWithPolicy(scoped, this.options.policy, this.options.bypassPolicyEnforcement);
|
|
865
1052
|
}
|
|
@@ -868,39 +1055,56 @@ export class StubFabricSession {
|
|
|
868
1055
|
const policy = callPolicy ?? this.options.policy;
|
|
869
1056
|
for (const call of calls) {
|
|
870
1057
|
const approvedSandboxCalls = [];
|
|
1058
|
+
let approvalGrant;
|
|
871
1059
|
const tool = tools.get(call.tool);
|
|
1060
|
+
const executingPrincipal = toolExecutingPrincipal(tool);
|
|
872
1061
|
const mountDecision = this.checkMountWriteAccess(call);
|
|
873
1062
|
const policyCall = tool?.policyAlias ? { ...call, tool: tool.policyAlias } : call;
|
|
874
1063
|
const decision = mountDecision ?? evaluateToolCallPolicy(policyCall, policy);
|
|
875
1064
|
if (!decision.allowed) {
|
|
876
1065
|
if (decision.approvalRequired) {
|
|
877
|
-
const
|
|
878
|
-
kind:
|
|
1066
|
+
const grant = await this.requestApproval({
|
|
1067
|
+
kind: "tool",
|
|
879
1068
|
tool: call.tool,
|
|
1069
|
+
toolCallId: call.id ?? `${this.id}:${call.tool}`,
|
|
1070
|
+
...(executingPrincipal ? { executingPrincipal } : {}),
|
|
880
1071
|
input: call.input,
|
|
881
1072
|
reason: decision.reason ?? `Approval required for tool: ${call.tool}`,
|
|
882
1073
|
...(decision.risk ? { risk: decision.risk } : {}),
|
|
883
1074
|
...(decision.matchedPattern ? { matchedPattern: decision.matchedPattern } : {}),
|
|
884
1075
|
...(decision.approvalAudience ? { audience: decision.approvalAudience } : {}),
|
|
885
|
-
...(decision.approvalTtlSeconds !== undefined
|
|
1076
|
+
...(decision.approvalTtlSeconds !== undefined
|
|
1077
|
+
? { ttlSeconds: decision.approvalTtlSeconds }
|
|
1078
|
+
: {}),
|
|
886
1079
|
affectedPaths: affectedPathsFromInput(call.input),
|
|
887
1080
|
}, policy, approvalTimeoutMs, approvalHandler);
|
|
888
|
-
if (
|
|
1081
|
+
if (grant) {
|
|
1082
|
+
approvalGrant = grant;
|
|
889
1083
|
approvedSandboxCalls.push(policyCall);
|
|
890
1084
|
}
|
|
891
1085
|
else {
|
|
892
1086
|
const error = decision.reason ?? `Approval required for tool call: ${call.tool}`;
|
|
893
|
-
results.push({
|
|
894
|
-
|
|
895
|
-
|
|
1087
|
+
results.push({
|
|
1088
|
+
tool: call.tool,
|
|
1089
|
+
input: call.input,
|
|
1090
|
+
error,
|
|
1091
|
+
...(call.id ? { id: call.id } : {}),
|
|
1092
|
+
});
|
|
1093
|
+
await this.recordEntry("error", { tool: call.tool, error, approvalRequired: true });
|
|
1094
|
+
await this.emit("error", { tool: call.tool, error, approvalRequired: true });
|
|
896
1095
|
continue;
|
|
897
1096
|
}
|
|
898
1097
|
}
|
|
899
1098
|
else {
|
|
900
1099
|
const error = decision.reason ?? `Tool call denied by policy: ${call.tool}`;
|
|
901
|
-
results.push({
|
|
902
|
-
|
|
903
|
-
|
|
1100
|
+
results.push({
|
|
1101
|
+
tool: call.tool,
|
|
1102
|
+
input: call.input,
|
|
1103
|
+
error,
|
|
1104
|
+
...(call.id ? { id: call.id } : {}),
|
|
1105
|
+
});
|
|
1106
|
+
await this.recordEntry("error", { tool: call.tool, error, approvalRequired: false });
|
|
1107
|
+
await this.emit("error", { tool: call.tool, error, approvalRequired: false });
|
|
904
1108
|
continue;
|
|
905
1109
|
}
|
|
906
1110
|
}
|
|
@@ -908,87 +1112,160 @@ export class StubFabricSession {
|
|
|
908
1112
|
if (!commandDecision.allowed) {
|
|
909
1113
|
if (commandDecision.approvalRequired) {
|
|
910
1114
|
const command = this.commandTextForApproval(tool, call.input);
|
|
911
|
-
const
|
|
912
|
-
kind:
|
|
1115
|
+
const grant = await this.requestApproval({
|
|
1116
|
+
kind: "command",
|
|
913
1117
|
tool: call.tool,
|
|
1118
|
+
toolCallId: call.id ?? `${this.id}:${call.tool}`,
|
|
1119
|
+
...(executingPrincipal ? { executingPrincipal } : {}),
|
|
914
1120
|
...(command ? { command } : {}),
|
|
915
1121
|
input: call.input,
|
|
916
1122
|
reason: commandDecision.reason ?? `Approval required for command: ${call.tool}`,
|
|
917
1123
|
...(commandDecision.risk ? { risk: commandDecision.risk } : {}),
|
|
918
|
-
...(commandDecision.matchedPattern
|
|
919
|
-
|
|
920
|
-
|
|
1124
|
+
...(commandDecision.matchedPattern
|
|
1125
|
+
? { matchedPattern: commandDecision.matchedPattern }
|
|
1126
|
+
: {}),
|
|
1127
|
+
...(commandDecision.approvalAudience
|
|
1128
|
+
? { audience: commandDecision.approvalAudience }
|
|
1129
|
+
: {}),
|
|
1130
|
+
...(commandDecision.approvalTtlSeconds !== undefined
|
|
1131
|
+
? { ttlSeconds: commandDecision.approvalTtlSeconds }
|
|
1132
|
+
: {}),
|
|
921
1133
|
envKeys: commandEnvKeys(tool),
|
|
922
1134
|
affectedPaths: affectedPathsFromInput(call.input),
|
|
923
1135
|
}, policy, approvalTimeoutMs, approvalHandler);
|
|
924
|
-
if (!
|
|
1136
|
+
if (!grant) {
|
|
925
1137
|
const error = commandDecision.reason ?? `Approval required for command: ${call.tool}`;
|
|
926
|
-
results.push({
|
|
927
|
-
|
|
928
|
-
|
|
1138
|
+
results.push({
|
|
1139
|
+
tool: call.tool,
|
|
1140
|
+
input: call.input,
|
|
1141
|
+
error,
|
|
1142
|
+
...(call.id ? { id: call.id } : {}),
|
|
1143
|
+
});
|
|
1144
|
+
await this.recordEntry("error", { tool: call.tool, error, approvalRequired: true });
|
|
1145
|
+
await this.emit("error", { tool: call.tool, error, approvalRequired: true });
|
|
929
1146
|
continue;
|
|
930
1147
|
}
|
|
1148
|
+
approvalGrant = grant;
|
|
931
1149
|
const approvedCommand = this.commandTextForApproval(tool, call.input);
|
|
932
1150
|
if (approvedCommand)
|
|
933
|
-
approvedSandboxCalls.push({ tool:
|
|
1151
|
+
approvedSandboxCalls.push({ tool: "bash", input: { command: approvedCommand } });
|
|
934
1152
|
}
|
|
935
1153
|
else {
|
|
936
1154
|
const error = commandDecision.reason ?? `Command denied by policy: ${call.tool}`;
|
|
937
|
-
results.push({
|
|
938
|
-
|
|
939
|
-
|
|
1155
|
+
results.push({
|
|
1156
|
+
tool: call.tool,
|
|
1157
|
+
input: call.input,
|
|
1158
|
+
error,
|
|
1159
|
+
...(call.id ? { id: call.id } : {}),
|
|
1160
|
+
});
|
|
1161
|
+
await this.recordEntry("error", { tool: call.tool, error, approvalRequired: false });
|
|
1162
|
+
await this.emit("error", { tool: call.tool, error, approvalRequired: false });
|
|
940
1163
|
continue;
|
|
941
1164
|
}
|
|
942
1165
|
}
|
|
943
|
-
await this.recordEntry(
|
|
944
|
-
|
|
1166
|
+
await this.recordEntry("tool_call", {
|
|
1167
|
+
tool: call.tool,
|
|
1168
|
+
input: call.input,
|
|
1169
|
+
...(tool?.metadata?.effect ? { effect: tool.metadata.effect } : {}),
|
|
1170
|
+
...(call.id ? { toolCallId: call.id } : {}),
|
|
1171
|
+
...(attribution ? { attribution: attribution } : {}),
|
|
1172
|
+
});
|
|
1173
|
+
await this.emit("tool_start", {
|
|
1174
|
+
tool: call.tool,
|
|
1175
|
+
...(attribution ? { attribution: attribution } : {}),
|
|
1176
|
+
});
|
|
945
1177
|
if (!tool?.execute) {
|
|
946
1178
|
const error = `Tool is not available: ${call.tool}`;
|
|
947
|
-
results.push({
|
|
948
|
-
|
|
949
|
-
|
|
1179
|
+
results.push({
|
|
1180
|
+
tool: call.tool,
|
|
1181
|
+
input: call.input,
|
|
1182
|
+
error,
|
|
1183
|
+
...(call.id ? { id: call.id } : {}),
|
|
1184
|
+
});
|
|
1185
|
+
await this.recordEntry("error", { tool: call.tool, error, approvalRequired: false });
|
|
1186
|
+
await this.emit("tool_end", {
|
|
1187
|
+
tool: call.tool,
|
|
1188
|
+
error,
|
|
1189
|
+
...(attribution ? { attribution: attribution } : {}),
|
|
1190
|
+
});
|
|
950
1191
|
continue;
|
|
951
1192
|
}
|
|
952
1193
|
try {
|
|
953
|
-
const input = call.tool ===
|
|
1194
|
+
const input = call.tool === "bash" ? this.clampBashToolInput(call.input, policy) : call.input;
|
|
954
1195
|
const startedAt = Date.now();
|
|
955
|
-
const execute = () => Promise.resolve(tool.execute?.(input, {
|
|
1196
|
+
const execute = () => Promise.resolve(tool.execute?.(input, {
|
|
1197
|
+
sandbox,
|
|
1198
|
+
toolCallId: call.id ?? `${this.id}:${call.tool}`,
|
|
1199
|
+
...(signal ? { signal } : {}),
|
|
1200
|
+
...(approvalGrant ? { approval: approvalGrant } : {}),
|
|
1201
|
+
}));
|
|
956
1202
|
const output = await approvedSandboxCalls.reduceRight((next, approvedCall) => () => runWithSandboxPolicyApproval(sandbox, approvedCall, next), execute)();
|
|
957
1203
|
const durationMs = Date.now() - startedAt;
|
|
958
1204
|
results.push({ tool: call.tool, input, output, ...(call.id ? { id: call.id } : {}) });
|
|
959
|
-
await this.recordEntry(
|
|
960
|
-
|
|
1205
|
+
await this.recordEntry("tool_result", {
|
|
1206
|
+
tool: call.tool,
|
|
1207
|
+
output: output,
|
|
1208
|
+
durationMs,
|
|
1209
|
+
...(call.id ? { toolCallId: call.id } : {}),
|
|
1210
|
+
...(attribution ? { attribution: attribution } : {}),
|
|
1211
|
+
});
|
|
1212
|
+
await this.emit("tool_end", {
|
|
1213
|
+
tool: call.tool,
|
|
1214
|
+
durationMs,
|
|
1215
|
+
...(attribution ? { attribution: attribution } : {}),
|
|
1216
|
+
});
|
|
961
1217
|
}
|
|
962
1218
|
catch (error) {
|
|
963
1219
|
const message = error instanceof Error ? error.message : String(error);
|
|
964
|
-
results.push({
|
|
965
|
-
|
|
966
|
-
|
|
1220
|
+
results.push({
|
|
1221
|
+
tool: call.tool,
|
|
1222
|
+
input: call.input,
|
|
1223
|
+
error: message,
|
|
1224
|
+
...(call.id ? { id: call.id } : {}),
|
|
1225
|
+
});
|
|
1226
|
+
await this.recordEntry("error", { tool: call.tool, error: message });
|
|
1227
|
+
await this.emit("tool_end", {
|
|
1228
|
+
tool: call.tool,
|
|
1229
|
+
error: message,
|
|
1230
|
+
...(attribution ? { attribution: attribution } : {}),
|
|
1231
|
+
});
|
|
967
1232
|
}
|
|
968
1233
|
}
|
|
969
1234
|
return results;
|
|
970
1235
|
}
|
|
971
1236
|
async requestCostLimitApproval(ctx, approvalTimeoutMs, approvalHandler) {
|
|
972
|
-
return this.requestApproval({
|
|
973
|
-
kind:
|
|
1237
|
+
return Boolean(await this.requestApproval({
|
|
1238
|
+
kind: "cost-limit",
|
|
974
1239
|
input: ctx,
|
|
975
1240
|
reason: `Cost limit (${ctx.scope}) exceeded: $${ctx.observedUsd.toFixed(6)} > $${ctx.limitUsd.toFixed(6)}`,
|
|
976
1241
|
subject: `cost-limit/${ctx.scope}`,
|
|
977
|
-
}, this.options.policy, approvalTimeoutMs, approvalHandler);
|
|
1242
|
+
}, this.options.policy, approvalTimeoutMs, approvalHandler));
|
|
978
1243
|
}
|
|
979
1244
|
async requestApproval(input, callPolicy, approvalTimeoutMs, approvalHandler) {
|
|
980
|
-
const
|
|
981
|
-
|
|
1245
|
+
const executingPrincipal = input.executingPrincipal ?? this.approvalPrincipal();
|
|
1246
|
+
let id = input.idempotencyKey !== undefined
|
|
1247
|
+
? `${this.id}-approval-${createHash("sha256").update(`${input.kind}\0${input.idempotencyKey}`).digest("hex").slice(0, 32)}`
|
|
982
1248
|
: `${this.id}-approval-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
983
1249
|
if (input.idempotencyKey !== undefined && this.store.getApprovalState) {
|
|
984
1250
|
const existing = await this.store.getApprovalState(this.id, id);
|
|
985
|
-
if (existing?.status ===
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1251
|
+
if (existing?.status === "approved") {
|
|
1252
|
+
const grant = approvalGrantFromState(existing, input.input, executingPrincipal, input.toolCallId ?? id);
|
|
1253
|
+
if (grant)
|
|
1254
|
+
return grant;
|
|
1255
|
+
// Pre-provenance approvals cannot authorize a model-facing operation.
|
|
1256
|
+
// Ask again under a new id so upgraded durable sessions recover safely.
|
|
1257
|
+
id = `${id}-provenance-${Date.now()}`;
|
|
1258
|
+
}
|
|
1259
|
+
if (existing?.status === "denied" || existing?.status === "expired")
|
|
1260
|
+
return undefined;
|
|
1261
|
+
if (existing?.status === "pending" && this.store.waitForApproval) {
|
|
990
1262
|
const response = await this.waitForStoredApproval(existing.request, approvalTimeoutMs ?? callPolicy?.approvals?.defaultTimeoutMs);
|
|
991
|
-
|
|
1263
|
+
if (response?.decision !== "approved")
|
|
1264
|
+
return undefined;
|
|
1265
|
+
const resolved = await this.store.getApprovalState(this.id, id);
|
|
1266
|
+
return resolved
|
|
1267
|
+
? approvalGrantFromState(resolved, input.input, executingPrincipal, input.toolCallId ?? id)
|
|
1268
|
+
: undefined;
|
|
992
1269
|
}
|
|
993
1270
|
}
|
|
994
1271
|
const request = {
|
|
@@ -999,27 +1276,34 @@ export class StubFabricSession {
|
|
|
999
1276
|
kind: input.kind,
|
|
1000
1277
|
createdAt: new Date().toISOString(),
|
|
1001
1278
|
...(input.risk ? { risk: input.risk } : {}),
|
|
1002
|
-
...(callPolicy?.approvals?.requiredApprovals
|
|
1279
|
+
...(callPolicy?.approvals?.requiredApprovals
|
|
1280
|
+
? { requiredApprovals: callPolicy.approvals.requiredApprovals }
|
|
1281
|
+
: {}),
|
|
1003
1282
|
...(callPolicy?.approvals?.escalation ? { escalation: callPolicy.approvals.escalation } : {}),
|
|
1004
1283
|
...(input.matchedPattern ? { matchedPattern: input.matchedPattern } : {}),
|
|
1005
1284
|
...(input.envKeys && input.envKeys.length > 0 ? { envKeys: input.envKeys } : {}),
|
|
1006
|
-
...(input.affectedPaths && input.affectedPaths.length > 0
|
|
1285
|
+
...(input.affectedPaths && input.affectedPaths.length > 0
|
|
1286
|
+
? { affectedPaths: input.affectedPaths }
|
|
1287
|
+
: {}),
|
|
1007
1288
|
...(input.tool ? { tool: input.tool } : {}),
|
|
1008
1289
|
...(input.command ? { command: input.command } : {}),
|
|
1009
1290
|
...(isJsonValue(input.input) ? { input: input.input } : {}),
|
|
1291
|
+
toolCallId: input.toolCallId ?? id,
|
|
1292
|
+
inputDigest: approvalInputDigest(input.input),
|
|
1293
|
+
executingPrincipal,
|
|
1010
1294
|
...(this.options.actor ? { actor: this.options.actor } : {}),
|
|
1011
1295
|
...(input.audience ? { audience: input.audience } : {}),
|
|
1012
1296
|
...(input.ttlSeconds !== undefined ? { ttlSeconds: input.ttlSeconds } : {}),
|
|
1013
1297
|
...(this.options.tenantId ? { tenantId: this.options.tenantId } : {}),
|
|
1014
1298
|
};
|
|
1015
|
-
await this.recordEntry(
|
|
1016
|
-
await this.emit(
|
|
1299
|
+
await this.recordEntry("approval_requested", request);
|
|
1300
|
+
await this.emit("approval_requested", request);
|
|
1017
1301
|
const handler = approvalHandler ?? this.options.onApproval;
|
|
1018
1302
|
const autonomy = this.effectiveAutonomy();
|
|
1019
1303
|
const waitTimeout = approvalTimeoutMs ?? callPolicy?.approvals?.defaultTimeoutMs;
|
|
1020
1304
|
const durable = await this.getDurableRuntime();
|
|
1021
|
-
const hasDurableApprovalChannel = typeof durable?.approvalRequest ===
|
|
1022
|
-
const hasStoredApprovalBackend = typeof this.store.waitForApproval ===
|
|
1305
|
+
const hasDurableApprovalChannel = typeof durable?.approvalRequest === "function";
|
|
1306
|
+
const hasStoredApprovalBackend = typeof this.store.waitForApproval === "function";
|
|
1023
1307
|
let response;
|
|
1024
1308
|
if (handler) {
|
|
1025
1309
|
response = normalizeApprovalResponse(await handler(request));
|
|
@@ -1042,25 +1326,66 @@ export class StubFabricSession {
|
|
|
1042
1326
|
? `Approval unavailable in ${autonomy.mode} mode: no approval handler, durable runtime channel, or store waiter is configured (${autonomy.onApprovalUnavailable}).`
|
|
1043
1327
|
: waitTimeout !== undefined
|
|
1044
1328
|
? `Approval timed out after ${waitTimeout}ms`
|
|
1045
|
-
:
|
|
1046
|
-
if (!handler &&
|
|
1047
|
-
|
|
1048
|
-
|
|
1329
|
+
: "No approval response was received";
|
|
1330
|
+
if (!handler &&
|
|
1331
|
+
!hasStoredApprovalBackend &&
|
|
1332
|
+
!hasDurableApprovalChannel &&
|
|
1333
|
+
autonomy.onApprovalUnavailable === "deny") {
|
|
1334
|
+
await this.recordEntry("approval_denied", { approvalId: id, reason: unavailableReason });
|
|
1335
|
+
await this.emit("approval_denied", { approvalId: id, reason: unavailableReason });
|
|
1049
1336
|
}
|
|
1050
1337
|
else {
|
|
1051
|
-
await this.recordEntry(
|
|
1052
|
-
await this.emit(
|
|
1338
|
+
await this.recordEntry("approval_expired", { approvalId: id, reason: unavailableReason });
|
|
1339
|
+
await this.emit("approval_expired", { approvalId: id, reason: unavailableReason });
|
|
1053
1340
|
}
|
|
1054
|
-
return
|
|
1341
|
+
return undefined;
|
|
1055
1342
|
}
|
|
1056
|
-
if (response.decision ===
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1343
|
+
if (response.decision === "approved") {
|
|
1344
|
+
if (this.store.getApprovalState) {
|
|
1345
|
+
const resolved = await this.store.getApprovalState(this.id, id);
|
|
1346
|
+
const persisted = resolved
|
|
1347
|
+
? approvalGrantFromState(resolved, input.input, executingPrincipal, input.toolCallId ?? id)
|
|
1348
|
+
: undefined;
|
|
1349
|
+
if (persisted)
|
|
1350
|
+
return persisted;
|
|
1351
|
+
}
|
|
1352
|
+
const grant = createApprovalGrant({
|
|
1353
|
+
approvalId: id,
|
|
1354
|
+
toolCallId: input.toolCallId ?? id,
|
|
1355
|
+
toolInput: input.input,
|
|
1356
|
+
principal: executingPrincipal,
|
|
1357
|
+
response,
|
|
1358
|
+
createdAt: request.createdAt,
|
|
1359
|
+
...(request.ttlSeconds !== undefined ? { ttlSeconds: request.ttlSeconds } : {}),
|
|
1360
|
+
});
|
|
1361
|
+
const grantData = grant;
|
|
1362
|
+
await this.recordEntry("approval_granted", {
|
|
1363
|
+
approvalId: id,
|
|
1364
|
+
reason: response.reason ?? "Approved by handler",
|
|
1365
|
+
grant: grantData,
|
|
1366
|
+
});
|
|
1367
|
+
await this.emit("approval_granted", {
|
|
1368
|
+
approvalId: id,
|
|
1369
|
+
reason: response.reason ?? "Approved by handler",
|
|
1370
|
+
grant: grantData,
|
|
1371
|
+
});
|
|
1372
|
+
return grant;
|
|
1060
1373
|
}
|
|
1061
|
-
await this.recordEntry(
|
|
1062
|
-
|
|
1063
|
-
|
|
1374
|
+
await this.recordEntry("approval_denied", {
|
|
1375
|
+
approvalId: id,
|
|
1376
|
+
reason: response.reason ?? "Denied by handler",
|
|
1377
|
+
});
|
|
1378
|
+
await this.emit("approval_denied", {
|
|
1379
|
+
approvalId: id,
|
|
1380
|
+
reason: response.reason ?? "Denied by handler",
|
|
1381
|
+
});
|
|
1382
|
+
return undefined;
|
|
1383
|
+
}
|
|
1384
|
+
approvalPrincipal() {
|
|
1385
|
+
return (this.options.actor?.principal ?? {
|
|
1386
|
+
kind: "app-principal",
|
|
1387
|
+
id: `fabric-session:${this.id}`,
|
|
1388
|
+
});
|
|
1064
1389
|
}
|
|
1065
1390
|
async waitForStoredApproval(request, timeoutMs) {
|
|
1066
1391
|
if (!this.store.waitForApproval)
|
|
@@ -1069,39 +1394,45 @@ export class StubFabricSession {
|
|
|
1069
1394
|
}
|
|
1070
1395
|
effectiveAutonomy() {
|
|
1071
1396
|
return {
|
|
1072
|
-
mode: this.options.autonomy?.mode ??
|
|
1073
|
-
onMissingInput: this.options.autonomy?.onMissingInput ??
|
|
1074
|
-
onApprovalUnavailable: this.options.autonomy?.onApprovalUnavailable ??
|
|
1075
|
-
onCredentialMissing: this.options.autonomy?.onCredentialMissing ??
|
|
1397
|
+
mode: this.options.autonomy?.mode ?? "background",
|
|
1398
|
+
onMissingInput: this.options.autonomy?.onMissingInput ?? "assume",
|
|
1399
|
+
onApprovalUnavailable: this.options.autonomy?.onApprovalUnavailable ?? "fail",
|
|
1400
|
+
onCredentialMissing: this.options.autonomy?.onCredentialMissing ?? "fail",
|
|
1076
1401
|
};
|
|
1077
1402
|
}
|
|
1078
1403
|
evaluateScopedCommandToolCall(tool, input, policy) {
|
|
1079
|
-
if (tool?.metadata?.kind !==
|
|
1404
|
+
if (tool?.metadata?.kind !== "command" || typeof tool.metadata.command !== "string")
|
|
1080
1405
|
return { allowed: true };
|
|
1081
|
-
const args = typeof input ===
|
|
1082
|
-
|
|
1406
|
+
const args = typeof input === "object" &&
|
|
1407
|
+
input !== null &&
|
|
1408
|
+
Array.isArray(input.args)
|
|
1409
|
+
? input.args.filter((value) => typeof value === "string")
|
|
1083
1410
|
: [];
|
|
1084
|
-
const command = [tool.metadata.command, ...args.map(shellQuote)].join(
|
|
1411
|
+
const command = [tool.metadata.command, ...args.map(shellQuote)].join(" ");
|
|
1085
1412
|
return evaluateCommandPolicy(command, policy);
|
|
1086
1413
|
}
|
|
1087
1414
|
commandTextForApproval(tool, input) {
|
|
1088
|
-
if (tool?.metadata?.kind !==
|
|
1415
|
+
if (tool?.metadata?.kind !== "command" || typeof tool.metadata.command !== "string")
|
|
1089
1416
|
return undefined;
|
|
1090
|
-
const args = typeof input ===
|
|
1091
|
-
|
|
1417
|
+
const args = typeof input === "object" &&
|
|
1418
|
+
input !== null &&
|
|
1419
|
+
Array.isArray(input.args)
|
|
1420
|
+
? input.args.filter((value) => typeof value === "string")
|
|
1092
1421
|
: [];
|
|
1093
|
-
return [tool.metadata.command, ...args].join(
|
|
1422
|
+
return [tool.metadata.command, ...args].join(" ");
|
|
1094
1423
|
}
|
|
1095
1424
|
clampBashToolInput(input, policy) {
|
|
1096
|
-
if (!policy?.maxCommandTimeoutMs || typeof input !==
|
|
1425
|
+
if (!policy?.maxCommandTimeoutMs || typeof input !== "object" || input === null)
|
|
1097
1426
|
return input;
|
|
1098
1427
|
const record = input;
|
|
1099
|
-
const timeout = typeof record.timeout ===
|
|
1428
|
+
const timeout = typeof record.timeout === "number" ? record.timeout : undefined;
|
|
1100
1429
|
return { ...record, timeout: clampCommandTimeout(timeout, policy) };
|
|
1101
1430
|
}
|
|
1102
1431
|
resolveTools(sandbox, promptTools = [], promptCommands = []) {
|
|
1103
1432
|
const tools = new Map();
|
|
1104
|
-
const commandToolOptions = this.options.resolveSecret
|
|
1433
|
+
const commandToolOptions = this.options.resolveSecret
|
|
1434
|
+
? { resolveSecret: this.options.resolveSecret }
|
|
1435
|
+
: {};
|
|
1105
1436
|
for (const tool of createBuiltinTools(sandbox, this.options.packagedSkills))
|
|
1106
1437
|
tools.set(tool.name, tool);
|
|
1107
1438
|
for (const tool of createCommandTools(this.options.commands ?? [], commandToolOptions))
|
|
@@ -1153,18 +1484,20 @@ export class StubFabricSession {
|
|
|
1153
1484
|
skill.name,
|
|
1154
1485
|
skill.relativePath,
|
|
1155
1486
|
skill.relativePath ? stripMarkdownExtension(skill.relativePath) : undefined,
|
|
1156
|
-
skill.relativePath?.endsWith(
|
|
1487
|
+
skill.relativePath?.endsWith("/SKILL.md")
|
|
1488
|
+
? skill.relativePath.slice(0, -"/SKILL.md".length)
|
|
1489
|
+
: undefined,
|
|
1157
1490
|
skill.path,
|
|
1158
1491
|
skill.path ? stripMarkdownExtension(skill.path) : undefined,
|
|
1159
|
-
skill.path?.endsWith(
|
|
1160
|
-
].filter((value) => typeof value ===
|
|
1492
|
+
skill.path?.endsWith("/SKILL.md") ? skill.path.slice(0, -"/SKILL.md".length) : undefined,
|
|
1493
|
+
].filter((value) => typeof value === "string");
|
|
1161
1494
|
return candidates.some((candidate) => normalizeSkillReference(candidate) === normalized);
|
|
1162
1495
|
});
|
|
1163
1496
|
}
|
|
1164
1497
|
resolveRole(role) {
|
|
1165
1498
|
if (!role)
|
|
1166
1499
|
return undefined;
|
|
1167
|
-
if (typeof role !==
|
|
1500
|
+
if (typeof role !== "string")
|
|
1168
1501
|
return role;
|
|
1169
1502
|
return this.options.roles?.find((candidate) => candidate.name === role);
|
|
1170
1503
|
}
|
|
@@ -1184,9 +1517,11 @@ export class StubFabricSession {
|
|
|
1184
1517
|
if (existing) {
|
|
1185
1518
|
// Cross-tenant access guard: a session that already belongs to a
|
|
1186
1519
|
// different tenant should not be silently appended to.
|
|
1187
|
-
if (this.options.tenantId !== undefined &&
|
|
1520
|
+
if (this.options.tenantId !== undefined &&
|
|
1521
|
+
existing.tenantId !== undefined &&
|
|
1522
|
+
existing.tenantId !== this.options.tenantId) {
|
|
1188
1523
|
throw new FabricError({
|
|
1189
|
-
code:
|
|
1524
|
+
code: "POLICY_DENIED",
|
|
1190
1525
|
message: `Session ${this.id} belongs to tenant ${existing.tenantId}; refusing to append from tenant ${this.options.tenantId}.`,
|
|
1191
1526
|
retryable: false,
|
|
1192
1527
|
});
|
|
@@ -1234,37 +1569,37 @@ export class StubFabricSession {
|
|
|
1234
1569
|
if (dangling.length === 0 && unfinishedTasks.length === 0)
|
|
1235
1570
|
return;
|
|
1236
1571
|
for (const call of dangling) {
|
|
1237
|
-
const tool = typeof call.data?.tool ===
|
|
1238
|
-
const toolCallId = typeof call.data?.toolCallId ===
|
|
1239
|
-
await this.recordEntry(
|
|
1572
|
+
const tool = typeof call.data?.tool === "string" ? call.data.tool : "unknown";
|
|
1573
|
+
const toolCallId = typeof call.data?.toolCallId === "string" ? call.data.toolCallId : undefined;
|
|
1574
|
+
await this.recordEntry("tool_result", {
|
|
1240
1575
|
tool,
|
|
1241
1576
|
...(toolCallId ? { toolCallId } : {}),
|
|
1242
1577
|
output: {
|
|
1243
|
-
type:
|
|
1244
|
-
message:
|
|
1578
|
+
type: "interrupted",
|
|
1579
|
+
message: "Tool execution was interrupted before completion. The outcome is unknown.",
|
|
1245
1580
|
},
|
|
1246
1581
|
isError: true,
|
|
1247
1582
|
interrupted: true,
|
|
1248
1583
|
synthetic: true,
|
|
1249
1584
|
});
|
|
1250
|
-
await this.emit(
|
|
1585
|
+
await this.emit("tool_end", { tool, error: "interrupted", synthetic: true });
|
|
1251
1586
|
}
|
|
1252
1587
|
for (const task of unfinishedTasks) {
|
|
1253
|
-
const taskId = typeof task.data?.taskId ===
|
|
1254
|
-
await this.recordEntry(
|
|
1588
|
+
const taskId = typeof task.data?.taskId === "string" ? task.data.taskId : "unknown";
|
|
1589
|
+
await this.recordEntry("task_end", {
|
|
1255
1590
|
taskId,
|
|
1256
|
-
status:
|
|
1591
|
+
status: "interrupted",
|
|
1257
1592
|
synthetic: true,
|
|
1258
1593
|
// Keep the child transcript locatable from the settled record.
|
|
1259
|
-
...(typeof task.data?.agent ===
|
|
1594
|
+
...(typeof task.data?.agent === "string" ? { agent: task.data.agent } : {}),
|
|
1260
1595
|
});
|
|
1261
1596
|
}
|
|
1262
1597
|
if (dangling.length > 0) {
|
|
1263
1598
|
const tools = dangling
|
|
1264
|
-
.map((call) => (typeof call.data?.tool ===
|
|
1265
|
-
.join(
|
|
1266
|
-
await this.recordEntry(
|
|
1267
|
-
signalType:
|
|
1599
|
+
.map((call) => (typeof call.data?.tool === "string" ? call.data.tool : "unknown"))
|
|
1600
|
+
.join(", ");
|
|
1601
|
+
await this.recordEntry("signal", {
|
|
1602
|
+
signalType: "interrupted",
|
|
1268
1603
|
content: `The previous turn was interrupted; ${dangling.length} tool call(s) did not complete and may or may not have taken effect: ${tools}. Verify their effects before repeating side-effecting operations.`,
|
|
1269
1604
|
});
|
|
1270
1605
|
}
|
|
@@ -1301,43 +1636,50 @@ export class StubFabricSession {
|
|
|
1301
1636
|
}
|
|
1302
1637
|
}
|
|
1303
1638
|
function normalizeSkillReference(value) {
|
|
1304
|
-
return stripMarkdownExtension(value
|
|
1639
|
+
return stripMarkdownExtension(value
|
|
1640
|
+
.replace(/\\/g, "/")
|
|
1641
|
+
.replace(/^\.\//, "")
|
|
1642
|
+
.replace(/^skills\//, "")
|
|
1643
|
+
.replace(/^\.fabricharness\/skills\//, "")
|
|
1644
|
+
.replace(/^\/+/, ""));
|
|
1305
1645
|
}
|
|
1306
1646
|
function stripMarkdownExtension(value) {
|
|
1307
|
-
return value.endsWith(
|
|
1647
|
+
return value.endsWith(".md") ? value.slice(0, -3) : value;
|
|
1308
1648
|
}
|
|
1309
1649
|
function affectedPathsFromInput(input) {
|
|
1310
|
-
if (typeof input !==
|
|
1650
|
+
if (typeof input !== "object" || input === null)
|
|
1311
1651
|
return [];
|
|
1312
1652
|
const record = input;
|
|
1313
|
-
const paths = [record.path, record.cwd].filter((value) => typeof value ===
|
|
1653
|
+
const paths = [record.path, record.cwd].filter((value) => typeof value === "string");
|
|
1314
1654
|
return [...new Set(paths)];
|
|
1315
1655
|
}
|
|
1316
1656
|
function commandEnvKeys(tool) {
|
|
1317
1657
|
const value = tool?.metadata?.envKeys;
|
|
1318
|
-
return Array.isArray(value)
|
|
1658
|
+
return Array.isArray(value)
|
|
1659
|
+
? value.filter((entry) => typeof entry === "string")
|
|
1660
|
+
: [];
|
|
1319
1661
|
}
|
|
1320
1662
|
function normalizeApprovalResponse(value) {
|
|
1321
|
-
if (typeof value ===
|
|
1322
|
-
return { decision: value ?
|
|
1323
|
-
if (typeof value ===
|
|
1663
|
+
if (typeof value === "boolean")
|
|
1664
|
+
return { decision: value ? "approved" : "denied" };
|
|
1665
|
+
if (typeof value === "string")
|
|
1324
1666
|
return { decision: value };
|
|
1325
1667
|
return value;
|
|
1326
1668
|
}
|
|
1327
1669
|
function readStringPropertyFromInput(input, key) {
|
|
1328
|
-
if (typeof input !==
|
|
1670
|
+
if (typeof input !== "object" || input === null || !(key in input))
|
|
1329
1671
|
return undefined;
|
|
1330
1672
|
const value = input[key];
|
|
1331
|
-
return typeof value ===
|
|
1673
|
+
return typeof value === "string" ? value : undefined;
|
|
1332
1674
|
}
|
|
1333
1675
|
function isJsonValue(value) {
|
|
1334
1676
|
if (value === null)
|
|
1335
1677
|
return true;
|
|
1336
|
-
if ([
|
|
1678
|
+
if (["string", "number", "boolean"].includes(typeof value))
|
|
1337
1679
|
return true;
|
|
1338
1680
|
if (Array.isArray(value))
|
|
1339
1681
|
return value.every(isJsonValue);
|
|
1340
|
-
if (typeof value !==
|
|
1682
|
+
if (typeof value !== "object")
|
|
1341
1683
|
return false;
|
|
1342
1684
|
return Object.values(value).every(isJsonValue);
|
|
1343
1685
|
}
|
|
@@ -1347,41 +1689,41 @@ function redactEnvValues(text, env) {
|
|
|
1347
1689
|
let result = text;
|
|
1348
1690
|
for (const [key, value] of Object.entries(env)) {
|
|
1349
1691
|
if (key.length > 0) {
|
|
1350
|
-
result = result.replaceAll(key,
|
|
1692
|
+
result = result.replaceAll(key, "[REDACTED]");
|
|
1351
1693
|
}
|
|
1352
1694
|
if (value.length > 0) {
|
|
1353
|
-
result = result.replaceAll(value,
|
|
1695
|
+
result = result.replaceAll(value, "[REDACTED]");
|
|
1354
1696
|
}
|
|
1355
1697
|
}
|
|
1356
1698
|
return result;
|
|
1357
1699
|
}
|
|
1358
1700
|
function summarizeEntries(entries) {
|
|
1359
1701
|
if (entries.length === 0)
|
|
1360
|
-
return
|
|
1702
|
+
return "No entries to compact.";
|
|
1361
1703
|
const counts = new Map();
|
|
1362
1704
|
for (const entry of entries)
|
|
1363
1705
|
counts.set(entry.type, (counts.get(entry.type) ?? 0) + 1);
|
|
1364
|
-
const summary = [...counts.entries()].map(([type, count]) => `${type}: ${count}`).join(
|
|
1706
|
+
const summary = [...counts.entries()].map(([type, count]) => `${type}: ${count}`).join(", ");
|
|
1365
1707
|
const first = entries[0];
|
|
1366
1708
|
const last = entries.at(-1);
|
|
1367
|
-
return `Compacted ${entries.length} entries (${summary}). Range: ${first?.timestamp ??
|
|
1709
|
+
return `Compacted ${entries.length} entries (${summary}). Range: ${first?.timestamp ?? "unknown"} to ${last?.timestamp ?? "unknown"}.`;
|
|
1368
1710
|
}
|
|
1369
1711
|
function collectFileReferences(entries) {
|
|
1370
1712
|
const readFiles = new Set();
|
|
1371
1713
|
const modifiedFiles = new Set();
|
|
1372
1714
|
for (const entry of entries) {
|
|
1373
|
-
if (entry.type ===
|
|
1374
|
-
const tool = typeof entry.data?.tool ===
|
|
1375
|
-
const effect = typeof entry.data?.effect ===
|
|
1715
|
+
if (entry.type === "tool_call") {
|
|
1716
|
+
const tool = typeof entry.data?.tool === "string" ? entry.data.tool : undefined;
|
|
1717
|
+
const effect = typeof entry.data?.effect === "string" ? entry.data.effect : undefined;
|
|
1376
1718
|
const input = entry.data?.input;
|
|
1377
1719
|
for (const path of filePathsFromToolInput(input)) {
|
|
1378
|
-
if (effect ===
|
|
1720
|
+
if (effect === "read" || (!effect && isReadOnlyFileTool(tool)))
|
|
1379
1721
|
readFiles.add(path);
|
|
1380
|
-
if (effect ===
|
|
1722
|
+
if (effect === "write" || (!effect && isMutatingFileTool(tool)))
|
|
1381
1723
|
modifiedFiles.add(path);
|
|
1382
1724
|
}
|
|
1383
1725
|
}
|
|
1384
|
-
if (entry.type ===
|
|
1726
|
+
if (entry.type === "shell_command" && typeof entry.data?.cwd === "string")
|
|
1385
1727
|
readFiles.add(entry.data.cwd);
|
|
1386
1728
|
}
|
|
1387
1729
|
return { readFiles: [...readFiles].sort(), modifiedFiles: [...modifiedFiles].sort() };
|
|
@@ -1389,17 +1731,17 @@ function collectFileReferences(entries) {
|
|
|
1389
1731
|
function appendFileReferenceTags(summary, refs) {
|
|
1390
1732
|
const sections = [summary];
|
|
1391
1733
|
if (refs.readFiles.length > 0)
|
|
1392
|
-
sections.push(`<read-files>\n${refs.readFiles.join(
|
|
1734
|
+
sections.push(`<read-files>\n${refs.readFiles.join("\n")}\n</read-files>`);
|
|
1393
1735
|
if (refs.modifiedFiles.length > 0)
|
|
1394
|
-
sections.push(`<modified-files>\n${refs.modifiedFiles.join(
|
|
1395
|
-
return sections.join(
|
|
1736
|
+
sections.push(`<modified-files>\n${refs.modifiedFiles.join("\n")}\n</modified-files>`);
|
|
1737
|
+
return sections.join("\n\n");
|
|
1396
1738
|
}
|
|
1397
1739
|
/** Valid cut points: user prompts and assistant messages. Never cut at tool_result. */
|
|
1398
1740
|
function findValidCutPoints(entries, start, end) {
|
|
1399
1741
|
const cutPoints = [];
|
|
1400
1742
|
for (let i = start; i < end; i++) {
|
|
1401
1743
|
const type = entries[i]?.type;
|
|
1402
|
-
if (type ===
|
|
1744
|
+
if (type === "user_prompt" || type === "assistant_message") {
|
|
1403
1745
|
cutPoints.push(i);
|
|
1404
1746
|
}
|
|
1405
1747
|
}
|
|
@@ -1407,7 +1749,7 @@ function findValidCutPoints(entries, start, end) {
|
|
|
1407
1749
|
}
|
|
1408
1750
|
function findTurnStartIndex(entries, index, start) {
|
|
1409
1751
|
for (let i = index; i >= start; i--) {
|
|
1410
|
-
if (entries[i]?.type ===
|
|
1752
|
+
if (entries[i]?.type === "user_prompt")
|
|
1411
1753
|
return i;
|
|
1412
1754
|
}
|
|
1413
1755
|
return -1;
|
|
@@ -1434,7 +1776,7 @@ function findCutPoint(entries, start, end, keepRecentTokens) {
|
|
|
1434
1776
|
break;
|
|
1435
1777
|
}
|
|
1436
1778
|
}
|
|
1437
|
-
const isUserMessage = entries[firstKeptIndex]?.type ===
|
|
1779
|
+
const isUserMessage = entries[firstKeptIndex]?.type === "user_prompt";
|
|
1438
1780
|
const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, firstKeptIndex, start);
|
|
1439
1781
|
return {
|
|
1440
1782
|
firstKeptIndex,
|
|
@@ -1466,30 +1808,49 @@ export function prepareCompaction(entries, keepRecentTokens, previousCompaction)
|
|
|
1466
1808
|
};
|
|
1467
1809
|
}
|
|
1468
1810
|
function isReadOnlyFileTool(tool) {
|
|
1469
|
-
return tool !== undefined &&
|
|
1811
|
+
return (tool !== undefined &&
|
|
1812
|
+
["read", "read_buffer", "stat", "readdir", "exists", "grep", "glob"].includes(tool));
|
|
1470
1813
|
}
|
|
1471
1814
|
function isMutatingFileTool(tool) {
|
|
1472
|
-
return tool !== undefined && [
|
|
1815
|
+
return tool !== undefined && ["write", "edit", "mkdir", "rm"].includes(tool);
|
|
1816
|
+
}
|
|
1817
|
+
function toolExecutingPrincipal(tool) {
|
|
1818
|
+
const value = tool?.metadata?.executingPrincipal;
|
|
1819
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1820
|
+
return undefined;
|
|
1821
|
+
const principal = value;
|
|
1822
|
+
if ((principal.kind !== "user" &&
|
|
1823
|
+
principal.kind !== "service-principal" &&
|
|
1824
|
+
principal.kind !== "app-principal") ||
|
|
1825
|
+
typeof principal.id !== "string")
|
|
1826
|
+
return undefined;
|
|
1827
|
+
return value;
|
|
1473
1828
|
}
|
|
1474
1829
|
function filePathsFromToolInput(input) {
|
|
1475
|
-
if (typeof input !==
|
|
1830
|
+
if (typeof input !== "object" || input === null)
|
|
1476
1831
|
return [];
|
|
1477
1832
|
const record = input;
|
|
1478
1833
|
const values = [record.path, record.cwd, record.pattern];
|
|
1479
1834
|
if (Array.isArray(record.paths))
|
|
1480
1835
|
values.push(...record.paths);
|
|
1481
|
-
return [
|
|
1836
|
+
return [
|
|
1837
|
+
...new Set(values.filter((value) => typeof value === "string" && value.length > 0)),
|
|
1838
|
+
];
|
|
1482
1839
|
}
|
|
1483
1840
|
function isSnapshot(value) {
|
|
1484
|
-
return typeof value ===
|
|
1841
|
+
return (typeof value === "object" &&
|
|
1842
|
+
value !== null &&
|
|
1843
|
+
"id" in value &&
|
|
1844
|
+
typeof value.id === "string");
|
|
1485
1845
|
}
|
|
1486
1846
|
function abortError() {
|
|
1487
|
-
const error = new Error(
|
|
1488
|
-
error.name =
|
|
1847
|
+
const error = new Error("Task cancelled");
|
|
1848
|
+
error.name = "AbortError";
|
|
1489
1849
|
return error;
|
|
1490
1850
|
}
|
|
1491
1851
|
function isAbortError(error) {
|
|
1492
|
-
return error instanceof Error &&
|
|
1852
|
+
return (error instanceof Error &&
|
|
1853
|
+
(error.name === "AbortError" || /aborted|cancelled|canceled/i.test(error.message)));
|
|
1493
1854
|
}
|
|
1494
1855
|
function wrapWithPolicy(env, policy, bypass) {
|
|
1495
1856
|
if (bypass === true)
|
|
@@ -1511,10 +1872,10 @@ function wrapWithPolicy(env, policy, bypass) {
|
|
|
1511
1872
|
*/
|
|
1512
1873
|
function resolveMountPath(cwd, mountAt) {
|
|
1513
1874
|
const normalized = path.posix.normalize(mountAt);
|
|
1514
|
-
if (!normalized.startsWith(
|
|
1875
|
+
if (!normalized.startsWith("/"))
|
|
1515
1876
|
return path.posix.join(cwd, normalized);
|
|
1516
1877
|
if (normalized === cwd || normalized.startsWith(`${cwd}/`))
|
|
1517
1878
|
return normalized;
|
|
1518
|
-
return path.posix.join(cwd, normalized.replace(/^\/+/,
|
|
1879
|
+
return path.posix.join(cwd, normalized.replace(/^\/+/, ""));
|
|
1519
1880
|
}
|
|
1520
1881
|
//# sourceMappingURL=session.js.map
|