@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.
Files changed (114) hide show
  1. package/dist/agent-definition.d.ts +6 -0
  2. package/dist/agent-definition.d.ts.map +1 -1
  3. package/dist/agent-definition.js +5 -4
  4. package/dist/agent-definition.js.map +1 -1
  5. package/dist/agent.d.ts +6 -0
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +45 -1
  8. package/dist/agent.js.map +1 -1
  9. package/dist/channel.d.ts +77 -0
  10. package/dist/channel.d.ts.map +1 -0
  11. package/dist/channel.js +115 -0
  12. package/dist/channel.js.map +1 -0
  13. package/dist/context-budget.d.ts +16 -0
  14. package/dist/context-budget.d.ts.map +1 -1
  15. package/dist/context-budget.js +38 -3
  16. package/dist/context-budget.js.map +1 -1
  17. package/dist/cost-budget.d.ts +106 -1
  18. package/dist/cost-budget.d.ts.map +1 -1
  19. package/dist/cost-budget.js +48 -2
  20. package/dist/cost-budget.js.map +1 -1
  21. package/dist/dispatch.d.ts +95 -0
  22. package/dist/dispatch.d.ts.map +1 -0
  23. package/dist/dispatch.js +175 -0
  24. package/dist/dispatch.js.map +1 -0
  25. package/dist/error.d.ts +1 -1
  26. package/dist/error.d.ts.map +1 -1
  27. package/dist/error.js.map +1 -1
  28. package/dist/filesystem.d.ts +52 -0
  29. package/dist/filesystem.d.ts.map +1 -0
  30. package/dist/filesystem.js +24 -0
  31. package/dist/filesystem.js.map +1 -0
  32. package/dist/history.d.ts +22 -2
  33. package/dist/history.d.ts.map +1 -1
  34. package/dist/history.js +69 -0
  35. package/dist/history.js.map +1 -1
  36. package/dist/index.d.ts +31 -12
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +17 -6
  39. package/dist/index.js.map +1 -1
  40. package/dist/loop.d.ts +29 -3
  41. package/dist/loop.d.ts.map +1 -1
  42. package/dist/loop.js +167 -24
  43. package/dist/loop.js.map +1 -1
  44. package/dist/model.d.ts +47 -0
  45. package/dist/model.d.ts.map +1 -1
  46. package/dist/model.js +250 -79
  47. package/dist/model.js.map +1 -1
  48. package/dist/otel-observer.d.ts +44 -0
  49. package/dist/otel-observer.d.ts.map +1 -0
  50. package/dist/otel-observer.js +165 -0
  51. package/dist/otel-observer.js.map +1 -0
  52. package/dist/persistence.d.ts +70 -0
  53. package/dist/persistence.d.ts.map +1 -0
  54. package/dist/persistence.js +17 -0
  55. package/dist/persistence.js.map +1 -0
  56. package/dist/persistent-agent.d.ts +80 -0
  57. package/dist/persistent-agent.d.ts.map +1 -0
  58. package/dist/persistent-agent.js +69 -0
  59. package/dist/persistent-agent.js.map +1 -0
  60. package/dist/pi-loop-runtime.d.ts.map +1 -1
  61. package/dist/pi-loop-runtime.js +9 -3
  62. package/dist/pi-loop-runtime.js.map +1 -1
  63. package/dist/profile.d.ts +33 -0
  64. package/dist/profile.d.ts.map +1 -0
  65. package/dist/profile.js +10 -0
  66. package/dist/profile.js.map +1 -0
  67. package/dist/providers.d.ts +14 -0
  68. package/dist/providers.d.ts.map +1 -1
  69. package/dist/providers.js +18 -1
  70. package/dist/providers.js.map +1 -1
  71. package/dist/result.d.ts +12 -0
  72. package/dist/result.d.ts.map +1 -1
  73. package/dist/result.js +26 -0
  74. package/dist/result.js.map +1 -1
  75. package/dist/retrieval.d.ts +61 -0
  76. package/dist/retrieval.d.ts.map +1 -0
  77. package/dist/retrieval.js +26 -0
  78. package/dist/retrieval.js.map +1 -0
  79. package/dist/runtime-mode.d.ts +1 -1
  80. package/dist/runtime-mode.d.ts.map +1 -1
  81. package/dist/runtime-mode.js +2 -1
  82. package/dist/runtime-mode.js.map +1 -1
  83. package/dist/scope-key.d.ts +25 -0
  84. package/dist/scope-key.d.ts.map +1 -0
  85. package/dist/scope-key.js +34 -0
  86. package/dist/scope-key.js.map +1 -0
  87. package/dist/session.d.ts +40 -1
  88. package/dist/session.d.ts.map +1 -1
  89. package/dist/session.js +308 -34
  90. package/dist/session.js.map +1 -1
  91. package/dist/skills.d.ts +33 -0
  92. package/dist/skills.d.ts.map +1 -0
  93. package/dist/skills.js +65 -0
  94. package/dist/skills.js.map +1 -0
  95. package/dist/store.d.ts +36 -1
  96. package/dist/store.d.ts.map +1 -1
  97. package/dist/store.js +83 -0
  98. package/dist/store.js.map +1 -1
  99. package/dist/stream-chunks.d.ts +42 -0
  100. package/dist/stream-chunks.d.ts.map +1 -0
  101. package/dist/stream-chunks.js +138 -0
  102. package/dist/stream-chunks.js.map +1 -0
  103. package/dist/strict.d.ts +21 -11
  104. package/dist/strict.d.ts.map +1 -1
  105. package/dist/strict.js +11 -6
  106. package/dist/strict.js.map +1 -1
  107. package/dist/tools.d.ts +32 -4
  108. package/dist/tools.d.ts.map +1 -1
  109. package/dist/tools.js +132 -6
  110. package/dist/tools.js.map +1 -1
  111. package/dist/types.d.ts +110 -3
  112. package/dist/types.d.ts.map +1 -1
  113. package/dist/types.js.map +1 -1
  114. 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
- let messages = buildModelMessagesFromHistory(await this.history(), role);
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
- executeToolCalls: (calls, policy, approvalTimeoutMs) => this.executeToolCalls(calls, tools, sandbox, policy, approvalTimeoutMs, options.onApproval),
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 result = await this.prompt(`${skill.content ?? ''}${argsText}`, options);
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
- await this.recordEntry('shell_result', { command, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}) });
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 keepRecentEntries = options.keepRecentEntries ?? 20;
522
- const compactedEntries = Math.max(0, entries.length - keepRecentEntries);
523
- const firstKeptEntry = compactedEntries > 0 ? entries[compactedEntries] : undefined;
524
- const compacted = compactedEntries > 0 ? entries.slice(0, compactedEntries) : [];
525
- const fileRefs = collectFileReferences(compacted);
526
- const baseSummary = options.summary ?? (options.generateSummary ? await this.generateCompactionSummary(compacted) : summarizeEntries(compacted));
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 ?? 20;
672
+ const keepRecentEntries = options.compactionKeepRecentEntries ?? settings?.keepRecentEntries;
673
+ const contextWindowTokens = options.contextWindowTokens ?? this.resolveModelContextWindow(options);
560
674
  const budget = evaluateContextBudget(messages, {
561
- ...(options.contextWindowTokens !== undefined ? { contextWindowTokens: options.contextWindowTokens } : {}),
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
- await this.compact({ keepRecentEntries, generateSummary: true, reason: 'threshold', tokensBefore, messagesBefore });
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
- async generateCompactionSummary(entries) {
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
- role: 'system',
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 = { id: this.id, agentId: this.agent.id, createdAt: now, updatedAt: now, events: [], entries: [] };
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
  }