@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.
Files changed (109) 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.map +1 -1
  4. package/dist/agent.d.ts +2 -0
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +35 -1
  7. package/dist/agent.js.map +1 -1
  8. package/dist/channel.d.ts +77 -0
  9. package/dist/channel.d.ts.map +1 -0
  10. package/dist/channel.js +115 -0
  11. package/dist/channel.js.map +1 -0
  12. package/dist/context-budget.d.ts +16 -0
  13. package/dist/context-budget.d.ts.map +1 -1
  14. package/dist/context-budget.js +38 -3
  15. package/dist/context-budget.js.map +1 -1
  16. package/dist/cost-budget.d.ts +106 -1
  17. package/dist/cost-budget.d.ts.map +1 -1
  18. package/dist/cost-budget.js +48 -2
  19. package/dist/cost-budget.js.map +1 -1
  20. package/dist/dispatch.d.ts +95 -0
  21. package/dist/dispatch.d.ts.map +1 -0
  22. package/dist/dispatch.js +175 -0
  23. package/dist/dispatch.js.map +1 -0
  24. package/dist/error.d.ts +1 -1
  25. package/dist/error.d.ts.map +1 -1
  26. package/dist/error.js.map +1 -1
  27. package/dist/history.d.ts +22 -2
  28. package/dist/history.d.ts.map +1 -1
  29. package/dist/history.js +69 -0
  30. package/dist/history.js.map +1 -1
  31. package/dist/index.d.ts +29 -12
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +16 -6
  34. package/dist/index.js.map +1 -1
  35. package/dist/loop.d.ts +29 -3
  36. package/dist/loop.d.ts.map +1 -1
  37. package/dist/loop.js +167 -24
  38. package/dist/loop.js.map +1 -1
  39. package/dist/model.d.ts +37 -0
  40. package/dist/model.d.ts.map +1 -1
  41. package/dist/model.js +250 -79
  42. package/dist/model.js.map +1 -1
  43. package/dist/otel-observer.d.ts +44 -0
  44. package/dist/otel-observer.d.ts.map +1 -0
  45. package/dist/otel-observer.js +165 -0
  46. package/dist/otel-observer.js.map +1 -0
  47. package/dist/persistence.d.ts +70 -0
  48. package/dist/persistence.d.ts.map +1 -0
  49. package/dist/persistence.js +17 -0
  50. package/dist/persistence.js.map +1 -0
  51. package/dist/persistent-agent.d.ts +80 -0
  52. package/dist/persistent-agent.d.ts.map +1 -0
  53. package/dist/persistent-agent.js +69 -0
  54. package/dist/persistent-agent.js.map +1 -0
  55. package/dist/pi-loop-runtime.d.ts.map +1 -1
  56. package/dist/pi-loop-runtime.js +7 -1
  57. package/dist/pi-loop-runtime.js.map +1 -1
  58. package/dist/profile.d.ts +33 -0
  59. package/dist/profile.d.ts.map +1 -0
  60. package/dist/profile.js +10 -0
  61. package/dist/profile.js.map +1 -0
  62. package/dist/providers.d.ts +14 -0
  63. package/dist/providers.d.ts.map +1 -1
  64. package/dist/providers.js +18 -1
  65. package/dist/providers.js.map +1 -1
  66. package/dist/result.d.ts +12 -0
  67. package/dist/result.d.ts.map +1 -1
  68. package/dist/result.js +26 -0
  69. package/dist/result.js.map +1 -1
  70. package/dist/retrieval.d.ts +61 -0
  71. package/dist/retrieval.d.ts.map +1 -0
  72. package/dist/retrieval.js +26 -0
  73. package/dist/retrieval.js.map +1 -0
  74. package/dist/runtime-mode.d.ts +1 -1
  75. package/dist/runtime-mode.d.ts.map +1 -1
  76. package/dist/runtime-mode.js +2 -1
  77. package/dist/runtime-mode.js.map +1 -1
  78. package/dist/scope-key.d.ts +25 -0
  79. package/dist/scope-key.d.ts.map +1 -0
  80. package/dist/scope-key.js +34 -0
  81. package/dist/scope-key.js.map +1 -0
  82. package/dist/session.d.ts +34 -1
  83. package/dist/session.d.ts.map +1 -1
  84. package/dist/session.js +286 -33
  85. package/dist/session.js.map +1 -1
  86. package/dist/skills.d.ts +33 -0
  87. package/dist/skills.d.ts.map +1 -0
  88. package/dist/skills.js +65 -0
  89. package/dist/skills.js.map +1 -0
  90. package/dist/store.d.ts +36 -1
  91. package/dist/store.d.ts.map +1 -1
  92. package/dist/store.js +83 -0
  93. package/dist/store.js.map +1 -1
  94. package/dist/stream-chunks.d.ts +42 -0
  95. package/dist/stream-chunks.d.ts.map +1 -0
  96. package/dist/stream-chunks.js +138 -0
  97. package/dist/stream-chunks.js.map +1 -0
  98. package/dist/strict.d.ts +18 -10
  99. package/dist/strict.d.ts.map +1 -1
  100. package/dist/strict.js +10 -6
  101. package/dist/strict.js.map +1 -1
  102. package/dist/tools.d.ts +32 -4
  103. package/dist/tools.d.ts.map +1 -1
  104. package/dist/tools.js +132 -6
  105. package/dist/tools.js.map +1 -1
  106. package/dist/types.d.ts +105 -3
  107. package/dist/types.d.ts.map +1 -1
  108. package/dist/types.js.map +1 -1
  109. 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
- let messages = buildModelMessagesFromHistory(await this.history(), role);
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
- 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),
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 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);
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
- 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
+ });
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 keepRecentEntries = options.keepRecentEntries ?? 20;
525
- const compactedEntries = Math.max(0, entries.length - keepRecentEntries);
526
- const firstKeptEntry = compactedEntries > 0 ? entries[compactedEntries] : undefined;
527
- const compacted = compactedEntries > 0 ? entries.slice(0, compactedEntries) : [];
528
- const fileRefs = collectFileReferences(compacted);
529
- 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));
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 ?? 20;
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
- 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);
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(entries) {
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
- role: 'system',
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 = { 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
+ };
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
  }