@fabric-harness/sdk 1.12.2 → 1.14.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/dist/agent-definition.d.ts +6 -0
- package/dist/agent-definition.d.ts.map +1 -1
- package/dist/agent-definition.js +5 -4
- package/dist/agent-definition.js.map +1 -1
- package/dist/agent.d.ts +6 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +45 -1
- package/dist/agent.js.map +1 -1
- package/dist/channel.d.ts +77 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +115 -0
- package/dist/channel.js.map +1 -0
- package/dist/context-budget.d.ts +16 -0
- package/dist/context-budget.d.ts.map +1 -1
- package/dist/context-budget.js +38 -3
- package/dist/context-budget.js.map +1 -1
- package/dist/cost-budget.d.ts +106 -1
- package/dist/cost-budget.d.ts.map +1 -1
- package/dist/cost-budget.js +48 -2
- package/dist/cost-budget.js.map +1 -1
- package/dist/dispatch.d.ts +95 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +175 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/error.d.ts +1 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js.map +1 -1
- package/dist/filesystem.d.ts +52 -0
- package/dist/filesystem.d.ts.map +1 -0
- package/dist/filesystem.js +24 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/history.d.ts +22 -2
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +69 -0
- package/dist/history.js.map +1 -1
- package/dist/index.d.ts +31 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -6
- package/dist/index.js.map +1 -1
- package/dist/loop.d.ts +29 -3
- package/dist/loop.d.ts.map +1 -1
- package/dist/loop.js +167 -24
- package/dist/loop.js.map +1 -1
- package/dist/model.d.ts +47 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +250 -79
- package/dist/model.js.map +1 -1
- package/dist/otel-observer.d.ts +44 -0
- package/dist/otel-observer.d.ts.map +1 -0
- package/dist/otel-observer.js +165 -0
- package/dist/otel-observer.js.map +1 -0
- package/dist/persistence.d.ts +70 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +17 -0
- package/dist/persistence.js.map +1 -0
- package/dist/persistent-agent.d.ts +80 -0
- package/dist/persistent-agent.d.ts.map +1 -0
- package/dist/persistent-agent.js +69 -0
- package/dist/persistent-agent.js.map +1 -0
- package/dist/pi-loop-runtime.d.ts.map +1 -1
- package/dist/pi-loop-runtime.js +9 -3
- package/dist/pi-loop-runtime.js.map +1 -1
- package/dist/profile.d.ts +33 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +10 -0
- package/dist/profile.js.map +1 -0
- package/dist/providers.d.ts +14 -0
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +18 -1
- package/dist/providers.js.map +1 -1
- package/dist/result.d.ts +12 -0
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +26 -0
- package/dist/result.js.map +1 -1
- package/dist/retrieval.d.ts +61 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +26 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-mode.d.ts +1 -1
- package/dist/runtime-mode.d.ts.map +1 -1
- package/dist/runtime-mode.js +2 -1
- package/dist/runtime-mode.js.map +1 -1
- package/dist/scope-key.d.ts +25 -0
- package/dist/scope-key.d.ts.map +1 -0
- package/dist/scope-key.js +34 -0
- package/dist/scope-key.js.map +1 -0
- package/dist/session.d.ts +40 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +308 -34
- package/dist/session.js.map +1 -1
- package/dist/skills.d.ts +33 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +65 -0
- package/dist/skills.js.map +1 -0
- package/dist/store.d.ts +36 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +83 -0
- package/dist/store.js.map +1 -1
- package/dist/stream-chunks.d.ts +42 -0
- package/dist/stream-chunks.d.ts.map +1 -0
- package/dist/stream-chunks.js +138 -0
- package/dist/stream-chunks.js.map +1 -0
- package/dist/strict.d.ts +21 -11
- package/dist/strict.d.ts.map +1 -1
- package/dist/strict.js +11 -6
- package/dist/strict.js.map +1 -1
- package/dist/tools.d.ts +32 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +132 -6
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +110 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +14 -10
package/dist/session.js
CHANGED
|
@@ -8,15 +8,69 @@ import { defaultLoopRuntime } from './loop.js';
|
|
|
8
8
|
import { defaultModelProvider, generateWithRuntime } from './model.js';
|
|
9
9
|
import { clampCommandTimeout, evaluateCommandPolicy, evaluateToolCallPolicy } from './policy.js';
|
|
10
10
|
import { buildResultRetryPrompt, validateResult } from './result.js';
|
|
11
|
-
import { evaluateContextBudget, isContextOverflowError } from './context-budget.js';
|
|
11
|
+
import { estimateSessionEntryTokens, evaluateContextBudget, isContextOverflowError } from './context-budget.js';
|
|
12
12
|
import { policiedSandboxEnv } from './policied-sandbox.js';
|
|
13
13
|
import { createSandboxEnv, createScopedSandboxEnv } from './sandbox.js';
|
|
14
|
+
import { createFabricFs } from './filesystem.js';
|
|
14
15
|
import { registerSandbox, serializeSandboxRef, unregisterSandbox } from './sandbox-ref.js';
|
|
15
16
|
import path from 'node:path';
|
|
16
17
|
import { defaultSessionStore } from './store.js';
|
|
17
|
-
import { createBuiltinTools, createCommandTools, shellQuote } from './tools.js';
|
|
18
|
+
import { createActivateSkillTool, createBuiltinTools, createCommandTools, shellQuote } from './tools.js';
|
|
19
|
+
import { formatPackagedSkillResourceList, readPackagedSkillFile } from './skills.js';
|
|
18
20
|
import { MAX_TASK_DEPTH } from './types.js';
|
|
19
21
|
let nextTaskId = 0;
|
|
22
|
+
const CROCKFORD_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
23
|
+
const AFFINITY_KEY_REGEX = /^aff_[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
|
|
24
|
+
function encodeBase32(buf, length) {
|
|
25
|
+
let result = '';
|
|
26
|
+
let bitIndex = 0; // 0 = MSB of buf[0]
|
|
27
|
+
for (let i = 0; i < length; i++) {
|
|
28
|
+
let value = 0;
|
|
29
|
+
for (let j = 0; j < 5; j++) {
|
|
30
|
+
const byteIndex = Math.floor(bitIndex / 8);
|
|
31
|
+
const bitInByte = 7 - (bitIndex % 8);
|
|
32
|
+
const byte = buf[byteIndex] ?? 0;
|
|
33
|
+
const bit = (byte >> bitInByte) & 1;
|
|
34
|
+
value = (value << 1) | bit;
|
|
35
|
+
bitIndex++;
|
|
36
|
+
}
|
|
37
|
+
result += CROCKFORD_ALPHABET[value];
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generate a deterministic `aff_\u003cULID\u003e` affinity key from an
|
|
43
|
+
* `(agentId, sessionId)` pair. The same pair always produces the same key,
|
|
44
|
+
* which is stable across restarts. Different pairs produce different keys
|
|
45
|
+
* with overwhelming probability.
|
|
46
|
+
*/
|
|
47
|
+
export function generateAffinityKey(agentId, sessionId) {
|
|
48
|
+
const hash = createHash('sha256').update(agentId).update('\0').update(sessionId).digest();
|
|
49
|
+
// Build a 128-bit buffer from the hash, clearing the top 5 bits of the
|
|
50
|
+
// first byte so the first Crockford base32 char is in [0-7].
|
|
51
|
+
const combined = Buffer.alloc(16);
|
|
52
|
+
combined[0] = (hash[0] ?? 0) & 0x07;
|
|
53
|
+
combined.set(hash.slice(1, 16), 1);
|
|
54
|
+
return `aff_${encodeBase32(combined, 26)}`;
|
|
55
|
+
}
|
|
56
|
+
export function isValidAffinityKey(key) {
|
|
57
|
+
return AFFINITY_KEY_REGEX.test(key);
|
|
58
|
+
}
|
|
59
|
+
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.';
|
|
60
|
+
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
|
|
61
|
+
|
|
62
|
+
Summarize the prefix to provide context for the retained suffix:
|
|
63
|
+
|
|
64
|
+
## Original Request
|
|
65
|
+
[What did the user ask for in this turn?]
|
|
66
|
+
|
|
67
|
+
## Early Progress
|
|
68
|
+
- [Key decisions and work done in the prefix]
|
|
69
|
+
|
|
70
|
+
## Context for Suffix
|
|
71
|
+
- [Information needed to understand the retained recent work]
|
|
72
|
+
|
|
73
|
+
Be concise. Focus on what's needed to understand the kept suffix.`;
|
|
20
74
|
export class StubFabricSession {
|
|
21
75
|
id;
|
|
22
76
|
agent;
|
|
@@ -25,6 +79,7 @@ export class StubFabricSession {
|
|
|
25
79
|
create: (options) => this.createCheckpoint(options),
|
|
26
80
|
restore: (options) => this.restoreCheckpoint(options),
|
|
27
81
|
};
|
|
82
|
+
fs;
|
|
28
83
|
approval = {
|
|
29
84
|
request: (input) => this.requestCustomApproval(input),
|
|
30
85
|
};
|
|
@@ -47,12 +102,14 @@ export class StubFabricSession {
|
|
|
47
102
|
/** Set when this session has registered its sandbox in the in-process ref registry. */
|
|
48
103
|
ownedSandboxRefId;
|
|
49
104
|
costBudget;
|
|
105
|
+
skillCache = new Map();
|
|
50
106
|
constructor(agent, id, options = {}) {
|
|
51
107
|
this.agent = agent;
|
|
52
108
|
this.id = id;
|
|
53
109
|
this.options = options;
|
|
54
110
|
this.store = options.store ?? defaultSessionStore;
|
|
55
111
|
this.sandbox = this.createSandbox();
|
|
112
|
+
this.fs = createFabricFs(this.sandbox);
|
|
56
113
|
this.costBudget = new CostBudgetTracker(options.costLimit);
|
|
57
114
|
}
|
|
58
115
|
getDurableRuntime() {
|
|
@@ -92,7 +149,8 @@ export class StubFabricSession {
|
|
|
92
149
|
const sandbox = createScopedSandboxEnv(await this.sandbox, options.cwd);
|
|
93
150
|
const tools = this.resolveTools(sandbox, options.tools, options.commands);
|
|
94
151
|
await this.recordEntry('user_prompt', { text, ...(role ? { role: role.name } : {}) });
|
|
95
|
-
|
|
152
|
+
const sessionData = await this.history();
|
|
153
|
+
let messages = buildModelMessagesFromHistory(sessionData, role);
|
|
96
154
|
if (!messages.some((message) => message.role === 'user' && message.content === text))
|
|
97
155
|
messages.push({ role: 'user', content: text });
|
|
98
156
|
messages = await this.autoCompactIfNeeded(messages, role, options);
|
|
@@ -102,14 +160,19 @@ export class StubFabricSession {
|
|
|
102
160
|
await this.emit('prompt_end', { text, result: results });
|
|
103
161
|
return validateResult(results, options.result, options.resultExtraction);
|
|
104
162
|
}
|
|
163
|
+
// Use tool-based result extraction when a validator is provided and the caller
|
|
164
|
+
// has not explicitly opted into delimiter-based extraction.
|
|
165
|
+
const useResultTools = options.result !== undefined && options.resultExtraction === undefined;
|
|
105
166
|
const runtime = options.loopRuntime ?? this.options.loopRuntime ?? defaultLoopRuntime;
|
|
106
167
|
const runRuntime = () => runtime.runPrompt({
|
|
107
168
|
sessionId: this.id,
|
|
169
|
+
...(sessionData.affinityKey !== undefined ? { affinityKey: sessionData.affinityKey } : {}),
|
|
108
170
|
text,
|
|
109
171
|
messages,
|
|
110
172
|
tools,
|
|
111
173
|
sandbox,
|
|
112
174
|
...(typeof (options.model ?? this.options.model ?? this.agent.model) === 'string' ? { model: (options.model ?? this.options.model ?? this.agent.model) } : {}),
|
|
175
|
+
...((options.thinkingLevel ?? this.options.thinkingLevel ?? this.agent.thinkingLevel) !== undefined ? { thinkingLevel: options.thinkingLevel ?? this.options.thinkingLevel ?? this.agent.thinkingLevel } : {}),
|
|
113
176
|
...((options.modelProvider ?? this.options.modelProvider) !== undefined ? { modelProvider: options.modelProvider ?? this.options.modelProvider } : {}),
|
|
114
177
|
...(options.maxIterations !== undefined ? { maxIterations: options.maxIterations } : {}),
|
|
115
178
|
...(options.modelTimeoutMs !== undefined ? { modelTimeoutMs: options.modelTimeoutMs } : {}),
|
|
@@ -122,7 +185,10 @@ export class StubFabricSession {
|
|
|
122
185
|
costBudget: this.costBudget,
|
|
123
186
|
requestCostLimitApproval: (ctx) => this.requestCostLimitApproval(ctx, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval),
|
|
124
187
|
} : {}),
|
|
125
|
-
|
|
188
|
+
...(useResultTools ? { resultValidator: options.result } : {}),
|
|
189
|
+
agentId: this.agent.id,
|
|
190
|
+
...(this.options.tenantId !== undefined ? { tenantId: this.options.tenantId } : {}),
|
|
191
|
+
executeToolCalls: (calls, policy, approvalTimeoutMs, attribution) => this.executeToolCalls(calls, tools, sandbox, policy, approvalTimeoutMs, options.onApproval, attribution),
|
|
126
192
|
recordEntry: (type, data) => this.recordEntry(type, data),
|
|
127
193
|
emit: (type, data) => this.emit(type, data),
|
|
128
194
|
});
|
|
@@ -144,6 +210,10 @@ export class StubFabricSession {
|
|
|
144
210
|
messages = buildModelMessagesFromHistory(await this.history(), role);
|
|
145
211
|
result = await runRuntime();
|
|
146
212
|
}
|
|
213
|
+
// If the loop produced a validated result via finish/give_up tools, return it directly.
|
|
214
|
+
if (result.validatedResult !== undefined) {
|
|
215
|
+
return result.validatedResult;
|
|
216
|
+
}
|
|
147
217
|
const resultRetries = options.resultRetries ?? (options.result ? 1 : 0);
|
|
148
218
|
for (let attempt = 0; attempt <= resultRetries; attempt += 1) {
|
|
149
219
|
try {
|
|
@@ -215,7 +285,8 @@ export class StubFabricSession {
|
|
|
215
285
|
if (!skill)
|
|
216
286
|
throw new FabricError({ code: 'SKILL_NOT_FOUND', message: `Skill not found: ${name}`, details: { skill: name } });
|
|
217
287
|
const argsText = options.args ? `\n\nArguments:\n${JSON.stringify(options.args, null, 2)}` : '';
|
|
218
|
-
const
|
|
288
|
+
const packagedText = skill.packaged ? formatPackagedSkillResourceList(skill.packaged) : '';
|
|
289
|
+
const result = await this.prompt(`${skill.content ?? ''}${argsText}${packagedText}`, options);
|
|
219
290
|
await this.recordEntry('skill_end', { skill: name });
|
|
220
291
|
await this.emit('skill_end', { skill: name });
|
|
221
292
|
return result;
|
|
@@ -377,10 +448,22 @@ export class StubFabricSession {
|
|
|
377
448
|
const timeout = clampCommandTimeout(options.timeout, policy);
|
|
378
449
|
if (timeout !== undefined)
|
|
379
450
|
execOptions.timeout = timeout;
|
|
380
|
-
await this.recordEntry('shell_command', { command });
|
|
381
451
|
await this.emit('command_start', { command });
|
|
382
452
|
const result = await sandbox.exec(command, execOptions);
|
|
383
|
-
|
|
453
|
+
const redactedCommand = redactEnvValues(command, options.env);
|
|
454
|
+
const toolCallId = `shell-${Date.now()}`;
|
|
455
|
+
await this.recordEntry('user_prompt', { text: `Run shell command: ${redactedCommand}` });
|
|
456
|
+
await this.recordEntry('tool_call', { tool: 'bash', input: { command: redactedCommand }, toolCallId, synthetic: true });
|
|
457
|
+
await this.recordEntry('tool_result', {
|
|
458
|
+
tool: 'bash',
|
|
459
|
+
output: {
|
|
460
|
+
stdout: redactEnvValues(result.stdout, options.env),
|
|
461
|
+
stderr: redactEnvValues(result.stderr, options.env),
|
|
462
|
+
exitCode: result.exitCode,
|
|
463
|
+
},
|
|
464
|
+
toolCallId,
|
|
465
|
+
synthetic: true,
|
|
466
|
+
});
|
|
384
467
|
await this.emit('command_end', { command, exitCode: result.exitCode, ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}) });
|
|
385
468
|
return result;
|
|
386
469
|
}
|
|
@@ -436,6 +519,9 @@ export class StubFabricSession {
|
|
|
436
519
|
async history() {
|
|
437
520
|
return this.ensureData();
|
|
438
521
|
}
|
|
522
|
+
async createSignalEntry(data) {
|
|
523
|
+
return this.recordEntry('signal', data);
|
|
524
|
+
}
|
|
439
525
|
async sandboxRef(options) {
|
|
440
526
|
const sandbox = await this.sandbox;
|
|
441
527
|
const ref = registerSandbox(sandbox, { ownerSessionId: this.id });
|
|
@@ -518,13 +604,39 @@ export class StubFabricSession {
|
|
|
518
604
|
async compact(options = {}) {
|
|
519
605
|
const history = await this.history();
|
|
520
606
|
const entries = history.entries ?? [];
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
607
|
+
const settings = this.options.compaction;
|
|
608
|
+
const explicitKeepRecentEntries = options.keepRecentEntries ?? settings?.keepRecentEntries;
|
|
609
|
+
const explicitKeepRecentTokens = options.keepRecentTokens ?? settings?.keepRecentTokens;
|
|
610
|
+
let compacted = [];
|
|
611
|
+
let firstKeptEntry;
|
|
612
|
+
let prep;
|
|
613
|
+
if (explicitKeepRecentEntries !== undefined) {
|
|
614
|
+
// Backward-compatible count-based cut.
|
|
615
|
+
const compactedCount = Math.max(0, entries.length - explicitKeepRecentEntries);
|
|
616
|
+
firstKeptEntry = compactedCount > 0 ? entries[compactedCount] : undefined;
|
|
617
|
+
compacted = compactedCount > 0 ? entries.slice(0, compactedCount) : [];
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
const effectiveKeepTokens = explicitKeepRecentTokens ?? 8_000;
|
|
621
|
+
prep = prepareCompaction(entries, effectiveKeepTokens);
|
|
622
|
+
if (prep && prep.firstKeptIndex > 0) {
|
|
623
|
+
compacted = [...prep.messagesToSummarize, ...prep.turnPrefixMessages];
|
|
624
|
+
firstKeptEntry = entries[prep.firstKeptIndex];
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const fileRefs = prep ? prep.fileRefs : collectFileReferences(compacted);
|
|
628
|
+
const summaryInput = prep ?? {
|
|
629
|
+
firstKeptIndex: firstKeptEntry ? entries.indexOf(firstKeptEntry) : 0,
|
|
630
|
+
messagesToSummarize: compacted,
|
|
631
|
+
turnPrefixMessages: [],
|
|
632
|
+
isSplitTurn: false,
|
|
633
|
+
fileRefs,
|
|
634
|
+
};
|
|
635
|
+
const baseSummary = options.summary ?? (options.generateSummary
|
|
636
|
+
? await this.generateCompactionSummary(summaryInput)
|
|
637
|
+
: summarizeEntries(compacted));
|
|
527
638
|
const summary = appendFileReferenceTags(baseSummary, fileRefs);
|
|
639
|
+
const compactedEntries = compacted.length;
|
|
528
640
|
const entry = await this.recordEntry('compaction', {
|
|
529
641
|
summary,
|
|
530
642
|
compactedEntries,
|
|
@@ -546,6 +658,7 @@ export class StubFabricSession {
|
|
|
546
658
|
summary,
|
|
547
659
|
compactedEntries,
|
|
548
660
|
...(firstKeptEntry ? { firstKeptEntryId: firstKeptEntry.id } : {}),
|
|
661
|
+
details: { readFiles: fileRefs.readFiles, modifiedFiles: fileRefs.modifiedFiles },
|
|
549
662
|
};
|
|
550
663
|
}
|
|
551
664
|
async autoCompactIfNeeded(messages, role, options) {
|
|
@@ -556,37 +669,69 @@ export class StubFabricSession {
|
|
|
556
669
|
return messages;
|
|
557
670
|
const compactAtTokens = options.compactAtTokens ?? settings?.compactAtTokens;
|
|
558
671
|
const reserveTokens = options.reserveTokens ?? settings?.reserveTokens;
|
|
559
|
-
const keepRecentEntries = options.compactionKeepRecentEntries ?? settings?.keepRecentEntries
|
|
672
|
+
const keepRecentEntries = options.compactionKeepRecentEntries ?? settings?.keepRecentEntries;
|
|
673
|
+
const contextWindowTokens = options.contextWindowTokens ?? this.resolveModelContextWindow(options);
|
|
560
674
|
const budget = evaluateContextBudget(messages, {
|
|
561
|
-
...(
|
|
675
|
+
...(contextWindowTokens !== undefined ? { contextWindowTokens } : {}),
|
|
562
676
|
...(compactAtTokens !== undefined ? { compactAtTokens } : {}),
|
|
563
677
|
...(reserveTokens !== undefined ? { reserveTokens } : {}),
|
|
678
|
+
...(settings?.keepRecentTokens !== undefined ? { keepRecentTokens: settings.keepRecentTokens } : {}),
|
|
564
679
|
});
|
|
565
680
|
if (!budget.shouldCompact)
|
|
566
681
|
return messages;
|
|
567
682
|
const messagesBefore = messages.length;
|
|
568
683
|
const tokensBefore = budget.estimatedTokens;
|
|
569
|
-
|
|
684
|
+
const compactOpts = { generateSummary: true, reason: 'threshold', tokensBefore, messagesBefore };
|
|
685
|
+
if (keepRecentEntries !== undefined) {
|
|
686
|
+
compactOpts.keepRecentEntries = keepRecentEntries;
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
compactOpts.keepRecentTokens = settings?.keepRecentTokens ?? budget.keepRecentTokens ?? 8_000;
|
|
690
|
+
}
|
|
691
|
+
await this.compact(compactOpts);
|
|
570
692
|
const compacted = buildModelMessagesFromHistory(await this.history(), role);
|
|
571
693
|
return compacted;
|
|
572
694
|
}
|
|
573
|
-
|
|
695
|
+
resolveModelContextWindow(options) {
|
|
696
|
+
const provider = options.modelProvider ?? this.options.modelProvider;
|
|
697
|
+
if (!provider)
|
|
698
|
+
return undefined;
|
|
699
|
+
const model = typeof (options.model ?? this.options.model ?? this.agent.model) === 'string'
|
|
700
|
+
? (options.model ?? this.options.model ?? this.agent.model)
|
|
701
|
+
: undefined;
|
|
702
|
+
return this.readProviderModelMetadata(provider, model)?.contextWindowTokens;
|
|
703
|
+
}
|
|
704
|
+
readProviderModelMetadata(provider, model) {
|
|
705
|
+
try {
|
|
706
|
+
return provider.getModelMetadata?.(model);
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async generateCompactionSummary(prep) {
|
|
713
|
+
if (prep.isSplitTurn && prep.turnPrefixMessages.length > 0) {
|
|
714
|
+
const [historyResult, prefixResult] = await Promise.all([
|
|
715
|
+
this.summarizeEntrySet(prep.messagesToSummarize),
|
|
716
|
+
this.summarizeEntrySet(prep.turnPrefixMessages, TURN_PREFIX_SUMMARIZATION_PROMPT),
|
|
717
|
+
]);
|
|
718
|
+
return `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${prefixResult}`;
|
|
719
|
+
}
|
|
720
|
+
return this.summarizeEntrySet(prep.messagesToSummarize);
|
|
721
|
+
}
|
|
722
|
+
async summarizeEntrySet(entries, prefixPrompt) {
|
|
574
723
|
if (entries.length === 0)
|
|
575
724
|
return summarizeEntries(entries);
|
|
576
725
|
const deterministic = summarizeEntries(entries);
|
|
577
726
|
const provider = this.options.modelProvider ?? defaultModelProvider;
|
|
578
727
|
try {
|
|
728
|
+
const serialized = JSON.stringify(entries.map((entry) => ({ type: entry.type, timestamp: entry.timestamp, data: entry.data ?? {} })).slice(0, 200));
|
|
729
|
+
const userContent = prefixPrompt ? `${serialized}\n\n${prefixPrompt}` : serialized;
|
|
579
730
|
const response = await generateWithRuntime(provider, {
|
|
580
731
|
...(this.agent.model !== undefined ? { model: this.agent.model } : {}),
|
|
581
732
|
messages: [
|
|
582
|
-
{
|
|
583
|
-
|
|
584
|
-
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.',
|
|
585
|
-
},
|
|
586
|
-
{
|
|
587
|
-
role: 'user',
|
|
588
|
-
content: JSON.stringify(entries.map((entry) => ({ type: entry.type, timestamp: entry.timestamp, data: entry.data ?? {} })).slice(0, 200)),
|
|
589
|
-
},
|
|
733
|
+
{ role: 'system', content: COMPACTION_SUMMARIZATION_SYSTEM_PROMPT },
|
|
734
|
+
{ role: 'user', content: userContent },
|
|
590
735
|
],
|
|
591
736
|
}, { retries: 1, retryDelayMs: 250 });
|
|
592
737
|
return response.message?.content?.trim() || deterministic;
|
|
@@ -688,7 +833,7 @@ export class StubFabricSession {
|
|
|
688
833
|
const scoped = createScopedSandboxEnv(env, this.options.cwd);
|
|
689
834
|
return wrapWithPolicy(scoped, this.options.policy, this.options.bypassPolicyEnforcement);
|
|
690
835
|
}
|
|
691
|
-
async executeToolCalls(calls, tools, sandbox, callPolicy, approvalTimeoutMs, approvalHandler) {
|
|
836
|
+
async executeToolCalls(calls, tools, sandbox, callPolicy, approvalTimeoutMs, approvalHandler, attribution) {
|
|
692
837
|
const results = [];
|
|
693
838
|
const policy = callPolicy ?? this.options.policy;
|
|
694
839
|
for (const call of calls) {
|
|
@@ -760,13 +905,13 @@ export class StubFabricSession {
|
|
|
760
905
|
continue;
|
|
761
906
|
}
|
|
762
907
|
}
|
|
763
|
-
await this.recordEntry('tool_call', { tool: call.tool, input: call.input, ...(tool?.metadata?.effect ? { effect: tool.metadata.effect } : {}), ...(call.id ? { toolCallId: call.id } : {}) });
|
|
764
|
-
await this.emit('tool_start', { tool: call.tool });
|
|
908
|
+
await this.recordEntry('tool_call', { tool: call.tool, input: call.input, ...(tool?.metadata?.effect ? { effect: tool.metadata.effect } : {}), ...(call.id ? { toolCallId: call.id } : {}), ...(attribution ? { attribution: attribution } : {}) });
|
|
909
|
+
await this.emit('tool_start', { tool: call.tool, ...(attribution ? { attribution: attribution } : {}) });
|
|
765
910
|
if (!tool?.execute) {
|
|
766
911
|
const error = `Tool is not available: ${call.tool}`;
|
|
767
912
|
results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
|
|
768
913
|
await this.recordEntry('error', { tool: call.tool, error, approvalRequired: false });
|
|
769
|
-
await this.emit('tool_end', { tool: call.tool, error });
|
|
914
|
+
await this.emit('tool_end', { tool: call.tool, error, ...(attribution ? { attribution: attribution } : {}) });
|
|
770
915
|
continue;
|
|
771
916
|
}
|
|
772
917
|
try {
|
|
@@ -775,14 +920,14 @@ export class StubFabricSession {
|
|
|
775
920
|
const output = await tool.execute(input, { sandbox });
|
|
776
921
|
const durationMs = Date.now() - startedAt;
|
|
777
922
|
results.push({ tool: call.tool, input, output, ...(call.id ? { id: call.id } : {}) });
|
|
778
|
-
await this.recordEntry('tool_result', { tool: call.tool, output: output, durationMs, ...(call.id ? { toolCallId: call.id } : {}) });
|
|
779
|
-
await this.emit('tool_end', { tool: call.tool, durationMs });
|
|
923
|
+
await this.recordEntry('tool_result', { tool: call.tool, output: output, durationMs, ...(call.id ? { toolCallId: call.id } : {}), ...(attribution ? { attribution: attribution } : {}) });
|
|
924
|
+
await this.emit('tool_end', { tool: call.tool, durationMs, ...(attribution ? { attribution: attribution } : {}) });
|
|
780
925
|
}
|
|
781
926
|
catch (error) {
|
|
782
927
|
const message = error instanceof Error ? error.message : String(error);
|
|
783
928
|
results.push({ tool: call.tool, input: call.input, error: message, ...(call.id ? { id: call.id } : {}) });
|
|
784
929
|
await this.recordEntry('error', { tool: call.tool, error: message });
|
|
785
|
-
await this.emit('tool_end', { tool: call.tool, error: message });
|
|
930
|
+
await this.emit('tool_end', { tool: call.tool, error: message, ...(attribution ? { attribution: attribution } : {}) });
|
|
786
931
|
}
|
|
787
932
|
}
|
|
788
933
|
return results;
|
|
@@ -908,7 +1053,7 @@ export class StubFabricSession {
|
|
|
908
1053
|
resolveTools(sandbox, promptTools = [], promptCommands = []) {
|
|
909
1054
|
const tools = new Map();
|
|
910
1055
|
const commandToolOptions = this.options.resolveSecret ? { resolveSecret: this.options.resolveSecret } : {};
|
|
911
|
-
for (const tool of createBuiltinTools(sandbox))
|
|
1056
|
+
for (const tool of createBuiltinTools(sandbox, this.options.packagedSkills))
|
|
912
1057
|
tools.set(tool.name, tool);
|
|
913
1058
|
for (const tool of createCommandTools(this.options.commands ?? [], commandToolOptions))
|
|
914
1059
|
tools.set(tool.name, tool);
|
|
@@ -918,8 +1063,40 @@ export class StubFabricSession {
|
|
|
918
1063
|
tools.set(tool.name, tool);
|
|
919
1064
|
for (const tool of promptTools)
|
|
920
1065
|
tools.set(tool.name, tool);
|
|
1066
|
+
const skills = this.options.skills ?? [];
|
|
1067
|
+
if (skills.length > 0) {
|
|
1068
|
+
const skillNames = skills.map((skill) => skill.name);
|
|
1069
|
+
const activateSkillTool = createActivateSkillTool(skillNames, (name) => this.activateSkillForTool(name));
|
|
1070
|
+
tools.set(activateSkillTool.name, activateSkillTool);
|
|
1071
|
+
}
|
|
921
1072
|
return tools;
|
|
922
1073
|
}
|
|
1074
|
+
async activateSkillForTool(name) {
|
|
1075
|
+
const cached = this.skillCache.get(name);
|
|
1076
|
+
if (cached !== undefined)
|
|
1077
|
+
return cached;
|
|
1078
|
+
const skill = this.resolveSkill(name);
|
|
1079
|
+
if (!skill)
|
|
1080
|
+
return `Error: Skill "${name}" not found.`;
|
|
1081
|
+
let content = skill.content;
|
|
1082
|
+
if (content === undefined && skill.path) {
|
|
1083
|
+
try {
|
|
1084
|
+
const sandbox = await this.sandbox;
|
|
1085
|
+
content = await sandbox.readFile(skill.path);
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
return `Error: Skill "${name}" found but could not read content from path "${skill.path}".`;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (content === undefined) {
|
|
1092
|
+
return `Error: Skill "${name}" found but has no content or readable path.`;
|
|
1093
|
+
}
|
|
1094
|
+
if (skill.packaged) {
|
|
1095
|
+
content += formatPackagedSkillResourceList(skill.packaged);
|
|
1096
|
+
}
|
|
1097
|
+
this.skillCache.set(name, content);
|
|
1098
|
+
return content;
|
|
1099
|
+
}
|
|
923
1100
|
resolveSkill(name) {
|
|
924
1101
|
const normalized = normalizeSkillReference(name);
|
|
925
1102
|
return this.options.skills?.find((skill) => {
|
|
@@ -965,10 +1142,22 @@ export class StubFabricSession {
|
|
|
965
1142
|
retryable: false,
|
|
966
1143
|
});
|
|
967
1144
|
}
|
|
1145
|
+
if (!existing.affinityKey) {
|
|
1146
|
+
existing.affinityKey = generateAffinityKey(this.agent.id, this.id);
|
|
1147
|
+
await this.store.save(existing);
|
|
1148
|
+
}
|
|
968
1149
|
return existing;
|
|
969
1150
|
}
|
|
970
1151
|
const now = new Date().toISOString();
|
|
971
|
-
const data = {
|
|
1152
|
+
const data = {
|
|
1153
|
+
id: this.id,
|
|
1154
|
+
agentId: this.agent.id,
|
|
1155
|
+
createdAt: now,
|
|
1156
|
+
updatedAt: now,
|
|
1157
|
+
events: [],
|
|
1158
|
+
entries: [],
|
|
1159
|
+
affinityKey: generateAffinityKey(this.agent.id, this.id),
|
|
1160
|
+
};
|
|
972
1161
|
if (this.options.metadata !== undefined)
|
|
973
1162
|
data.metadata = this.options.metadata;
|
|
974
1163
|
if (this.options.tenantId !== undefined)
|
|
@@ -1048,6 +1237,20 @@ function isJsonValue(value) {
|
|
|
1048
1237
|
return false;
|
|
1049
1238
|
return Object.values(value).every(isJsonValue);
|
|
1050
1239
|
}
|
|
1240
|
+
function redactEnvValues(text, env) {
|
|
1241
|
+
if (!env)
|
|
1242
|
+
return text;
|
|
1243
|
+
let result = text;
|
|
1244
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1245
|
+
if (key.length > 0) {
|
|
1246
|
+
result = result.replaceAll(key, '[REDACTED]');
|
|
1247
|
+
}
|
|
1248
|
+
if (value.length > 0) {
|
|
1249
|
+
result = result.replaceAll(value, '[REDACTED]');
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return result;
|
|
1253
|
+
}
|
|
1051
1254
|
function summarizeEntries(entries) {
|
|
1052
1255
|
if (entries.length === 0)
|
|
1053
1256
|
return 'No entries to compact.';
|
|
@@ -1087,6 +1290,77 @@ function appendFileReferenceTags(summary, refs) {
|
|
|
1087
1290
|
sections.push(`<modified-files>\n${refs.modifiedFiles.join('\n')}\n</modified-files>`);
|
|
1088
1291
|
return sections.join('\n\n');
|
|
1089
1292
|
}
|
|
1293
|
+
/** Valid cut points: user prompts and assistant messages. Never cut at tool_result. */
|
|
1294
|
+
function findValidCutPoints(entries, start, end) {
|
|
1295
|
+
const cutPoints = [];
|
|
1296
|
+
for (let i = start; i < end; i++) {
|
|
1297
|
+
const type = entries[i]?.type;
|
|
1298
|
+
if (type === 'user_prompt' || type === 'assistant_message') {
|
|
1299
|
+
cutPoints.push(i);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return cutPoints;
|
|
1303
|
+
}
|
|
1304
|
+
function findTurnStartIndex(entries, index, start) {
|
|
1305
|
+
for (let i = index; i >= start; i--) {
|
|
1306
|
+
if (entries[i]?.type === 'user_prompt')
|
|
1307
|
+
return i;
|
|
1308
|
+
}
|
|
1309
|
+
return -1;
|
|
1310
|
+
}
|
|
1311
|
+
function findCutPoint(entries, start, end, keepRecentTokens) {
|
|
1312
|
+
const cutPoints = findValidCutPoints(entries, start, end);
|
|
1313
|
+
if (cutPoints.length === 0) {
|
|
1314
|
+
return { firstKeptIndex: start, turnStartIndex: -1, isSplitTurn: false };
|
|
1315
|
+
}
|
|
1316
|
+
let accumulatedTokens = 0;
|
|
1317
|
+
let firstKeptIndex = start;
|
|
1318
|
+
for (let i = end - 1; i >= start; i--) {
|
|
1319
|
+
const entry = entries[i];
|
|
1320
|
+
if (!entry)
|
|
1321
|
+
continue;
|
|
1322
|
+
accumulatedTokens += estimateSessionEntryTokens(entry);
|
|
1323
|
+
if (accumulatedTokens >= keepRecentTokens) {
|
|
1324
|
+
for (const cutPoint of cutPoints) {
|
|
1325
|
+
if (cutPoint >= i) {
|
|
1326
|
+
firstKeptIndex = cutPoint;
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const isUserMessage = entries[firstKeptIndex]?.type === 'user_prompt';
|
|
1334
|
+
const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, firstKeptIndex, start);
|
|
1335
|
+
return {
|
|
1336
|
+
firstKeptIndex,
|
|
1337
|
+
turnStartIndex,
|
|
1338
|
+
isSplitTurn: !isUserMessage && turnStartIndex !== -1,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
/** Pure function — no I/O. Finds the token-based cut point, extracts messages to summarize, and tracks file ops. */
|
|
1342
|
+
export function prepareCompaction(entries, keepRecentTokens, previousCompaction) {
|
|
1343
|
+
if (entries.length === 0)
|
|
1344
|
+
return undefined;
|
|
1345
|
+
const boundaryStart = previousCompaction ? previousCompaction.firstKeptIndex : 0;
|
|
1346
|
+
const boundaryEnd = entries.length;
|
|
1347
|
+
const cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, keepRecentTokens);
|
|
1348
|
+
if (cutPoint.firstKeptIndex <= boundaryStart)
|
|
1349
|
+
return undefined;
|
|
1350
|
+
const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptIndex;
|
|
1351
|
+
const messagesToSummarize = entries.slice(boundaryStart, historyEnd);
|
|
1352
|
+
const turnPrefixMessages = cutPoint.isSplitTurn
|
|
1353
|
+
? entries.slice(cutPoint.turnStartIndex, cutPoint.firstKeptIndex)
|
|
1354
|
+
: [];
|
|
1355
|
+
const fileRefs = collectFileReferences([...messagesToSummarize, ...turnPrefixMessages]);
|
|
1356
|
+
return {
|
|
1357
|
+
firstKeptIndex: cutPoint.firstKeptIndex,
|
|
1358
|
+
messagesToSummarize,
|
|
1359
|
+
turnPrefixMessages,
|
|
1360
|
+
isSplitTurn: cutPoint.isSplitTurn,
|
|
1361
|
+
fileRefs,
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1090
1364
|
function isReadOnlyFileTool(tool) {
|
|
1091
1365
|
return tool !== undefined && ['read', 'read_buffer', 'stat', 'readdir', 'exists', 'grep', 'glob'].includes(tool);
|
|
1092
1366
|
}
|