@flue/sdk 0.3.10 → 0.4.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.
@@ -1,9 +1,74 @@
1
- import { i as loadSkillByPath, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-BTB0809P.mjs";
1
+ import { a as buildSkillByPathPrompt, c as createTools, f as resolveSkillFilePath, i as buildSkillByNamePrompt, l as formatBashResult, n as buildPromptText, o as createResultTools, p as skillsDirIn, r as buildResultFollowUpPrompt, s as BUILTIN_TOOL_NAMES, t as ResultUnavailableError } from "./result-K1IRhWKM.mjs";
2
+ import { i as getRegisteredApiKey, r as getProviderConfiguration } from "./providers-DeFRIwp0.mjs";
3
+ import { n as createCallHandle, t as abortErrorFor } from "./abort-Bg3qsAkU.mjs";
4
+ import { createFlueFs } from "./sandbox.mjs";
2
5
  import { completeSimple, isContextOverflow } from "@mariozechner/pi-ai";
3
6
  import { Agent } from "@mariozechner/pi-agent-core";
4
- import { toJsonSchema } from "@valibot/to-json-schema";
5
- import * as v from "valibot";
6
7
 
8
+ //#region src/usage.ts
9
+ /** All-zero `PromptUsage`. Identity element for `addUsage`. */
10
+ function emptyUsage() {
11
+ return {
12
+ input: 0,
13
+ output: 0,
14
+ cacheRead: 0,
15
+ cacheWrite: 0,
16
+ totalTokens: 0,
17
+ cost: {
18
+ input: 0,
19
+ output: 0,
20
+ cacheRead: 0,
21
+ cacheWrite: 0,
22
+ total: 0
23
+ }
24
+ };
25
+ }
26
+ /**
27
+ * Field-wise sum of two `PromptUsage` values, including the nested `cost`
28
+ * sub-object. Returns a fresh object; neither argument is mutated.
29
+ */
30
+ function addUsage(a, b) {
31
+ return {
32
+ input: a.input + b.input,
33
+ output: a.output + b.output,
34
+ cacheRead: a.cacheRead + b.cacheRead,
35
+ cacheWrite: a.cacheWrite + b.cacheWrite,
36
+ totalTokens: a.totalTokens + b.totalTokens,
37
+ cost: {
38
+ input: a.cost.input + b.cost.input,
39
+ output: a.cost.output + b.cost.output,
40
+ cacheRead: a.cost.cacheRead + b.cost.cacheRead,
41
+ cacheWrite: a.cost.cacheWrite + b.cost.cacheWrite,
42
+ total: a.cost.total + b.cost.total
43
+ }
44
+ };
45
+ }
46
+ /**
47
+ * Convert pi-ai's `Usage` into Flue's public `PromptUsage`. The shapes are
48
+ * structurally identical today, but going through this normalizer keeps
49
+ * Flue's public types decoupled from pi-ai's so future divergence in
50
+ * pi-ai (e.g. additional fields) doesn't leak into the SDK's public
51
+ * surface. Returns `undefined` when the input is `undefined`.
52
+ */
53
+ function fromProviderUsage(usage) {
54
+ if (!usage) return void 0;
55
+ return {
56
+ input: usage.input,
57
+ output: usage.output,
58
+ cacheRead: usage.cacheRead,
59
+ cacheWrite: usage.cacheWrite,
60
+ totalTokens: usage.totalTokens,
61
+ cost: {
62
+ input: usage.cost.input,
63
+ output: usage.cost.output,
64
+ cacheRead: usage.cost.cacheRead,
65
+ cacheWrite: usage.cost.cacheWrite,
66
+ total: usage.cost.total
67
+ }
68
+ };
69
+ }
70
+
71
+ //#endregion
7
72
  //#region src/compaction.ts
8
73
  const DEFAULT_COMPACTION_SETTINGS = {
9
74
  enabled: true,
@@ -341,7 +406,10 @@ async function generateSummary(currentMessages, model, reserveTokens, apiKey, si
341
406
  messages: summarizationMessages
342
407
  }, completionOptions);
343
408
  if (response.stopReason === "error") throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
344
- return response.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
409
+ return {
410
+ text: response.content.filter((c) => c.type === "text").map((c) => c.text).join("\n"),
411
+ usage: response.usage
412
+ };
345
413
  }
346
414
  async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, signal) {
347
415
  const maxTokens = Math.min(Math.floor(.5 * reserveTokens), 16e3);
@@ -358,20 +426,39 @@ async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey,
358
426
  signal
359
427
  };
360
428
  if (apiKey) completionOptions.apiKey = apiKey;
429
+ if (model.reasoning) completionOptions.reasoning = "high";
361
430
  const response = await completeSimple(model, {
362
431
  systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
363
432
  messages: summarizationMessages
364
433
  }, completionOptions);
365
434
  if (response.stopReason === "error") throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
366
- return response.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
435
+ return {
436
+ text: response.content.filter((c) => c.type === "text").map((c) => c.text).join("\n"),
437
+ usage: response.usage
438
+ };
367
439
  }
368
440
  async function compact(preparation, model, apiKey, signal) {
369
441
  const { firstKeptIndex, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, settings } = preparation;
370
442
  let summary;
443
+ let aggregateUsage;
444
+ const addCallUsage = (usage) => {
445
+ const normalized = fromProviderUsage(usage);
446
+ if (!normalized) return;
447
+ aggregateUsage = aggregateUsage ? addUsage(aggregateUsage, normalized) : normalized;
448
+ };
371
449
  if (isSplitTurn && turnPrefixMessages.length > 0) {
372
- const [historyResult, turnPrefixResult] = await Promise.all([messagesToSummarize.length > 0 ? generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, signal, previousSummary) : Promise.resolve("No prior history."), generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal)]);
373
- summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
374
- } else summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, signal, previousSummary);
450
+ const [historyResult, turnPrefixResult] = await Promise.all([messagesToSummarize.length > 0 ? generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, signal, previousSummary) : Promise.resolve({
451
+ text: "No prior history.",
452
+ usage: void 0
453
+ }), generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal)]);
454
+ addCallUsage(historyResult.usage);
455
+ addCallUsage(turnPrefixResult.usage);
456
+ summary = `${historyResult.text}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.text}`;
457
+ } else {
458
+ const historyResult = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, signal, previousSummary);
459
+ addCallUsage(historyResult.usage);
460
+ summary = historyResult.text;
461
+ }
375
462
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
376
463
  summary += formatFileOperations(readFiles, modifiedFiles);
377
464
  return {
@@ -381,109 +468,11 @@ async function compact(preparation, model, apiKey, signal) {
381
468
  details: {
382
469
  readFiles,
383
470
  modifiedFiles
384
- }
471
+ },
472
+ usage: aggregateUsage
385
473
  };
386
474
  }
387
475
 
388
- //#endregion
389
- //#region src/result.ts
390
- const HEADLESS_PREAMBLE = "You are running in headless mode with no human operator. Work autonomously — never ask questions, never wait for user input. Make your best judgment and proceed independently.";
391
- function buildResultInstructions(schema) {
392
- const { $schema: _, ...schemaWithoutMeta } = toJsonSchema(schema, { errorMode: "ignore" });
393
- return [
394
- "",
395
- "```json",
396
- JSON.stringify(schemaWithoutMeta, null, 2),
397
- "```",
398
- "",
399
- "Example: (Object)",
400
- "---RESULT_START---",
401
- "{\"key\": \"value\"}",
402
- "---RESULT_END---",
403
- "",
404
- "Example: (String)",
405
- "---RESULT_START---",
406
- "Hello, world!",
407
- "---RESULT_END---"
408
- ].join("\n");
409
- }
410
- /** Follow-up prompt used when the LLM forgets to include RESULT_START/RESULT_END delimiters. */
411
- function buildResultExtractionPrompt(schema) {
412
- return [
413
- "Your task is complete. Now respond with ONLY your final result.",
414
- "No explanation, no preamble — just the result in the following format, conforming to this schema:",
415
- buildResultInstructions(schema)
416
- ].join("\n");
417
- }
418
- function buildSkillPrompt(skillInstructions, args, schema) {
419
- const parts = [
420
- HEADLESS_PREAMBLE,
421
- "",
422
- skillInstructions
423
- ];
424
- if (args && Object.keys(args).length > 0) parts.push(`\nArguments:\n${JSON.stringify(args, null, 2)}`);
425
- if (schema) {
426
- parts.push("When complete, you MUST output your result between these exact delimiters conforming to this schema:");
427
- parts.push(buildResultInstructions(schema));
428
- }
429
- return parts.join("\n");
430
- }
431
- function buildPromptText(text, schema) {
432
- const parts = [
433
- HEADLESS_PREAMBLE,
434
- "",
435
- text
436
- ];
437
- if (schema) {
438
- parts.push("When complete, you MUST output your result between these exact delimiters conforming to this schema:");
439
- parts.push(buildResultInstructions(schema));
440
- }
441
- return parts.join("\n");
442
- }
443
- /** Extract the last ---RESULT_START---/---RESULT_END--- block from agent text and validate against schema. */
444
- function extractResult(text, schema) {
445
- const resultBlock = extractLastResultBlock(text);
446
- if (resultBlock === null) throw new ResultExtractionError("No ---RESULT_START--- / ---RESULT_END--- block found in the assistant response.", text);
447
- let result = resultBlock;
448
- if (schema.type === "object" || schema.type === "array") try {
449
- result = JSON.parse(resultBlock);
450
- } catch {
451
- throw new ResultExtractionError("Result block contains invalid JSON for the expected schema.", resultBlock);
452
- }
453
- const parsed = v.safeParse(schema, result);
454
- if (!parsed.success) throw new ResultExtractionError(`Result does not match the expected schema: ${parsed.issues.map((i) => i.message).join(", ")}`, resultBlock);
455
- return parsed.output;
456
- }
457
- function extractLastResultBlock(text) {
458
- const matches = text.matchAll(/---RESULT_START---\s*\n([\s\S]*?)---RESULT_END---/g);
459
- let lastMatch = null;
460
- for (const match of matches) lastMatch = match[1]?.trim() ?? null;
461
- return lastMatch;
462
- }
463
- var ResultExtractionError = class extends Error {
464
- constructor(message, rawOutput) {
465
- super(message);
466
- this.rawOutput = rawOutput;
467
- this.name = "ResultExtractionError";
468
- }
469
- };
470
-
471
- //#endregion
472
- //#region src/env-utils.ts
473
- async function createScopedEnv(env, commands) {
474
- if (env.scope) return env.scope({ commands });
475
- if (commands.length > 0) throw new Error("[flue] Cannot use commands: this environment does not support scoped command execution. Commands are only available in BashFactory sandbox mode. Remote sandboxes handle command execution at the platform level.");
476
- return env;
477
- }
478
- function mergeCommands(defaults, perCall) {
479
- if (!perCall || perCall.length === 0) return defaults;
480
- if (defaults.length === 0) return perCall;
481
- const byName = /* @__PURE__ */ new Map();
482
- for (const cmd of defaults) byName.set(cmd.name, cmd);
483
- for (const cmd of perCall) byName.set(cmd.name, cmd);
484
- return Array.from(byName.values());
485
- }
486
-
487
476
  //#endregion
488
477
  //#region src/roles.ts
489
478
  function assertRoleExists(roles, roleName) {
@@ -491,7 +480,7 @@ function assertRoleExists(roles, roleName) {
491
480
  if (roles[roleName]) return;
492
481
  const available = Object.keys(roles);
493
482
  const list = available.length > 0 ? available.join(", ") : "(none defined)";
494
- throw new Error(`[flue] Role "${roleName}" not registered. Available roles: ${list}. Define roles as markdown files in \`roles/\` (or \`.flue/roles/\`).`);
483
+ throw new Error(`[flue] Role "${roleName}" not registered. Available roles: ${list}. Define roles as markdown files in \`roles/\` (or \`.flue/roles/\` if your root uses the .flue/ source layout).`);
495
484
  }
496
485
  function resolveEffectiveRole(options) {
497
486
  const role = options.callRole ?? options.sessionRole ?? options.agentRole;
@@ -502,9 +491,39 @@ function resolveRoleModel(roles, roleName) {
502
491
  assertRoleExists(roles, roleName);
503
492
  return roleName ? roles[roleName]?.model : void 0;
504
493
  }
494
+ function resolveRoleThinkingLevel(roles, roleName) {
495
+ assertRoleExists(roles, roleName);
496
+ return roleName ? roles[roleName]?.thinkingLevel : void 0;
497
+ }
505
498
 
506
499
  //#endregion
507
- //#region src/session-history.ts
500
+ //#region src/session.ts
501
+ const MAX_TASK_DEPTH = 4;
502
+ /**
503
+ * Read the per-call schema option, accepting both the canonical `schema`
504
+ * field and the deprecated `result` alias. The deprecated alias is typed
505
+ * as `never` on the public option interfaces so TypeScript flags new
506
+ * usage; we still honor it at runtime during the deprecation window so
507
+ * existing callers keep working without code changes.
508
+ */
509
+ function resolveSchemaOption(options) {
510
+ if (!options) return void 0;
511
+ if (options.schema !== void 0) return options.schema;
512
+ return options.result;
513
+ }
514
+ /** In-memory session store. Sessions persist for the lifetime of the process. */
515
+ var InMemorySessionStore = class {
516
+ store = /* @__PURE__ */ new Map();
517
+ async save(id, data) {
518
+ this.store.set(id, data);
519
+ }
520
+ async load(id) {
521
+ return this.store.get(id) ?? null;
522
+ }
523
+ async delete(id) {
524
+ this.store.delete(id);
525
+ }
526
+ };
508
527
  var SessionHistory = class SessionHistory {
509
528
  entries;
510
529
  byId;
@@ -533,6 +552,26 @@ var SessionHistory = class SessionHistory {
533
552
  }
534
553
  return path.reverse();
535
554
  }
555
+ /**
556
+ * Active-path entries appended after `afterLeafId` (exclusive), in order.
557
+ *
558
+ * - `afterLeafId === null` means "from the start of the path" → returns
559
+ * the entire active path.
560
+ * - When the id is found, returns entries strictly after it.
561
+ * - When the id is *not* on the current active path (e.g. a branch
562
+ * switch happened mid-window), returns `[]`. Callers use this for
563
+ * bounded windowing — falling back to the full path would silently
564
+ * include unrelated history. An empty result is the safer answer
565
+ * for usage aggregation: zero is loud (sums won't match expectations)
566
+ * while full-history is silent overcounting.
567
+ */
568
+ getActivePathSince(afterLeafId) {
569
+ const path = this.getActivePath();
570
+ if (afterLeafId === null) return path;
571
+ const startIndex = path.findIndex((entry) => entry.id === afterLeafId);
572
+ if (startIndex === -1) return [];
573
+ return path.slice(startIndex + 1);
574
+ }
536
575
  buildContextEntries() {
537
576
  const path = this.getActivePath();
538
577
  const latestCompactionIndex = findLatestCompactionIndex(path);
@@ -583,7 +622,8 @@ var SessionHistory = class SessionHistory {
583
622
  summary: input.summary,
584
623
  firstKeptEntryId: input.firstKeptEntryId,
585
624
  tokensBefore: input.tokensBefore,
586
- details: input.details
625
+ details: input.details,
626
+ usage: input.usage
587
627
  };
588
628
  this.appendEntry(entry);
589
629
  return entry.id;
@@ -662,27 +702,9 @@ function generateEntryId(byId) {
662
702
  }
663
703
  return crypto.randomUUID();
664
704
  }
665
-
666
- //#endregion
667
- //#region src/session.ts
668
- /** Internal session implementation. Not exported publicly — wrapped by FlueSession. */
669
- const MAX_SHELL_HISTORY_CHARS = 50 * 1024;
670
- const MAX_TASK_DEPTH = 4;
671
- /** In-memory session store. Sessions persist for the lifetime of the process. */
672
- var InMemorySessionStore = class {
673
- store = /* @__PURE__ */ new Map();
674
- async save(id, data) {
675
- this.store.set(id, data);
676
- }
677
- async load(id) {
678
- return this.store.get(id) ?? null;
679
- }
680
- async delete(id) {
681
- this.store.delete(id);
682
- }
683
- };
684
705
  var Session = class {
685
706
  id;
707
+ fs;
686
708
  metadata;
687
709
  get role() {
688
710
  return this.sessionRole;
@@ -698,7 +720,6 @@ var Session = class {
698
720
  overflowRecoveryAttempted = false;
699
721
  compactionAbortController;
700
722
  eventCallback;
701
- agentCommands;
702
723
  agentTools;
703
724
  deleted = false;
704
725
  activeOperation;
@@ -712,8 +733,8 @@ var Session = class {
712
733
  this.storageKey = options.storageKey;
713
734
  this.config = options.config;
714
735
  this.env = options.env;
736
+ this.fs = createFlueFs(options.env);
715
737
  this.store = options.store;
716
- this.agentCommands = options.agentCommands ?? [];
717
738
  this.agentTools = options.agentTools ?? [];
718
739
  this.sessionRole = options.sessionRole;
719
740
  this.taskDepth = options.taskDepth ?? 0;
@@ -731,16 +752,18 @@ var Session = class {
731
752
  const systemPrompt = this.config.systemPrompt;
732
753
  assertRoleExists(this.config.roles, this.config.role);
733
754
  assertRoleExists(this.config.roles, this.sessionRole);
734
- const tools = [...this.createBuiltinTools(this.env, this.agentCommands, []), ...this.createCustomTools(this.agentTools)];
755
+ const tools = [...this.createBuiltinTools(this.env, []), ...this.createCustomTools(this.agentTools)];
735
756
  const previousMessages = this.history.buildContext();
736
757
  this.harness = new Agent({
737
758
  initialState: {
738
759
  systemPrompt,
739
760
  model: this.config.model,
740
761
  tools,
741
- messages: previousMessages
762
+ messages: previousMessages,
763
+ thinkingLevel: this.config.thinkingLevel ?? "medium"
742
764
  },
743
765
  getApiKey: (provider) => this.getProviderApiKey(provider),
766
+ onPayload: (payload, model) => this.applyProviderPayloadOverrides(payload, model),
744
767
  toolExecution: "parallel"
745
768
  });
746
769
  this.eventCallback = options.onAgentEvent;
@@ -755,6 +778,15 @@ var Session = class {
755
778
  type: "text_delta",
756
779
  text: aEvent.delta
757
780
  });
781
+ else if (aEvent.type === "thinking_start") this.emit({ type: "thinking_start" });
782
+ else if (aEvent.type === "thinking_delta") this.emit({
783
+ type: "thinking_delta",
784
+ delta: aEvent.delta
785
+ });
786
+ else if (aEvent.type === "thinking_end") this.emit({
787
+ type: "thinking_end",
788
+ content: aEvent.content
789
+ });
758
790
  break;
759
791
  }
760
792
  case "tool_execution_start":
@@ -781,86 +813,115 @@ var Session = class {
781
813
  }
782
814
  });
783
815
  }
784
- async prompt(text, options) {
785
- return this.runOperation("prompt", async () => {
786
- const role = this.resolveEffectiveRole(options?.role);
787
- const schema = options?.result;
788
- const fullPrompt = buildPromptText(text, schema);
789
- const effectiveCommands = mergeCommands(this.agentCommands, options?.commands);
790
- return this.withScopedRuntime({
791
- commands: effectiveCommands,
792
- tools: options?.tools ?? [],
793
- role,
816
+ prompt(text, options) {
817
+ return createCallHandle(options?.signal, (signal) => this.runOperation("prompt", signal, async () => {
818
+ const schema = resolveSchemaOption(options);
819
+ return this.runPromptCall({
820
+ promptText: buildPromptText(text, schema),
821
+ schema,
822
+ tools: options?.tools,
823
+ role: options?.role,
794
824
  model: options?.model,
795
- callSite: "this prompt() call"
796
- }, async () => {
797
- const beforeLength = this.harness.state.messages.length;
798
- await this.harness.prompt(fullPrompt);
799
- await this.harness.waitForIdle();
800
- await this.syncHarnessMessagesSince(beforeLength, "prompt");
801
- await this.checkLatestAssistantForCompaction();
802
- this.throwIfError("prompt");
803
- if (schema) return this.extractResultWithRetry(schema);
804
- return { text: this.getAssistantText() };
825
+ thinkingLevel: options?.thinkingLevel,
826
+ images: options?.images,
827
+ source: "prompt",
828
+ errorLabel: "prompt",
829
+ callSite: "this prompt() call",
830
+ signal
805
831
  });
806
- });
832
+ }));
807
833
  }
808
- async skill(name, options) {
809
- return this.runOperation("skill", async () => {
810
- const role = this.resolveEffectiveRole(options?.role);
811
- let registeredSkill = this.config.skills[name];
812
- if (!registeredSkill && (name.includes("/") || /\.(md|markdown)$/i.test(name))) {
813
- const loaded = await loadSkillByPath(this.env, this.env.cwd, name);
814
- if (loaded) registeredSkill = loaded;
815
- }
816
- if (!registeredSkill) {
817
- const available = Object.keys(this.config.skills).join(", ") || "(none)";
818
- const cwd = this.env.cwd;
819
- throw new Error(`Skill "${name}" not registered. Available: ${available}.\n\nSkills are loaded at init() time from ${cwd}/.agents/skills/<name>/SKILL.md inside the session's sandbox. If you expected "${name}" to be there, make sure the file exists in your sandbox at that path before calling init() — the default empty sandbox starts with no files, so it has no skills unless you put them there.\n\nSkills can also be referenced by relative path under .agents/skills/ (e.g. "triage/reproduce.md").`);
834
+ skill(name, options) {
835
+ return createCallHandle(options?.signal, (signal) => this.runOperation("skill", signal, async () => {
836
+ const looksLikePath = name.includes("/") || /\.(md|markdown)$/i.test(name);
837
+ const schema = resolveSchemaOption(options);
838
+ let promptText;
839
+ if (looksLikePath) {
840
+ const resolvedPath = await resolveSkillFilePath(this.env, this.env.cwd, name);
841
+ if (!resolvedPath) throw new Error(`[flue] Skill file "${name}" not found at ${skillsDirIn(this.env.cwd)}/${name} inside the session's sandbox. Make sure the file exists at that path.`);
842
+ promptText = buildSkillByPathPrompt(name, resolvedPath, options?.args, schema);
843
+ } else {
844
+ if (!this.config.skills[name]) {
845
+ const available = Object.keys(this.config.skills).join(", ") || "(none)";
846
+ throw new Error(`[flue] Skill "${name}" not registered. Available: ${available}.\n\nSkills are discovered at init() time from ${skillsDirIn(this.env.cwd)}/<name>/SKILL.md inside the session's sandbox. If you expected "${name}" to be there, make sure the SKILL.md file exists at that path before calling init() — the default empty sandbox starts with no files, so it has no skills unless you put them there.\n\nSkills can also be referenced by relative path under .agents/skills/ (e.g. "triage/reproduce.md").`);
847
+ }
848
+ promptText = buildSkillByNamePrompt(name, options?.args, schema);
820
849
  }
821
- const schema = options?.result;
822
- const skillPrompt = buildSkillPrompt(registeredSkill.instructions, options?.args, schema);
823
- const effectiveCommands = mergeCommands(this.agentCommands, options?.commands);
824
- return this.withScopedRuntime({
825
- commands: effectiveCommands,
826
- tools: options?.tools ?? [],
827
- role,
850
+ return this.runPromptCall({
851
+ promptText,
852
+ schema,
853
+ tools: options?.tools,
854
+ role: options?.role,
828
855
  model: options?.model,
829
- callSite: `this skill("${name}") call`
830
- }, async () => {
831
- const beforeLength = this.harness.state.messages.length;
832
- await this.harness.prompt(skillPrompt);
833
- await this.harness.waitForIdle();
834
- await this.syncHarnessMessagesSince(beforeLength, "skill");
835
- await this.checkLatestAssistantForCompaction();
836
- this.throwIfError(`skill("${name}")`);
837
- if (schema) return this.extractResultWithRetry(schema);
838
- return { text: this.getAssistantText() };
856
+ thinkingLevel: options?.thinkingLevel,
857
+ images: options?.images,
858
+ source: "skill",
859
+ errorLabel: `skill("${name}")`,
860
+ callSite: `this skill("${name}") call`,
861
+ signal
839
862
  });
840
- });
863
+ }));
841
864
  }
842
- async task(text, options) {
843
- return (await this.runTask(text, options, void 0)).output;
865
+ task(text, options) {
866
+ return createCallHandle(options?.signal, async (signal) => {
867
+ return (await this.runTask(text, options, signal)).output;
868
+ });
844
869
  }
845
- async shell(command, options) {
846
- return this.runOperation("shell", async () => {
847
- const effectiveCommands = mergeCommands(this.agentCommands, options?.commands);
848
- const result = await (await createScopedEnv(this.env, effectiveCommands)).exec(command, {
849
- env: options?.env,
850
- cwd: options?.cwd,
851
- timeout: options?.timeout
870
+ shell(command, options) {
871
+ return createCallHandle(options?.signal, (signal) => this.runOperation("shell", signal, async () => {
872
+ const toolCallId = crypto.randomUUID();
873
+ const args = { command };
874
+ if (options?.cwd !== void 0) args.cwd = options.cwd;
875
+ if (options?.env !== void 0) args.env = options.env;
876
+ this.emit({
877
+ type: "tool_start",
878
+ toolName: "bash",
879
+ toolCallId,
880
+ args
852
881
  });
853
- const shellResult = {
854
- stdout: result.stdout,
855
- stderr: result.stderr,
856
- exitCode: result.exitCode
857
- };
858
- const message = this.createShellMessage(command, shellResult, options);
859
- this.history.appendMessage(message, "shell");
860
- this.harness.state.messages = this.history.buildContext();
861
- await this.save();
862
- return shellResult;
863
- });
882
+ try {
883
+ const result = await this.env.exec(command, {
884
+ env: options?.env,
885
+ cwd: options?.cwd,
886
+ signal
887
+ });
888
+ const shellResult = {
889
+ stdout: result.stdout,
890
+ stderr: result.stderr,
891
+ exitCode: result.exitCode
892
+ };
893
+ const toolResult = formatBashResult(shellResult, command);
894
+ await this.appendShellTriple(toolCallId, args, toolResult, false);
895
+ this.emit({
896
+ type: "tool_end",
897
+ toolName: "bash",
898
+ toolCallId,
899
+ isError: false,
900
+ result: toolResult
901
+ });
902
+ return shellResult;
903
+ } catch (error) {
904
+ const errResult = {
905
+ content: [{
906
+ type: "text",
907
+ text: getErrorMessage(error)
908
+ }],
909
+ details: {
910
+ command,
911
+ exitCode: -1
912
+ }
913
+ };
914
+ await this.appendShellTriple(toolCallId, args, errResult, true);
915
+ this.emit({
916
+ type: "tool_end",
917
+ toolName: "bash",
918
+ toolCallId,
919
+ isError: true,
920
+ result: errResult
921
+ });
922
+ throw error;
923
+ }
924
+ }));
864
925
  }
865
926
  abort() {
866
927
  this.harness.abort();
@@ -892,10 +953,17 @@ var Session = class {
892
953
  resolveModelForCall(promptModel, roleName, callSite) {
893
954
  let model = this.config.model;
894
955
  const roleModel = resolveRoleModel(this.config.roles, roleName);
895
- if (roleModel) model = this.config.resolveModel(roleModel, this.config.providers);
896
- if (promptModel) model = this.config.resolveModel(promptModel, this.config.providers);
956
+ if (roleModel) model = this.config.resolveModel(roleModel);
957
+ if (promptModel) model = this.config.resolveModel(promptModel);
897
958
  return this.requireModel(model, callSite);
898
959
  }
960
+ /** Precedence: call-level > role-level > agent-level default > 'medium'. */
961
+ resolveThinkingLevelForCall(callValue, roleName) {
962
+ if (callValue !== void 0) return callValue;
963
+ const roleLevel = resolveRoleThinkingLevel(this.config.roles, roleName);
964
+ if (roleLevel !== void 0) return roleLevel;
965
+ return this.config.thinkingLevel ?? "medium";
966
+ }
899
967
  /**
900
968
  * Throws a clear, actionable error when no model is configured for a call.
901
969
  * Use with the resolved model (post-precedence) to guarantee we never hand
@@ -906,7 +974,21 @@ var Session = class {
906
974
  throw new Error(`[flue] No model configured for ${callSite}. Pass \`{ model: "provider/model-id" }\` to this call or configure a role model.`);
907
975
  }
908
976
  getProviderApiKey(provider) {
909
- return this.config.providers?.[provider]?.apiKey;
977
+ const override = getProviderConfiguration(provider)?.apiKey;
978
+ if (override !== void 0) return override;
979
+ return getRegisteredApiKey(provider);
980
+ }
981
+ /**
982
+ * Provider-specific payload overrides. Returning undefined keeps the
983
+ * upstream-built payload as-is.
984
+ */
985
+ applyProviderPayloadOverrides(payload, model) {
986
+ if (model.api !== "openai-responses" && model.api !== "azure-openai-responses") return;
987
+ if (getProviderConfiguration(model.provider)?.storeResponses !== true) return;
988
+ return {
989
+ ...payload,
990
+ store: true
991
+ };
910
992
  }
911
993
  buildSystemPrompt(roleName) {
912
994
  const parts = [this.config.systemPrompt];
@@ -943,35 +1025,42 @@ var Session = class {
943
1025
  names.add(toolDef.name);
944
1026
  }
945
1027
  }
946
- createBuiltinTools(env, commands, tools, role, model) {
1028
+ createBuiltinTools(env, tools, role, model, thinkingLevel) {
947
1029
  return createTools(env, {
948
1030
  roles: this.config.roles,
949
- task: (params, signal) => this.runTaskForTool(params, commands, tools, role, model, signal)
1031
+ task: (params, signal) => this.runTaskForTool(params, tools, role, model, thinkingLevel, signal)
950
1032
  });
951
1033
  }
952
1034
  async withScopedRuntime(options, fn) {
953
1035
  const customTools = this.createCustomTools([...this.agentTools, ...options.tools]);
954
- const scopedEnv = await createScopedEnv(this.env, options.commands);
955
1036
  const previousTools = this.harness.state.tools;
956
1037
  const previousModel = this.harness.state.model;
957
1038
  const previousSystemPrompt = this.harness.state.systemPrompt;
958
- this.harness.state.model = this.resolveModelForCall(options.model, options.role, options.callSite);
1039
+ const previousThinkingLevel = this.harness.state.thinkingLevel;
1040
+ const resolvedModel = this.resolveModelForCall(options.model, options.role, options.callSite);
1041
+ this.harness.state.model = resolvedModel;
959
1042
  this.harness.state.systemPrompt = this.buildSystemPrompt(options.role);
960
- this.harness.state.tools = [...this.createBuiltinTools(scopedEnv, options.commands, options.tools, options.role, options.model), ...customTools];
1043
+ this.harness.state.thinkingLevel = this.resolveThinkingLevelForCall(options.thinkingLevel, options.role);
1044
+ this.harness.state.tools = [
1045
+ ...this.createBuiltinTools(this.env, options.tools, options.role, options.model, options.thinkingLevel),
1046
+ ...customTools,
1047
+ ...options.extraTools ?? []
1048
+ ];
961
1049
  try {
962
- return await fn();
1050
+ return await fn({ resolvedModel });
963
1051
  } finally {
964
1052
  this.harness.state.tools = previousTools;
965
1053
  this.harness.state.model = previousModel;
966
1054
  this.harness.state.systemPrompt = previousSystemPrompt;
1055
+ this.harness.state.thinkingLevel = previousThinkingLevel;
967
1056
  }
968
1057
  }
969
- async runTaskForTool(params, commands, tools, inheritedRole, inheritedModel, signal) {
1058
+ async runTaskForTool(params, tools, inheritedRole, inheritedModel, inheritedThinkingLevel, signal) {
970
1059
  const result = await this.runTask(params.prompt, {
971
1060
  role: params.role ?? inheritedRole,
972
1061
  inheritedModel,
1062
+ inheritedThinkingLevel,
973
1063
  cwd: params.cwd,
974
- commands,
975
1064
  tools
976
1065
  }, signal);
977
1066
  return {
@@ -992,7 +1081,7 @@ var Session = class {
992
1081
  this.assertActive();
993
1082
  if (!this.createTaskSession) throw new Error("[flue] This session cannot create task sessions.");
994
1083
  if (this.taskDepth >= MAX_TASK_DEPTH) throw new Error(`[flue] Maximum task depth (${MAX_TASK_DEPTH}) exceeded.`);
995
- if (signal?.aborted) throw new Error("Operation aborted");
1084
+ if (signal?.aborted) throw abortErrorFor(signal);
996
1085
  const taskId = crypto.randomUUID();
997
1086
  const requestedRole = options?.role ?? this.sessionRole ?? this.config.role;
998
1087
  let child;
@@ -1007,14 +1096,12 @@ var Session = class {
1007
1096
  });
1008
1097
  try {
1009
1098
  const role = this.resolveEffectiveRole(options?.role);
1010
- const commands = mergeCommands(this.agentCommands, options?.commands);
1011
1099
  child = await this.createTaskSession({
1012
1100
  parentSessionId: this.id,
1013
1101
  taskId,
1014
1102
  parentEnv: this.env,
1015
1103
  cwd: options?.cwd,
1016
1104
  role,
1017
- commands,
1018
1105
  depth: this.taskDepth + 1
1019
1106
  });
1020
1107
  await this.recordTaskSession(child.id, child.storageKey, taskId);
@@ -1022,15 +1109,18 @@ var Session = class {
1022
1109
  if (signal) {
1023
1110
  abortListener = () => child?.abort();
1024
1111
  signal.addEventListener("abort", abortListener, { once: true });
1025
- if (signal.aborted) throw new Error("Operation aborted");
1026
1112
  }
1027
- const schema = options?.result;
1113
+ const schema = resolveSchemaOption(options);
1028
1114
  const roleModel = resolveRoleModel(this.config.roles, role);
1115
+ const roleThinkingLevel = resolveRoleThinkingLevel(this.config.roles, role);
1029
1116
  const childOptions = {
1030
1117
  model: options?.model ?? (roleModel ? void 0 : options?.inheritedModel),
1031
- tools: options?.tools
1118
+ thinkingLevel: options?.thinkingLevel ?? (roleThinkingLevel !== void 0 ? void 0 : options?.inheritedThinkingLevel),
1119
+ tools: options?.tools,
1120
+ images: options?.images,
1121
+ signal
1032
1122
  };
1033
- if (schema) childOptions.result = schema;
1123
+ if (schema) childOptions.schema = schema;
1034
1124
  const output = await child.prompt(text, childOptions);
1035
1125
  const taskResult = {
1036
1126
  output,
@@ -1057,10 +1147,6 @@ var Session = class {
1057
1147
  result: getErrorMessage(error),
1058
1148
  parentSessionId: this.id
1059
1149
  });
1060
- this.emit({
1061
- type: "error",
1062
- error: getErrorMessage(error)
1063
- });
1064
1150
  throw error;
1065
1151
  } finally {
1066
1152
  if (signal && abortListener) signal.removeEventListener("abort", abortListener);
@@ -1070,17 +1156,21 @@ var Session = class {
1070
1156
  }
1071
1157
  }
1072
1158
  }
1073
- async runOperation(operation, fn) {
1159
+ async runOperation(operation, signal, fn) {
1074
1160
  return this.runExclusive(operation, async () => {
1161
+ if (signal?.aborted) throw abortErrorFor(signal);
1162
+ const onAbort = () => {
1163
+ this.harness.abort();
1164
+ this.compactionAbortController?.abort(signal?.reason);
1165
+ for (const task of this.activeTasks) task.abort();
1166
+ };
1167
+ signal?.addEventListener("abort", onAbort, { once: true });
1075
1168
  try {
1076
1169
  return await fn();
1077
1170
  } catch (error) {
1078
- this.emit({
1079
- type: "error",
1080
- error: getErrorMessage(error)
1081
- });
1082
- throw error;
1171
+ throw signal?.aborted ? abortErrorFor(signal) : error;
1083
1172
  } finally {
1173
+ signal?.removeEventListener("abort", onAbort);
1084
1174
  this.emit({ type: "idle" });
1085
1175
  }
1086
1176
  });
@@ -1104,15 +1194,70 @@ var Session = class {
1104
1194
  assertActive() {
1105
1195
  if (this.deleted) throw new Error(`[flue] Session "${this.id}" has been deleted.`);
1106
1196
  }
1107
- createShellMessage(command, result, options) {
1108
- return {
1197
+ /**
1198
+ * Append the three-message conversational triple that represents a
1199
+ * `session.shell()` call in the message history:
1200
+ *
1201
+ * 1. user — out-of-band request to run the command
1202
+ * 2. assistant — synthetic turn whose content is a single bash
1203
+ * tool_use block (matching the shape pi-ai's
1204
+ * providers produce when the LLM itself calls bash)
1205
+ * 3. toolResult — the bash output, keyed to the same toolCallId
1206
+ *
1207
+ * This makes a session.shell() call indistinguishable from an
1208
+ * LLM-issued bash tool call when later turns read the transcript.
1209
+ */
1210
+ async appendShellTriple(toolCallId, args, toolResult, isError) {
1211
+ const timestamp = Date.now();
1212
+ const userMessage = {
1109
1213
  role: "user",
1214
+ content: `Run this shell command:\n\n\`\`\`bash\n${args.command}\n\`\`\``,
1215
+ timestamp
1216
+ };
1217
+ const assistantMessage = {
1218
+ role: "assistant",
1110
1219
  content: [{
1111
- type: "text",
1112
- text: formatShellHistory(command, result, options?.cwd ? `\ncwd: ${options.cwd}` : "", options?.env ? `\nenv: ${Object.keys(options.env).sort().join(", ")}` : "")
1220
+ type: "toolCall",
1221
+ id: toolCallId,
1222
+ name: "bash",
1223
+ arguments: args
1113
1224
  }],
1114
- timestamp: Date.now()
1225
+ api: "flue-shell",
1226
+ provider: "flue",
1227
+ model: "",
1228
+ usage: {
1229
+ input: 0,
1230
+ output: 0,
1231
+ cacheRead: 0,
1232
+ cacheWrite: 0,
1233
+ totalTokens: 0,
1234
+ cost: {
1235
+ input: 0,
1236
+ output: 0,
1237
+ cacheRead: 0,
1238
+ cacheWrite: 0,
1239
+ total: 0
1240
+ }
1241
+ },
1242
+ stopReason: "toolUse",
1243
+ timestamp
1244
+ };
1245
+ const toolResultMessage = {
1246
+ role: "toolResult",
1247
+ toolCallId,
1248
+ toolName: "bash",
1249
+ content: toolResult.content,
1250
+ details: toolResult.details,
1251
+ isError,
1252
+ timestamp
1115
1253
  };
1254
+ this.history.appendMessages([
1255
+ userMessage,
1256
+ assistantMessage,
1257
+ toolResultMessage
1258
+ ], "shell");
1259
+ this.harness.state.messages = this.history.buildContext();
1260
+ await this.save();
1116
1261
  }
1117
1262
  async syncHarnessMessagesSince(index, source) {
1118
1263
  const messages = this.harness.state.messages.slice(index);
@@ -1158,7 +1303,11 @@ var Session = class {
1158
1303
  this.history.removeLeafMessage(lastMsg);
1159
1304
  await this.save();
1160
1305
  }
1161
- await this.runCompaction("overflow", true);
1306
+ try {
1307
+ await this.runCompaction("overflow", true);
1308
+ } finally {
1309
+ this.overflowRecoveryAttempted = false;
1310
+ }
1162
1311
  return;
1163
1312
  }
1164
1313
  if (assistantMessage.stopReason === "error") return;
@@ -1168,6 +1317,13 @@ var Session = class {
1168
1317
  await this.runCompaction("threshold", false);
1169
1318
  }
1170
1319
  }
1320
+ /**
1321
+ * Runs a compaction pass. The summarization cost (1–2 internal LLM
1322
+ * calls) is persisted on the resulting `CompactionEntry.usage`, which
1323
+ * `aggregateUsageSince` later folds into the surrounding call's
1324
+ * `response.usage` — so users see the true cost of the call that
1325
+ * triggered compaction.
1326
+ */
1171
1327
  async runCompaction(reason, willRetry) {
1172
1328
  this.compactionAbortController = new AbortController();
1173
1329
  const messagesBefore = this.harness.state.messages.length;
@@ -1203,7 +1359,8 @@ var Session = class {
1203
1359
  summary: result.summary,
1204
1360
  firstKeptEntryId: firstKeptEntry.id,
1205
1361
  tokensBefore: result.tokensBefore,
1206
- details: result.details
1362
+ details: result.details,
1363
+ usage: result.usage
1207
1364
  });
1208
1365
  this.harness.state.messages = this.history.buildContext();
1209
1366
  const messagesAfter = this.harness.state.messages.length;
@@ -1235,6 +1392,27 @@ var Session = class {
1235
1392
  const errorMsg = this.harness.state.errorMessage;
1236
1393
  if (errorMsg) throw new Error(`[flue] ${context} failed: ${errorMsg}`);
1237
1394
  }
1395
+ /**
1396
+ * Sum the usage of every entry the call appended to the active path
1397
+ * after `beforeLeafId`: assistant messages contribute their per-turn
1398
+ * `usage` (provider-reported, normalized through `fromProviderUsage`),
1399
+ * and compaction entries contribute the aggregated cost of the
1400
+ * summarization call(s) they dispatched. Returns zeros when nothing
1401
+ * was appended (defensive — `throwIfError` normally fires first).
1402
+ *
1403
+ * Walks the durable, parent-linked active path rather than the volatile
1404
+ * flat `harness.state.messages` array, so the result is robust to
1405
+ * mid-call mutations (e.g. overflow recovery removing a failed
1406
+ * assistant turn before retry).
1407
+ */
1408
+ aggregateUsageSince(beforeLeafId) {
1409
+ let totals = emptyUsage();
1410
+ for (const entry of this.history.getActivePathSince(beforeLeafId)) if (entry.type === "message" && entry.message.role === "assistant") {
1411
+ const usage = fromProviderUsage(entry.message.usage);
1412
+ if (usage) totals = addUsage(totals, usage);
1413
+ } else if (entry.type === "compaction" && entry.usage) totals = addUsage(totals, entry.usage);
1414
+ return totals;
1415
+ }
1238
1416
  getAssistantText() {
1239
1417
  const messages = this.harness.state.messages;
1240
1418
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -1255,21 +1433,80 @@ var Session = class {
1255
1433
  if (entry.type === "message" && entry.message.role === "assistant") return entry.id;
1256
1434
  }
1257
1435
  }
1258
- async extractResultWithRetry(schema) {
1259
- const text = this.getAssistantText();
1260
- try {
1261
- return extractResult(text, schema);
1262
- } catch (err) {
1263
- if (!(err instanceof ResultExtractionError)) throw err;
1264
- if (!err.message.includes("RESULT_START")) throw err;
1265
- const followUpPrompt = buildResultExtractionPrompt(schema);
1266
- const beforeRetry = this.harness.state.messages.length;
1267
- await this.harness.prompt(followUpPrompt);
1436
+ /**
1437
+ * Shared body of `prompt()` and `skill()`: scope the runtime, optionally
1438
+ * inject the result-tool pair, drive the harness, and aggregate usage.
1439
+ *
1440
+ * Returns `PromptResultResponse<T>` when `schema` is set, else `PromptResponse`.
1441
+ */
1442
+ async runPromptCall(args) {
1443
+ const role = this.resolveEffectiveRole(args.role);
1444
+ const resultBundle = args.schema ? createResultTools(args.schema) : void 0;
1445
+ return this.withScopedRuntime({
1446
+ tools: args.tools ?? [],
1447
+ role,
1448
+ model: args.model,
1449
+ thinkingLevel: args.thinkingLevel,
1450
+ callSite: args.callSite,
1451
+ extraTools: resultBundle?.tools
1452
+ }, async ({ resolvedModel }) => {
1453
+ const beforeLength = this.harness.state.messages.length;
1454
+ const beforeLeafId = this.history.getLeafId();
1455
+ const model = { id: resolvedModel.id };
1456
+ if (resultBundle) {
1457
+ const result = await this.runWithResultTools(args.promptText, resultBundle, beforeLength, args.source, args.errorLabel, args.signal, args.images);
1458
+ return {
1459
+ data: result,
1460
+ result,
1461
+ usage: this.aggregateUsageSince(beforeLeafId),
1462
+ model
1463
+ };
1464
+ }
1465
+ await this.harness.prompt(args.promptText, args.images);
1268
1466
  await this.harness.waitForIdle();
1269
- await this.syncHarnessMessagesSince(beforeRetry, "retry");
1467
+ await this.syncHarnessMessagesSince(beforeLength, args.source);
1270
1468
  await this.checkLatestAssistantForCompaction();
1271
- return extractResult(this.getAssistantText(), schema);
1469
+ this.throwIfError(args.errorLabel);
1470
+ return {
1471
+ text: this.getAssistantText(),
1472
+ usage: this.aggregateUsageSince(beforeLeafId),
1473
+ model
1474
+ };
1475
+ });
1476
+ }
1477
+ /**
1478
+ * Drive the harness through one or more turns until the LLM either calls
1479
+ * the `finish` tool (success) or the `give_up` tool (typed error).
1480
+ *
1481
+ * If a turn ends with neither tool called, we send a brief reminder and
1482
+ * loop. There is no retry cap from the SDK's perspective: the model has a
1483
+ * clear escape hatch via `give_up`, the user has cancellation via `signal`,
1484
+ * and pi-agent-core has its own iteration limits as the final ceiling.
1485
+ * `MAX_FOLLOWUPS` is a defense-in-depth ceiling against pathological loops.
1486
+ *
1487
+ * `beforeLength` is the harness-message-array length sampled by the caller
1488
+ * *before* the very first prompt; we keep advancing it across iterations so
1489
+ * `syncHarnessMessagesSince` only copies newly-produced messages each turn.
1490
+ */
1491
+ async runWithResultTools(initialPrompt, bundle, beforeLength, source, errorLabel, signal, initialImages) {
1492
+ let nextPrompt = initialPrompt;
1493
+ let cursor = beforeLength;
1494
+ const MAX_FOLLOWUPS = 32;
1495
+ for (let attempt = 0; attempt <= MAX_FOLLOWUPS; attempt++) {
1496
+ if (signal.aborted) throw abortErrorFor(signal);
1497
+ await this.harness.prompt(nextPrompt, attempt === 0 ? initialImages : void 0);
1498
+ await this.harness.waitForIdle();
1499
+ await this.syncHarnessMessagesSince(cursor, source);
1500
+ cursor = this.harness.state.messages.length;
1501
+ await this.checkLatestAssistantForCompaction();
1502
+ this.throwIfError(errorLabel);
1503
+ const outcome = bundle.getOutcome();
1504
+ if (outcome.type === "finished") return outcome.value;
1505
+ if (outcome.type === "gave_up") throw new ResultUnavailableError(outcome.reason, this.getAssistantText());
1506
+ nextPrompt = buildResultFollowUpPrompt();
1507
+ source = "retry";
1272
1508
  }
1509
+ throw new ResultUnavailableError(`Agent did not call \`finish\` or \`give_up\` after ${MAX_FOLLOWUPS + 1} attempts.`, this.getAssistantText());
1273
1510
  }
1274
1511
  };
1275
1512
  function normalizePath(p) {
@@ -1290,20 +1527,9 @@ async function deleteSessionTree(store, storageKey, seen = /* @__PURE__ */ new S
1290
1527
  for (const task of taskSessions) if (typeof task?.storageKey === "string") await deleteSessionTree(store, task.storageKey, seen);
1291
1528
  await store.delete(storageKey);
1292
1529
  }
1293
- function formatShellHistory(command, result, cwdLine, envLine) {
1294
- const sections = [`<shell_command>\n$ ${command}${cwdLine}${envLine}\n</shell_command>`, `<shell_result exitCode="${result.exitCode}">`];
1295
- if (result.stdout) sections.push(`<stdout>\n${result.stdout}\n</stdout>`);
1296
- if (result.stderr) sections.push(`<stderr>\n${result.stderr}\n</stderr>`);
1297
- sections.push("</shell_result>");
1298
- return truncateShellHistory(sections.join("\n"));
1299
- }
1300
- function truncateShellHistory(text) {
1301
- if (text.length <= MAX_SHELL_HISTORY_CHARS) return text;
1302
- return `[Shell output truncated: ${text.length - MAX_SHELL_HISTORY_CHARS} leading characters omitted]\n` + text.slice(text.length - MAX_SHELL_HISTORY_CHARS);
1303
- }
1304
1530
  function getErrorMessage(error) {
1305
1531
  return error instanceof Error ? error.message : String(error);
1306
1532
  }
1307
1533
 
1308
1534
  //#endregion
1309
- export { assertRoleExists as a, normalizePath as i, Session as n, createScopedEnv as o, deleteSessionTree as r, mergeCommands as s, InMemorySessionStore as t };
1535
+ export { assertRoleExists as a, normalizePath as i, Session as n, deleteSessionTree as r, InMemorySessionStore as t };