@fabric-harness/sdk 1.13.0 → 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.map +1 -1
- package/dist/agent.d.ts +2 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +35 -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/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 +29 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -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 +37 -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 +7 -1
- 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 +34 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +286 -33
- 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 +18 -10
- package/dist/strict.d.ts.map +1 -1
- package/dist/strict.js +10 -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 +105 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +12 -8
package/dist/session.js
CHANGED
|
@@ -8,16 +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
14
|
import { createFabricFs } from './filesystem.js';
|
|
15
15
|
import { registerSandbox, serializeSandboxRef, unregisterSandbox } from './sandbox-ref.js';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { defaultSessionStore } from './store.js';
|
|
18
|
-
import { createBuiltinTools, createCommandTools, shellQuote } from './tools.js';
|
|
18
|
+
import { createActivateSkillTool, createBuiltinTools, createCommandTools, shellQuote } from './tools.js';
|
|
19
|
+
import { formatPackagedSkillResourceList, readPackagedSkillFile } from './skills.js';
|
|
19
20
|
import { MAX_TASK_DEPTH } from './types.js';
|
|
20
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.`;
|
|
21
74
|
export class StubFabricSession {
|
|
22
75
|
id;
|
|
23
76
|
agent;
|
|
@@ -49,6 +102,7 @@ export class StubFabricSession {
|
|
|
49
102
|
/** Set when this session has registered its sandbox in the in-process ref registry. */
|
|
50
103
|
ownedSandboxRefId;
|
|
51
104
|
costBudget;
|
|
105
|
+
skillCache = new Map();
|
|
52
106
|
constructor(agent, id, options = {}) {
|
|
53
107
|
this.agent = agent;
|
|
54
108
|
this.id = id;
|
|
@@ -95,7 +149,8 @@ export class StubFabricSession {
|
|
|
95
149
|
const sandbox = createScopedSandboxEnv(await this.sandbox, options.cwd);
|
|
96
150
|
const tools = this.resolveTools(sandbox, options.tools, options.commands);
|
|
97
151
|
await this.recordEntry('user_prompt', { text, ...(role ? { role: role.name } : {}) });
|
|
98
|
-
|
|
152
|
+
const sessionData = await this.history();
|
|
153
|
+
let messages = buildModelMessagesFromHistory(sessionData, role);
|
|
99
154
|
if (!messages.some((message) => message.role === 'user' && message.content === text))
|
|
100
155
|
messages.push({ role: 'user', content: text });
|
|
101
156
|
messages = await this.autoCompactIfNeeded(messages, role, options);
|
|
@@ -105,14 +160,19 @@ export class StubFabricSession {
|
|
|
105
160
|
await this.emit('prompt_end', { text, result: results });
|
|
106
161
|
return validateResult(results, options.result, options.resultExtraction);
|
|
107
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;
|
|
108
166
|
const runtime = options.loopRuntime ?? this.options.loopRuntime ?? defaultLoopRuntime;
|
|
109
167
|
const runRuntime = () => runtime.runPrompt({
|
|
110
168
|
sessionId: this.id,
|
|
169
|
+
...(sessionData.affinityKey !== undefined ? { affinityKey: sessionData.affinityKey } : {}),
|
|
111
170
|
text,
|
|
112
171
|
messages,
|
|
113
172
|
tools,
|
|
114
173
|
sandbox,
|
|
115
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 } : {}),
|
|
116
176
|
...((options.modelProvider ?? this.options.modelProvider) !== undefined ? { modelProvider: options.modelProvider ?? this.options.modelProvider } : {}),
|
|
117
177
|
...(options.maxIterations !== undefined ? { maxIterations: options.maxIterations } : {}),
|
|
118
178
|
...(options.modelTimeoutMs !== undefined ? { modelTimeoutMs: options.modelTimeoutMs } : {}),
|
|
@@ -125,7 +185,10 @@ export class StubFabricSession {
|
|
|
125
185
|
costBudget: this.costBudget,
|
|
126
186
|
requestCostLimitApproval: (ctx) => this.requestCostLimitApproval(ctx, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval),
|
|
127
187
|
} : {}),
|
|
128
|
-
|
|
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),
|
|
129
192
|
recordEntry: (type, data) => this.recordEntry(type, data),
|
|
130
193
|
emit: (type, data) => this.emit(type, data),
|
|
131
194
|
});
|
|
@@ -147,6 +210,10 @@ export class StubFabricSession {
|
|
|
147
210
|
messages = buildModelMessagesFromHistory(await this.history(), role);
|
|
148
211
|
result = await runRuntime();
|
|
149
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
|
+
}
|
|
150
217
|
const resultRetries = options.resultRetries ?? (options.result ? 1 : 0);
|
|
151
218
|
for (let attempt = 0; attempt <= resultRetries; attempt += 1) {
|
|
152
219
|
try {
|
|
@@ -218,7 +285,8 @@ export class StubFabricSession {
|
|
|
218
285
|
if (!skill)
|
|
219
286
|
throw new FabricError({ code: 'SKILL_NOT_FOUND', message: `Skill not found: ${name}`, details: { skill: name } });
|
|
220
287
|
const argsText = options.args ? `\n\nArguments:\n${JSON.stringify(options.args, null, 2)}` : '';
|
|
221
|
-
const
|
|
288
|
+
const packagedText = skill.packaged ? formatPackagedSkillResourceList(skill.packaged) : '';
|
|
289
|
+
const result = await this.prompt(`${skill.content ?? ''}${argsText}${packagedText}`, options);
|
|
222
290
|
await this.recordEntry('skill_end', { skill: name });
|
|
223
291
|
await this.emit('skill_end', { skill: name });
|
|
224
292
|
return result;
|
|
@@ -380,10 +448,22 @@ export class StubFabricSession {
|
|
|
380
448
|
const timeout = clampCommandTimeout(options.timeout, policy);
|
|
381
449
|
if (timeout !== undefined)
|
|
382
450
|
execOptions.timeout = timeout;
|
|
383
|
-
await this.recordEntry('shell_command', { command });
|
|
384
451
|
await this.emit('command_start', { command });
|
|
385
452
|
const result = await sandbox.exec(command, execOptions);
|
|
386
|
-
|
|
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
|
+
});
|
|
387
467
|
await this.emit('command_end', { command, exitCode: result.exitCode, ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}) });
|
|
388
468
|
return result;
|
|
389
469
|
}
|
|
@@ -439,6 +519,9 @@ export class StubFabricSession {
|
|
|
439
519
|
async history() {
|
|
440
520
|
return this.ensureData();
|
|
441
521
|
}
|
|
522
|
+
async createSignalEntry(data) {
|
|
523
|
+
return this.recordEntry('signal', data);
|
|
524
|
+
}
|
|
442
525
|
async sandboxRef(options) {
|
|
443
526
|
const sandbox = await this.sandbox;
|
|
444
527
|
const ref = registerSandbox(sandbox, { ownerSessionId: this.id });
|
|
@@ -521,13 +604,39 @@ export class StubFabricSession {
|
|
|
521
604
|
async compact(options = {}) {
|
|
522
605
|
const history = await this.history();
|
|
523
606
|
const entries = history.entries ?? [];
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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));
|
|
530
638
|
const summary = appendFileReferenceTags(baseSummary, fileRefs);
|
|
639
|
+
const compactedEntries = compacted.length;
|
|
531
640
|
const entry = await this.recordEntry('compaction', {
|
|
532
641
|
summary,
|
|
533
642
|
compactedEntries,
|
|
@@ -549,6 +658,7 @@ export class StubFabricSession {
|
|
|
549
658
|
summary,
|
|
550
659
|
compactedEntries,
|
|
551
660
|
...(firstKeptEntry ? { firstKeptEntryId: firstKeptEntry.id } : {}),
|
|
661
|
+
details: { readFiles: fileRefs.readFiles, modifiedFiles: fileRefs.modifiedFiles },
|
|
552
662
|
};
|
|
553
663
|
}
|
|
554
664
|
async autoCompactIfNeeded(messages, role, options) {
|
|
@@ -559,18 +669,26 @@ export class StubFabricSession {
|
|
|
559
669
|
return messages;
|
|
560
670
|
const compactAtTokens = options.compactAtTokens ?? settings?.compactAtTokens;
|
|
561
671
|
const reserveTokens = options.reserveTokens ?? settings?.reserveTokens;
|
|
562
|
-
const keepRecentEntries = options.compactionKeepRecentEntries ?? settings?.keepRecentEntries
|
|
672
|
+
const keepRecentEntries = options.compactionKeepRecentEntries ?? settings?.keepRecentEntries;
|
|
563
673
|
const contextWindowTokens = options.contextWindowTokens ?? this.resolveModelContextWindow(options);
|
|
564
674
|
const budget = evaluateContextBudget(messages, {
|
|
565
675
|
...(contextWindowTokens !== undefined ? { contextWindowTokens } : {}),
|
|
566
676
|
...(compactAtTokens !== undefined ? { compactAtTokens } : {}),
|
|
567
677
|
...(reserveTokens !== undefined ? { reserveTokens } : {}),
|
|
678
|
+
...(settings?.keepRecentTokens !== undefined ? { keepRecentTokens: settings.keepRecentTokens } : {}),
|
|
568
679
|
});
|
|
569
680
|
if (!budget.shouldCompact)
|
|
570
681
|
return messages;
|
|
571
682
|
const messagesBefore = messages.length;
|
|
572
683
|
const tokensBefore = budget.estimatedTokens;
|
|
573
|
-
|
|
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);
|
|
574
692
|
const compacted = buildModelMessagesFromHistory(await this.history(), role);
|
|
575
693
|
return compacted;
|
|
576
694
|
}
|
|
@@ -591,23 +709,29 @@ export class StubFabricSession {
|
|
|
591
709
|
return undefined;
|
|
592
710
|
}
|
|
593
711
|
}
|
|
594
|
-
async generateCompactionSummary(
|
|
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) {
|
|
595
723
|
if (entries.length === 0)
|
|
596
724
|
return summarizeEntries(entries);
|
|
597
725
|
const deterministic = summarizeEntries(entries);
|
|
598
726
|
const provider = this.options.modelProvider ?? defaultModelProvider;
|
|
599
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;
|
|
600
730
|
const response = await generateWithRuntime(provider, {
|
|
601
731
|
...(this.agent.model !== undefined ? { model: this.agent.model } : {}),
|
|
602
732
|
messages: [
|
|
603
|
-
{
|
|
604
|
-
|
|
605
|
-
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.',
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
role: 'user',
|
|
609
|
-
content: JSON.stringify(entries.map((entry) => ({ type: entry.type, timestamp: entry.timestamp, data: entry.data ?? {} })).slice(0, 200)),
|
|
610
|
-
},
|
|
733
|
+
{ role: 'system', content: COMPACTION_SUMMARIZATION_SYSTEM_PROMPT },
|
|
734
|
+
{ role: 'user', content: userContent },
|
|
611
735
|
],
|
|
612
736
|
}, { retries: 1, retryDelayMs: 250 });
|
|
613
737
|
return response.message?.content?.trim() || deterministic;
|
|
@@ -709,7 +833,7 @@ export class StubFabricSession {
|
|
|
709
833
|
const scoped = createScopedSandboxEnv(env, this.options.cwd);
|
|
710
834
|
return wrapWithPolicy(scoped, this.options.policy, this.options.bypassPolicyEnforcement);
|
|
711
835
|
}
|
|
712
|
-
async executeToolCalls(calls, tools, sandbox, callPolicy, approvalTimeoutMs, approvalHandler) {
|
|
836
|
+
async executeToolCalls(calls, tools, sandbox, callPolicy, approvalTimeoutMs, approvalHandler, attribution) {
|
|
713
837
|
const results = [];
|
|
714
838
|
const policy = callPolicy ?? this.options.policy;
|
|
715
839
|
for (const call of calls) {
|
|
@@ -781,13 +905,13 @@ export class StubFabricSession {
|
|
|
781
905
|
continue;
|
|
782
906
|
}
|
|
783
907
|
}
|
|
784
|
-
await this.recordEntry('tool_call', { tool: call.tool, input: call.input, ...(tool?.metadata?.effect ? { effect: tool.metadata.effect } : {}), ...(call.id ? { toolCallId: call.id } : {}) });
|
|
785
|
-
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 } : {}) });
|
|
786
910
|
if (!tool?.execute) {
|
|
787
911
|
const error = `Tool is not available: ${call.tool}`;
|
|
788
912
|
results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
|
|
789
913
|
await this.recordEntry('error', { tool: call.tool, error, approvalRequired: false });
|
|
790
|
-
await this.emit('tool_end', { tool: call.tool, error });
|
|
914
|
+
await this.emit('tool_end', { tool: call.tool, error, ...(attribution ? { attribution: attribution } : {}) });
|
|
791
915
|
continue;
|
|
792
916
|
}
|
|
793
917
|
try {
|
|
@@ -796,14 +920,14 @@ export class StubFabricSession {
|
|
|
796
920
|
const output = await tool.execute(input, { sandbox });
|
|
797
921
|
const durationMs = Date.now() - startedAt;
|
|
798
922
|
results.push({ tool: call.tool, input, output, ...(call.id ? { id: call.id } : {}) });
|
|
799
|
-
await this.recordEntry('tool_result', { tool: call.tool, output: output, durationMs, ...(call.id ? { toolCallId: call.id } : {}) });
|
|
800
|
-
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 } : {}) });
|
|
801
925
|
}
|
|
802
926
|
catch (error) {
|
|
803
927
|
const message = error instanceof Error ? error.message : String(error);
|
|
804
928
|
results.push({ tool: call.tool, input: call.input, error: message, ...(call.id ? { id: call.id } : {}) });
|
|
805
929
|
await this.recordEntry('error', { tool: call.tool, error: message });
|
|
806
|
-
await this.emit('tool_end', { tool: call.tool, error: message });
|
|
930
|
+
await this.emit('tool_end', { tool: call.tool, error: message, ...(attribution ? { attribution: attribution } : {}) });
|
|
807
931
|
}
|
|
808
932
|
}
|
|
809
933
|
return results;
|
|
@@ -929,7 +1053,7 @@ export class StubFabricSession {
|
|
|
929
1053
|
resolveTools(sandbox, promptTools = [], promptCommands = []) {
|
|
930
1054
|
const tools = new Map();
|
|
931
1055
|
const commandToolOptions = this.options.resolveSecret ? { resolveSecret: this.options.resolveSecret } : {};
|
|
932
|
-
for (const tool of createBuiltinTools(sandbox))
|
|
1056
|
+
for (const tool of createBuiltinTools(sandbox, this.options.packagedSkills))
|
|
933
1057
|
tools.set(tool.name, tool);
|
|
934
1058
|
for (const tool of createCommandTools(this.options.commands ?? [], commandToolOptions))
|
|
935
1059
|
tools.set(tool.name, tool);
|
|
@@ -939,8 +1063,40 @@ export class StubFabricSession {
|
|
|
939
1063
|
tools.set(tool.name, tool);
|
|
940
1064
|
for (const tool of promptTools)
|
|
941
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
|
+
}
|
|
942
1072
|
return tools;
|
|
943
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
|
+
}
|
|
944
1100
|
resolveSkill(name) {
|
|
945
1101
|
const normalized = normalizeSkillReference(name);
|
|
946
1102
|
return this.options.skills?.find((skill) => {
|
|
@@ -986,10 +1142,22 @@ export class StubFabricSession {
|
|
|
986
1142
|
retryable: false,
|
|
987
1143
|
});
|
|
988
1144
|
}
|
|
1145
|
+
if (!existing.affinityKey) {
|
|
1146
|
+
existing.affinityKey = generateAffinityKey(this.agent.id, this.id);
|
|
1147
|
+
await this.store.save(existing);
|
|
1148
|
+
}
|
|
989
1149
|
return existing;
|
|
990
1150
|
}
|
|
991
1151
|
const now = new Date().toISOString();
|
|
992
|
-
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
|
+
};
|
|
993
1161
|
if (this.options.metadata !== undefined)
|
|
994
1162
|
data.metadata = this.options.metadata;
|
|
995
1163
|
if (this.options.tenantId !== undefined)
|
|
@@ -1069,6 +1237,20 @@ function isJsonValue(value) {
|
|
|
1069
1237
|
return false;
|
|
1070
1238
|
return Object.values(value).every(isJsonValue);
|
|
1071
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
|
+
}
|
|
1072
1254
|
function summarizeEntries(entries) {
|
|
1073
1255
|
if (entries.length === 0)
|
|
1074
1256
|
return 'No entries to compact.';
|
|
@@ -1108,6 +1290,77 @@ function appendFileReferenceTags(summary, refs) {
|
|
|
1108
1290
|
sections.push(`<modified-files>\n${refs.modifiedFiles.join('\n')}\n</modified-files>`);
|
|
1109
1291
|
return sections.join('\n\n');
|
|
1110
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
|
+
}
|
|
1111
1364
|
function isReadOnlyFileTool(tool) {
|
|
1112
1365
|
return tool !== undefined && ['read', 'read_buffer', 'stat', 'readdir', 'exists', 'grep', 'glob'].includes(tool);
|
|
1113
1366
|
}
|