@flue/sdk 0.2.0 → 0.3.1

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,4 +1,4 @@
1
- import { i as loadSkillByPath, n as createTools, r as discoverSessionContext, t as BUILTIN_TOOL_NAMES } from "./agent-BYG0nVbQ.mjs";
1
+ import { i as loadSkillByPath, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-BB4lwAd5.mjs";
2
2
  import { completeSimple, isContextOverflow } from "@mariozechner/pi-ai";
3
3
  import { Agent } from "@mariozechner/pi-agent-core";
4
4
  import { toJsonSchema } from "@valibot/to-json-schema";
@@ -384,16 +384,6 @@ async function compact(preparation, model, apiKey, signal) {
384
384
  }
385
385
  };
386
386
  }
387
- function buildCompactedMessages(messages, result) {
388
- return [{
389
- role: "user",
390
- content: [{
391
- type: "text",
392
- text: `[Context Summary]\n\n${result.summary}`
393
- }],
394
- timestamp: Date.now()
395
- }, ...messages.slice(result.firstKeptIndex)];
396
- }
397
387
 
398
388
  //#endregion
399
389
  //#region src/result.ts
@@ -478,9 +468,206 @@ var ResultExtractionError = class extends Error {
478
468
  }
479
469
  };
480
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
+ //#endregion
488
+ //#region src/roles.ts
489
+ function assertRoleExists(roles, roleName) {
490
+ if (!roleName) return;
491
+ if (roles[roleName]) return;
492
+ const available = Object.keys(roles);
493
+ 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 under \`.flue/roles/\`.`);
495
+ }
496
+ function resolveEffectiveRole(options) {
497
+ const role = options.callRole ?? options.sessionRole ?? options.agentRole;
498
+ assertRoleExists(options.roles, role);
499
+ return role;
500
+ }
501
+ function resolveRoleModel(roles, roleName) {
502
+ assertRoleExists(roles, roleName);
503
+ return roleName ? roles[roleName]?.model : void 0;
504
+ }
505
+
506
+ //#endregion
507
+ //#region src/session-history.ts
508
+ var SessionHistory = class SessionHistory {
509
+ entries;
510
+ byId;
511
+ leafId;
512
+ constructor(entries, leafId) {
513
+ this.entries = [...entries];
514
+ this.leafId = leafId;
515
+ this.byId = new Map(this.entries.map((entry) => [entry.id, entry]));
516
+ }
517
+ static empty() {
518
+ return new SessionHistory([], null);
519
+ }
520
+ static fromData(data) {
521
+ if (!data) return SessionHistory.empty();
522
+ return new SessionHistory(data.entries, data.leafId);
523
+ }
524
+ getLeafId() {
525
+ return this.leafId;
526
+ }
527
+ getActivePath() {
528
+ const path = [];
529
+ let current = this.leafId ? this.byId.get(this.leafId) : void 0;
530
+ while (current) {
531
+ path.push(current);
532
+ current = current.parentId ? this.byId.get(current.parentId) : void 0;
533
+ }
534
+ return path.reverse();
535
+ }
536
+ buildContextEntries() {
537
+ const path = this.getActivePath();
538
+ const latestCompactionIndex = findLatestCompactionIndex(path);
539
+ if (latestCompactionIndex === -1) return pathToContextEntries(path);
540
+ const compaction = path[latestCompactionIndex];
541
+ const firstKeptIndex = path.findIndex((entry) => entry.id === compaction.firstKeptEntryId);
542
+ const keptStart = firstKeptIndex >= 0 ? firstKeptIndex : latestCompactionIndex + 1;
543
+ const context = [{
544
+ message: createContextSummaryMessage(compaction.summary, compaction.timestamp),
545
+ entry: compaction
546
+ }];
547
+ context.push(...pathToContextEntries(path.slice(keptStart, latestCompactionIndex)));
548
+ context.push(...pathToContextEntries(path.slice(latestCompactionIndex + 1)));
549
+ return context;
550
+ }
551
+ buildContext() {
552
+ return this.buildContextEntries().map((entry) => entry.message);
553
+ }
554
+ getLatestCompaction() {
555
+ const path = this.getActivePath();
556
+ for (let i = path.length - 1; i >= 0; i--) {
557
+ const entry = path[i];
558
+ if (entry.type === "compaction") return entry;
559
+ }
560
+ }
561
+ appendMessage(message, source) {
562
+ const entry = {
563
+ type: "message",
564
+ id: generateEntryId(this.byId),
565
+ parentId: this.leafId,
566
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
567
+ message,
568
+ source
569
+ };
570
+ this.appendEntry(entry);
571
+ return entry.id;
572
+ }
573
+ appendMessages(messages, source) {
574
+ return messages.map((message) => this.appendMessage(message, source));
575
+ }
576
+ appendCompaction(input) {
577
+ if (!this.byId.has(input.firstKeptEntryId)) throw new Error(`[flue] Cannot compact: entry "${input.firstKeptEntryId}" does not exist.`);
578
+ const entry = {
579
+ type: "compaction",
580
+ id: generateEntryId(this.byId),
581
+ parentId: this.leafId,
582
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
583
+ summary: input.summary,
584
+ firstKeptEntryId: input.firstKeptEntryId,
585
+ tokensBefore: input.tokensBefore,
586
+ details: input.details
587
+ };
588
+ this.appendEntry(entry);
589
+ return entry.id;
590
+ }
591
+ appendBranchSummary(summary, fromId, details) {
592
+ const entry = {
593
+ type: "branch_summary",
594
+ id: generateEntryId(this.byId),
595
+ parentId: this.leafId,
596
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
597
+ fromId,
598
+ summary,
599
+ details
600
+ };
601
+ this.appendEntry(entry);
602
+ return entry.id;
603
+ }
604
+ removeLeafMessage(message) {
605
+ if (!this.leafId) return false;
606
+ const leaf = this.byId.get(this.leafId);
607
+ if (!leaf || leaf.type !== "message" || leaf.message !== message) return false;
608
+ this.byId.delete(leaf.id);
609
+ this.entries = this.entries.filter((entry) => entry.id !== leaf.id);
610
+ this.leafId = leaf.parentId;
611
+ return true;
612
+ }
613
+ toData(metadata, createdAt, updatedAt) {
614
+ return {
615
+ version: 2,
616
+ entries: [...this.entries],
617
+ leafId: this.leafId,
618
+ metadata,
619
+ createdAt,
620
+ updatedAt
621
+ };
622
+ }
623
+ appendEntry(entry) {
624
+ this.entries.push(entry);
625
+ this.byId.set(entry.id, entry);
626
+ this.leafId = entry.id;
627
+ }
628
+ };
629
+ function pathToContextEntries(path) {
630
+ const context = [];
631
+ for (const entry of path) if (entry.type === "message") context.push({
632
+ message: entry.message,
633
+ entry
634
+ });
635
+ else if (entry.type === "branch_summary") context.push({
636
+ message: createUserContextMessage(`[Branch Summary]\n\n${entry.summary}`, entry.timestamp),
637
+ entry
638
+ });
639
+ return context;
640
+ }
641
+ function findLatestCompactionIndex(path) {
642
+ for (let i = path.length - 1; i >= 0; i--) if (path[i].type === "compaction") return i;
643
+ return -1;
644
+ }
645
+ function createContextSummaryMessage(summary, timestamp) {
646
+ return createUserContextMessage(summary.startsWith("[Context Summary]") ? summary : `[Context Summary]\n\n${summary}`, timestamp);
647
+ }
648
+ function createUserContextMessage(text, timestamp) {
649
+ return {
650
+ role: "user",
651
+ content: [{
652
+ type: "text",
653
+ text
654
+ }],
655
+ timestamp: new Date(timestamp).getTime()
656
+ };
657
+ }
658
+ function generateEntryId(byId) {
659
+ for (let i = 0; i < 100; i++) {
660
+ const id = crypto.randomUUID().slice(0, 8);
661
+ if (!byId.has(id)) return id;
662
+ }
663
+ return crypto.randomUUID();
664
+ }
665
+
481
666
  //#endregion
482
667
  //#region src/session.ts
483
668
  /** Internal session implementation. Not exported publicly — wrapped by FlueSession. */
669
+ const MAX_SHELL_HISTORY_CHARS = 50 * 1024;
670
+ const MAX_TASK_DEPTH = 4;
484
671
  /** In-memory session store. Sessions persist for the lifetime of the process. */
485
672
  var InMemorySessionStore = class {
486
673
  store = /* @__PURE__ */ new Map();
@@ -494,66 +681,83 @@ var InMemorySessionStore = class {
494
681
  this.store.delete(id);
495
682
  }
496
683
  };
497
- var Session = class Session {
684
+ var Session = class {
498
685
  id;
499
686
  metadata;
500
- agent;
687
+ get role() {
688
+ return this.sessionRole;
689
+ }
690
+ harness;
691
+ storageKey;
501
692
  config;
502
693
  env;
503
694
  store;
695
+ history;
504
696
  createdAt;
505
697
  compactionSettings;
506
- lastCompaction;
507
698
  overflowRecoveryAttempted = false;
508
699
  compactionAbortController;
509
700
  eventCallback;
510
- builtinTools;
511
- sessionCommands;
512
- constructor(id, config, env, store, existingData, onAgentEvent, sessionCommands) {
513
- this.id = id;
514
- this.config = config;
515
- this.env = env;
516
- this.store = store;
517
- this.sessionCommands = sessionCommands ?? [];
518
- this.metadata = existingData?.metadata ?? {};
519
- this.createdAt = existingData?.createdAt;
520
- this.lastCompaction = existingData?.lastCompaction;
521
- const cc = config.compaction;
701
+ agentCommands;
702
+ agentTools;
703
+ deleted = false;
704
+ activeOperation;
705
+ activeTasks = /* @__PURE__ */ new Set();
706
+ sessionRole;
707
+ taskDepth;
708
+ createTaskSession;
709
+ onDelete;
710
+ constructor(options) {
711
+ this.id = options.id;
712
+ this.storageKey = options.storageKey;
713
+ this.config = options.config;
714
+ this.env = options.env;
715
+ this.store = options.store;
716
+ this.agentCommands = options.agentCommands ?? [];
717
+ this.agentTools = options.agentTools ?? [];
718
+ this.sessionRole = options.sessionRole;
719
+ this.taskDepth = options.taskDepth ?? 0;
720
+ this.createTaskSession = options.createTaskSession;
721
+ this.onDelete = options.onDelete;
722
+ this.metadata = options.existingData?.metadata ?? {};
723
+ this.createdAt = options.existingData?.createdAt;
724
+ this.history = SessionHistory.fromData(options.existingData);
725
+ const cc = this.config.compaction;
522
726
  this.compactionSettings = {
523
727
  enabled: cc?.enabled ?? DEFAULT_COMPACTION_SETTINGS.enabled,
524
728
  reserveTokens: cc?.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens,
525
729
  keepRecentTokens: cc?.keepRecentTokens ?? DEFAULT_COMPACTION_SETTINGS.keepRecentTokens
526
730
  };
527
- const systemPrompt = config.systemPrompt;
528
- const tools = createTools(env);
529
- this.builtinTools = tools;
530
- const previousMessages = existingData?.messages ?? [];
531
- this.agent = new Agent({
731
+ const systemPrompt = this.config.systemPrompt;
732
+ assertRoleExists(this.config.roles, this.config.role);
733
+ assertRoleExists(this.config.roles, this.sessionRole);
734
+ const tools = [...this.createBuiltinTools(this.env, this.agentCommands, []), ...this.createCustomTools(this.agentTools)];
735
+ const previousMessages = this.history.buildContext();
736
+ this.harness = new Agent({
532
737
  initialState: {
533
738
  systemPrompt,
534
- model: config.model,
739
+ model: this.config.model,
535
740
  tools,
536
741
  messages: previousMessages
537
742
  },
538
743
  toolExecution: "parallel"
539
744
  });
540
- this.eventCallback = onAgentEvent;
541
- const emit = onAgentEvent;
542
- this.agent.subscribe(async (event) => {
745
+ this.eventCallback = options.onAgentEvent;
746
+ this.harness.subscribe(async (event) => {
543
747
  switch (event.type) {
544
748
  case "agent_start":
545
- emit?.({ type: "agent_start" });
749
+ this.emit({ type: "agent_start" });
546
750
  break;
547
751
  case "message_update": {
548
752
  const aEvent = event.assistantMessageEvent;
549
- if (aEvent.type === "text_delta") emit?.({
753
+ if (aEvent.type === "text_delta") this.emit({
550
754
  type: "text_delta",
551
755
  text: aEvent.delta
552
756
  });
553
757
  break;
554
758
  }
555
759
  case "tool_execution_start":
556
- emit?.({
760
+ this.emit({
557
761
  type: "tool_start",
558
762
  toolName: event.toolName,
559
763
  toolCallId: event.toolCallId,
@@ -561,7 +765,7 @@ var Session = class Session {
561
765
  });
562
766
  break;
563
767
  case "tool_execution_end":
564
- emit?.({
768
+ this.emit({
565
769
  type: "tool_end",
566
770
  toolName: event.toolName,
567
771
  toolCallId: event.toolCallId,
@@ -570,159 +774,124 @@ var Session = class Session {
570
774
  });
571
775
  break;
572
776
  case "turn_end":
573
- emit?.({ type: "turn_end" });
777
+ this.emit({ type: "turn_end" });
574
778
  break;
575
- case "agent_end": {
576
- const messages = this.agent.state.messages;
577
- const lastMsg = messages[messages.length - 1];
578
- if (lastMsg?.role === "assistant") await this.checkCompaction(lastMsg);
579
- emit?.({ type: "done" });
580
- break;
581
- }
779
+ case "agent_end": break;
582
780
  }
583
781
  });
584
782
  }
585
783
  async prompt(text, options) {
586
- this.assertRoleExists(options?.role);
587
- this.resolveModelForCall(options?.model, options?.role);
588
- const promptWithRole = this.injectRoleInstructions(text, options?.role);
589
- const schema = options?.result;
590
- const fullPrompt = buildPromptText(promptWithRole, schema);
591
- const effectiveCommands = this.mergeCommands(options?.commands);
592
- if (effectiveCommands.length > 0) this.assertCommandSupport(effectiveCommands);
593
- const registeredCommandNames = this.registerCommands(effectiveCommands);
594
- const registeredToolNames = options?.tools ? this.registerCustomTools(options.tools) : [];
595
- try {
596
- await this.agent.prompt(fullPrompt);
597
- await this.agent.waitForIdle();
598
- this.throwIfError("prompt");
599
- await this.save();
600
- if (schema) return this.extractResultWithRetry(schema);
601
- return { text: this.getAssistantText() };
602
- } finally {
603
- this.unregisterCommands(registeredCommandNames);
604
- if (registeredToolNames.length > 0) this.unregisterCustomTools();
605
- }
784
+ return this.runOperation("prompt", async () => {
785
+ const role = this.resolveEffectiveRole(options?.role);
786
+ const schema = options?.result;
787
+ const fullPrompt = buildPromptText(text, schema);
788
+ const effectiveCommands = mergeCommands(this.agentCommands, options?.commands);
789
+ return this.withScopedRuntime({
790
+ commands: effectiveCommands,
791
+ tools: options?.tools ?? [],
792
+ role,
793
+ model: options?.model,
794
+ callSite: "this prompt() call"
795
+ }, async () => {
796
+ const beforeLength = this.harness.state.messages.length;
797
+ await this.harness.prompt(fullPrompt);
798
+ await this.harness.waitForIdle();
799
+ await this.syncHarnessMessagesSince(beforeLength, "prompt");
800
+ await this.checkLatestAssistantForCompaction();
801
+ this.throwIfError("prompt");
802
+ if (schema) return this.extractResultWithRetry(schema);
803
+ return { text: this.getAssistantText() };
804
+ });
805
+ });
606
806
  }
607
807
  async skill(name, options) {
608
- this.assertRoleExists(options?.role);
609
- let registeredSkill = this.config.skills[name];
610
- if (!registeredSkill && (name.includes("/") || /\.(md|markdown)$/i.test(name))) {
611
- const loaded = await loadSkillByPath(this.env, this.env.cwd, name);
612
- if (loaded) registeredSkill = loaded;
613
- }
614
- if (!registeredSkill) {
615
- const available = Object.keys(this.config.skills).join(", ") || "(none)";
616
- throw new Error(`Skill "${name}" not registered. Available: ${available}. Skills can also be referenced by relative path under .agents/skills/ (e.g. "triage/reproduce.md").`);
617
- }
618
- this.resolveModelForCall(options?.model, options?.role);
619
- const schema = options?.result;
620
- const skillPrompt = buildSkillPrompt(registeredSkill.instructions, options?.args, schema);
621
- const promptWithRole = this.injectRoleInstructions(skillPrompt, options?.role);
622
- const effectiveCommands = this.mergeCommands(options?.commands);
623
- if (effectiveCommands.length > 0) this.assertCommandSupport(effectiveCommands);
624
- const registeredCommandNames = this.registerCommands(effectiveCommands);
625
- const registeredToolNames = options?.tools ? this.registerCustomTools(options.tools) : [];
626
- try {
627
- await this.agent.prompt(promptWithRole);
628
- await this.agent.waitForIdle();
629
- this.throwIfError(`skill("${name}")`);
630
- await this.save();
631
- if (schema) return this.extractResultWithRetry(schema);
632
- return { text: this.getAssistantText() };
633
- } finally {
634
- this.unregisterCommands(registeredCommandNames);
635
- if (registeredToolNames.length > 0) this.unregisterCustomTools();
636
- }
808
+ return this.runOperation("skill", async () => {
809
+ const role = this.resolveEffectiveRole(options?.role);
810
+ let registeredSkill = this.config.skills[name];
811
+ if (!registeredSkill && (name.includes("/") || /\.(md|markdown)$/i.test(name))) {
812
+ const loaded = await loadSkillByPath(this.env, this.env.cwd, name);
813
+ if (loaded) registeredSkill = loaded;
814
+ }
815
+ if (!registeredSkill) {
816
+ const available = Object.keys(this.config.skills).join(", ") || "(none)";
817
+ throw new Error(`Skill "${name}" not registered. Available: ${available}. Skills can also be referenced by relative path under .agents/skills/ (e.g. "triage/reproduce.md").`);
818
+ }
819
+ const schema = options?.result;
820
+ const skillPrompt = buildSkillPrompt(registeredSkill.instructions, options?.args, schema);
821
+ const effectiveCommands = mergeCommands(this.agentCommands, options?.commands);
822
+ return this.withScopedRuntime({
823
+ commands: effectiveCommands,
824
+ tools: options?.tools ?? [],
825
+ role,
826
+ model: options?.model,
827
+ callSite: `this skill("${name}") call`
828
+ }, async () => {
829
+ const beforeLength = this.harness.state.messages.length;
830
+ await this.harness.prompt(skillPrompt);
831
+ await this.harness.waitForIdle();
832
+ await this.syncHarnessMessagesSince(beforeLength, "skill");
833
+ await this.checkLatestAssistantForCompaction();
834
+ this.throwIfError(`skill("${name}")`);
835
+ if (schema) return this.extractResultWithRetry(schema);
836
+ return { text: this.getAssistantText() };
837
+ });
838
+ });
839
+ }
840
+ async task(text, options) {
841
+ return (await this.runTask(text, options, void 0)).output;
637
842
  }
638
843
  async shell(command, options) {
639
- const effectiveCommands = this.mergeCommands(options?.commands);
640
- if (effectiveCommands.length > 0) this.assertCommandSupport(effectiveCommands);
641
- const registeredNames = this.registerCommands(effectiveCommands);
642
- try {
643
- const result = await this.env.exec(command, {
844
+ return this.runOperation("shell", async () => {
845
+ const effectiveCommands = mergeCommands(this.agentCommands, options?.commands);
846
+ const result = await (await createScopedEnv(this.env, effectiveCommands)).exec(command, {
644
847
  env: options?.env,
645
848
  cwd: options?.cwd
646
849
  });
647
- return {
850
+ const shellResult = {
648
851
  stdout: result.stdout,
649
852
  stderr: result.stderr,
650
853
  exitCode: result.exitCode
651
854
  };
652
- } finally {
653
- this.unregisterCommands(registeredNames);
654
- }
655
- }
656
- async task(prompt, options) {
657
- this.assertRoleExists(options?.role);
658
- if (!options?.workspace) throw new Error("[flue] task() requires a workspace option.");
659
- const taskCwd = options.workspace.startsWith("/") ? options.workspace : normalizePath(this.env.cwd + "/" + options.workspace);
660
- function taskResolvePath(p) {
661
- if (p.startsWith("/")) return normalizePath(p);
662
- if (taskCwd === "/") return normalizePath("/" + p);
663
- return normalizePath(taskCwd + "/" + p);
664
- }
665
- const parentEnv = this.env;
666
- const taskEnv = {
667
- exec: (cmd, opts) => parentEnv.exec(cmd, {
668
- cwd: opts?.cwd ?? taskCwd,
669
- env: opts?.env
670
- }),
671
- readFile: (p) => parentEnv.readFile(taskResolvePath(p)),
672
- readFileBuffer: (p) => parentEnv.readFileBuffer(taskResolvePath(p)),
673
- writeFile: (p, c) => parentEnv.writeFile(taskResolvePath(p), c),
674
- stat: (p) => parentEnv.stat(taskResolvePath(p)),
675
- readdir: (p) => parentEnv.readdir(taskResolvePath(p)),
676
- exists: (p) => parentEnv.exists(taskResolvePath(p)),
677
- mkdir: (p, o) => parentEnv.mkdir(taskResolvePath(p), o),
678
- rm: (p, o) => parentEnv.rm(taskResolvePath(p), o),
679
- cwd: taskCwd,
680
- resolvePath: taskResolvePath,
681
- commandSupport: parentEnv.commandSupport,
682
- cleanup: async () => {}
683
- };
684
- const localContext = await discoverSessionContext(taskEnv);
685
- let taskModel = this.config.model;
686
- const taskRole = options?.role ? this.config.roles[options.role] : void 0;
687
- if (taskRole?.model && this.config.resolveModel) taskModel = this.config.resolveModel(taskRole.model);
688
- if (options?.model && this.config.resolveModel) taskModel = this.config.resolveModel(options.model);
689
- const taskConfig = {
690
- systemPrompt: localContext.systemPrompt,
691
- skills: localContext.skills,
692
- roles: this.config.roles,
693
- model: this.requireModel(taskModel, "this task() call"),
694
- resolveModel: this.config.resolveModel,
695
- compaction: this.config.compaction
696
- };
697
- this.eventCallback?.({
698
- type: "task_start",
699
- workspace: taskCwd
855
+ const message = this.createShellMessage(command, shellResult, options);
856
+ this.history.appendMessage(message, "shell");
857
+ this.harness.state.messages = this.history.buildContext();
858
+ await this.save();
859
+ return shellResult;
700
860
  });
701
- const taskStore = new InMemorySessionStore();
702
- const taskSession = new Session(`${this.id}:task:${Date.now()}`, taskConfig, taskEnv, taskStore, null, this.eventCallback);
703
- try {
704
- const promptOpts = { role: options?.role };
705
- if (options?.result) promptOpts.result = options.result;
706
- return await taskSession.prompt(prompt, promptOpts);
707
- } finally {
708
- this.eventCallback?.({ type: "task_end" });
709
- await taskSession.destroy();
710
- }
711
861
  }
712
862
  abort() {
713
- this.agent.abort();
863
+ this.harness.abort();
864
+ this.compactionAbortController?.abort();
865
+ for (const task of this.activeTasks) task.abort();
866
+ }
867
+ close() {
868
+ if (this.deleted) return;
869
+ this.deleted = true;
870
+ this.abort();
871
+ this.onDelete?.();
872
+ }
873
+ async delete() {
874
+ if (this.deleted) return;
875
+ this.deleted = true;
876
+ this.abort();
877
+ await deleteSessionTree(this.store, this.storageKey);
878
+ this.onDelete?.();
714
879
  }
715
- async destroy() {
716
- this.agent.abort();
717
- await this.store.delete(this.id);
718
- await this.env.cleanup();
880
+ resolveEffectiveRole(callRole) {
881
+ return resolveEffectiveRole({
882
+ roles: this.config.roles,
883
+ agentRole: this.config.role,
884
+ sessionRole: this.sessionRole,
885
+ callRole
886
+ });
719
887
  }
720
- /** Precedence: prompt-level > role-level > agent-level default. */
721
- resolveModelForCall(promptModel, roleName) {
888
+ /** Precedence: call-level > role-level > agent-level default. */
889
+ resolveModelForCall(promptModel, roleName, callSite) {
722
890
  let model = this.config.model;
723
- if (roleName && this.config.roles[roleName]?.model && this.config.resolveModel) model = this.config.resolveModel(this.config.roles[roleName].model);
891
+ const roleModel = resolveRoleModel(this.config.roles, roleName);
892
+ if (roleModel && this.config.resolveModel) model = this.config.resolveModel(roleModel);
724
893
  if (promptModel && this.config.resolveModel) model = this.config.resolveModel(promptModel);
725
- this.agent.state.model = this.requireModel(model, "this prompt()/skill()/task() call");
894
+ return this.requireModel(model, callSite);
726
895
  }
727
896
  /**
728
897
  * Throws a clear, actionable error when no model is configured for a call.
@@ -731,64 +900,19 @@ var Session = class Session {
731
900
  */
732
901
  requireModel(model, callSite) {
733
902
  if (model) return model;
734
- throw new Error(`[flue] No model configured for ${callSite}. Pass \`{ model: "provider/model-id" }\` to \`init()\` for a session-wide default, or to this prompt()/skill()/task() call for a one-off override.`);
903
+ throw new Error(`[flue] No model configured for ${callSite}. Pass \`{ model: "provider/model-id" }\` to \`init()\` for an agent-wide default, or to this prompt()/skill() call for a one-off override.`);
735
904
  }
736
- /**
737
- * Throws a clear error when a caller references a role that isn't registered.
738
- * Roles are loaded from `.flue/roles/` at build time. Called eagerly at the top
739
- * of prompt()/skill()/task() so typos surface before any LLM work begins.
740
- */
741
- assertRoleExists(roleName) {
742
- if (!roleName) return;
743
- if (this.config.roles[roleName]) return;
744
- const available = Object.keys(this.config.roles);
745
- const list = available.length > 0 ? available.join(", ") : "(none defined)";
746
- throw new Error(`[flue] Role "${roleName}" not registered. Available roles: ${list}. Define roles as markdown files under \`.flue/roles/\`.`);
747
- }
748
- injectRoleInstructions(text, roleName) {
749
- if (!roleName) return text;
905
+ buildSystemPrompt(roleName) {
906
+ const parts = [this.config.systemPrompt];
907
+ if (!roleName) return parts.join("\n\n");
750
908
  const role = this.config.roles[roleName];
751
- if (!role) return text;
752
- return `<role>\n${role.instructions}\n</role>\n\n${text}`;
753
- }
754
- assertCommandSupport(commands) {
755
- if (commands.length === 0) return;
756
- if (!this.env.commandSupport) throw new Error("[flue] Cannot use commands: this environment does not support command registration. Commands are only available in isolate sandbox mode. Remote sandboxes handle command execution at the platform level.");
757
- }
758
- /**
759
- * Merge session-wide `commands` (from init()) with per-call commands. When
760
- * both define a command with the same name, the per-call entry wins for
761
- * that call.
762
- */
763
- mergeCommands(perCall) {
764
- if (!perCall || perCall.length === 0) return this.sessionCommands;
765
- if (this.sessionCommands.length === 0) return perCall;
766
- const byName = /* @__PURE__ */ new Map();
767
- for (const cmd of this.sessionCommands) byName.set(cmd.name, cmd);
768
- for (const cmd of perCall) byName.set(cmd.name, cmd);
769
- return Array.from(byName.values());
770
- }
771
- registerCommands(commands) {
772
- if (!this.env.commandSupport || commands.length === 0) return [];
773
- const names = [];
774
- for (const cmd of commands) {
775
- this.env.commandSupport.register(cmd);
776
- names.push(cmd.name);
777
- }
778
- return names;
779
- }
780
- unregisterCommands(names) {
781
- if (!this.env.commandSupport || names.length === 0) return;
782
- for (const name of names) this.env.commandSupport.unregister(name);
909
+ if (!role) return parts.join("\n\n");
910
+ parts.push(`<role name="${role.name}">\n${role.instructions}\n</role>`);
911
+ return parts.filter(Boolean).join("\n\n");
783
912
  }
784
- registerCustomTools(tools) {
785
- const names = [];
786
- for (const toolDef of tools) {
787
- if (BUILTIN_TOOL_NAMES.has(toolDef.name)) throw new Error(`[flue] Custom tool "${toolDef.name}" conflicts with a built-in tool. Built-in tools: ${[...BUILTIN_TOOL_NAMES].join(", ")}`);
788
- if (names.includes(toolDef.name)) throw new Error(`[flue] Duplicate custom tool name "${toolDef.name}". Tool names must be unique.`);
789
- names.push(toolDef.name);
790
- }
791
- const agentTools = tools.map((toolDef) => ({
913
+ createCustomTools(tools) {
914
+ this.validateCustomToolNames(tools);
915
+ return tools.map((toolDef) => ({
792
916
  name: toolDef.name,
793
917
  label: toolDef.name,
794
918
  description: toolDef.description,
@@ -798,50 +922,241 @@ var Session = class Session {
798
922
  return {
799
923
  content: [{
800
924
  type: "text",
801
- text: await toolDef.execute(params)
925
+ text: await toolDef.execute(params, signal)
802
926
  }],
803
927
  details: { customTool: toolDef.name }
804
928
  };
805
929
  }
806
930
  }));
807
- this.agent.state.tools = [...this.agent.state.tools, ...agentTools];
808
- return names;
809
931
  }
810
- unregisterCustomTools() {
811
- this.agent.state.tools = [...this.builtinTools];
932
+ validateCustomToolNames(tools) {
933
+ const names = /* @__PURE__ */ new Set();
934
+ for (const toolDef of tools) {
935
+ if (BUILTIN_TOOL_NAMES.has(toolDef.name)) throw new Error(`[flue] Custom tool "${toolDef.name}" conflicts with a built-in tool. Built-in tools: ${[...BUILTIN_TOOL_NAMES].join(", ")}`);
936
+ if (names.has(toolDef.name)) throw new Error(`[flue] Duplicate custom tool name "${toolDef.name}". Tool names must be unique.`);
937
+ names.add(toolDef.name);
938
+ }
939
+ }
940
+ createBuiltinTools(env, commands, tools, role, model) {
941
+ return createTools(env, {
942
+ roles: this.config.roles,
943
+ task: (params, signal) => this.runTaskForTool(params, commands, tools, role, model, signal)
944
+ });
945
+ }
946
+ async withScopedRuntime(options, fn) {
947
+ const customTools = this.createCustomTools([...this.agentTools, ...options.tools]);
948
+ const scopedEnv = await createScopedEnv(this.env, options.commands);
949
+ const previousTools = this.harness.state.tools;
950
+ const previousModel = this.harness.state.model;
951
+ const previousSystemPrompt = this.harness.state.systemPrompt;
952
+ this.harness.state.model = this.resolveModelForCall(options.model, options.role, options.callSite);
953
+ this.harness.state.systemPrompt = this.buildSystemPrompt(options.role);
954
+ this.harness.state.tools = [...this.createBuiltinTools(scopedEnv, options.commands, options.tools, options.role, options.model), ...customTools];
955
+ try {
956
+ return await fn();
957
+ } finally {
958
+ this.harness.state.tools = previousTools;
959
+ this.harness.state.model = previousModel;
960
+ this.harness.state.systemPrompt = previousSystemPrompt;
961
+ }
962
+ }
963
+ async runTaskForTool(params, commands, tools, inheritedRole, inheritedModel, signal) {
964
+ const result = await this.runTask(params.prompt, {
965
+ role: params.role ?? inheritedRole,
966
+ inheritedModel,
967
+ cwd: params.cwd,
968
+ commands,
969
+ tools
970
+ }, signal);
971
+ return {
972
+ content: [{
973
+ type: "text",
974
+ text: result.text || "(task completed with no text)"
975
+ }],
976
+ details: {
977
+ taskId: result.taskId,
978
+ sessionId: result.sessionId,
979
+ messageId: result.messageId,
980
+ role: result.role,
981
+ cwd: result.cwd
982
+ }
983
+ };
984
+ }
985
+ async runTask(text, options, signal) {
986
+ this.assertActive();
987
+ if (!this.createTaskSession) throw new Error("[flue] This session cannot create task sessions.");
988
+ if (this.taskDepth >= MAX_TASK_DEPTH) throw new Error(`[flue] Maximum task depth (${MAX_TASK_DEPTH}) exceeded.`);
989
+ if (signal?.aborted) throw new Error("Operation aborted");
990
+ const taskId = crypto.randomUUID();
991
+ const requestedRole = options?.role ?? this.sessionRole ?? this.config.role;
992
+ let child;
993
+ let abortListener;
994
+ this.emit({
995
+ type: "task_start",
996
+ taskId,
997
+ prompt: text,
998
+ role: requestedRole,
999
+ cwd: options?.cwd,
1000
+ parentSessionId: this.id
1001
+ });
1002
+ try {
1003
+ const role = this.resolveEffectiveRole(options?.role);
1004
+ const commands = mergeCommands(this.agentCommands, options?.commands);
1005
+ child = await this.createTaskSession({
1006
+ parentSessionId: this.id,
1007
+ taskId,
1008
+ parentEnv: this.env,
1009
+ cwd: options?.cwd,
1010
+ role,
1011
+ commands,
1012
+ depth: this.taskDepth + 1
1013
+ });
1014
+ await this.recordTaskSession(child.id, child.storageKey, taskId);
1015
+ this.activeTasks.add(child);
1016
+ if (signal) {
1017
+ abortListener = () => child?.abort();
1018
+ signal.addEventListener("abort", abortListener, { once: true });
1019
+ if (signal.aborted) throw new Error("Operation aborted");
1020
+ }
1021
+ const schema = options?.result;
1022
+ const roleModel = resolveRoleModel(this.config.roles, role);
1023
+ const childOptions = {
1024
+ model: options?.model ?? (roleModel ? void 0 : options?.inheritedModel),
1025
+ tools: options?.tools
1026
+ };
1027
+ if (schema) childOptions.result = schema;
1028
+ const output = await child.prompt(text, childOptions);
1029
+ const taskResult = {
1030
+ output,
1031
+ text: typeof output?.text === "string" ? output.text : child.getAssistantText(),
1032
+ taskId,
1033
+ sessionId: child.id,
1034
+ messageId: child.getLatestAssistantMessageId(),
1035
+ role,
1036
+ cwd: options?.cwd
1037
+ };
1038
+ this.emit({
1039
+ type: "task_end",
1040
+ taskId,
1041
+ isError: false,
1042
+ result: taskResult.text,
1043
+ parentSessionId: this.id
1044
+ });
1045
+ return taskResult;
1046
+ } catch (error) {
1047
+ this.emit({
1048
+ type: "task_end",
1049
+ taskId,
1050
+ isError: true,
1051
+ result: getErrorMessage(error),
1052
+ parentSessionId: this.id
1053
+ });
1054
+ this.emit({
1055
+ type: "error",
1056
+ error: getErrorMessage(error)
1057
+ });
1058
+ throw error;
1059
+ } finally {
1060
+ if (signal && abortListener) signal.removeEventListener("abort", abortListener);
1061
+ if (child) {
1062
+ this.activeTasks.delete(child);
1063
+ child.close();
1064
+ }
1065
+ }
1066
+ }
1067
+ async runOperation(operation, fn) {
1068
+ return this.runExclusive(operation, async () => {
1069
+ try {
1070
+ return await fn();
1071
+ } catch (error) {
1072
+ this.emit({
1073
+ type: "error",
1074
+ error: getErrorMessage(error)
1075
+ });
1076
+ throw error;
1077
+ } finally {
1078
+ this.emit({ type: "idle" });
1079
+ }
1080
+ });
1081
+ }
1082
+ async runExclusive(operation, fn) {
1083
+ this.assertActive();
1084
+ if (this.activeOperation) throw new Error(`[flue] Session "${this.id}" is already running ${this.activeOperation}. Start another session for parallel conversation branches.`);
1085
+ this.activeOperation = operation;
1086
+ try {
1087
+ return await fn();
1088
+ } finally {
1089
+ this.activeOperation = void 0;
1090
+ }
1091
+ }
1092
+ emit(event) {
1093
+ this.eventCallback?.({
1094
+ ...event,
1095
+ sessionId: this.id
1096
+ });
1097
+ }
1098
+ assertActive() {
1099
+ if (this.deleted) throw new Error(`[flue] Session "${this.id}" has been deleted.`);
1100
+ }
1101
+ createShellMessage(command, result, options) {
1102
+ return {
1103
+ role: "user",
1104
+ content: [{
1105
+ type: "text",
1106
+ text: formatShellHistory(command, result, options?.cwd ? `\ncwd: ${options.cwd}` : "", options?.env ? `\nenv: ${Object.keys(options.env).sort().join(", ")}` : "")
1107
+ }],
1108
+ timestamp: Date.now()
1109
+ };
1110
+ }
1111
+ async syncHarnessMessagesSince(index, source) {
1112
+ const messages = this.harness.state.messages.slice(index);
1113
+ if (messages.length === 0) return;
1114
+ this.history.appendMessages(messages, source);
1115
+ await this.save();
812
1116
  }
813
1117
  async save() {
814
1118
  const now = (/* @__PURE__ */ new Date()).toISOString();
815
- const data = {
816
- messages: this.agent.state.messages,
817
- metadata: this.metadata,
818
- createdAt: this.createdAt ?? now,
819
- updatedAt: now,
820
- lastCompaction: this.lastCompaction
821
- };
1119
+ const data = this.history.toData(this.metadata, this.createdAt ?? now, now);
822
1120
  if (!this.createdAt) this.createdAt = now;
823
- await this.store.save(this.id, data);
1121
+ await this.store.save(this.storageKey, data);
1122
+ }
1123
+ async recordTaskSession(sessionId, storageKey, taskId) {
1124
+ const taskSessions = Array.isArray(this.metadata.taskSessions) ? this.metadata.taskSessions : [];
1125
+ if (!taskSessions.some((task) => task?.sessionId === sessionId)) {
1126
+ taskSessions.push({
1127
+ sessionId,
1128
+ taskId,
1129
+ storageKey
1130
+ });
1131
+ this.metadata.taskSessions = taskSessions;
1132
+ await this.save();
1133
+ }
1134
+ }
1135
+ async checkLatestAssistantForCompaction() {
1136
+ const messages = this.harness.state.messages;
1137
+ const lastMsg = messages[messages.length - 1];
1138
+ if (lastMsg?.role === "assistant") await this.checkCompaction(lastMsg);
824
1139
  }
825
1140
  async checkCompaction(assistantMessage) {
826
1141
  if (!this.compactionSettings.enabled) return;
827
1142
  if (assistantMessage.stopReason === "aborted") return;
828
- const contextWindow = this.agent.state.model.contextWindow ?? 0;
1143
+ const contextWindow = this.harness.state.model.contextWindow ?? 0;
829
1144
  if (isContextOverflow(assistantMessage, contextWindow)) {
830
1145
  if (this.overflowRecoveryAttempted) return;
831
1146
  this.overflowRecoveryAttempted = true;
832
1147
  console.error(`[flue:compaction] Overflow detected, compacting and retrying...`);
833
- const messages = this.agent.state.messages;
1148
+ const messages = this.harness.state.messages;
834
1149
  const lastMsg = messages[messages.length - 1];
835
- if (lastMsg && lastMsg.role === "assistant") this.agent.state.messages = messages.slice(0, -1);
1150
+ if (lastMsg && lastMsg.role === "assistant") {
1151
+ this.harness.state.messages = messages.slice(0, -1);
1152
+ this.history.removeLeafMessage(lastMsg);
1153
+ await this.save();
1154
+ }
836
1155
  await this.runCompaction("overflow", true);
837
1156
  return;
838
1157
  }
839
- let contextTokens;
840
- if (assistantMessage.stopReason === "error") {
841
- const estimate = estimateContextTokens(this.agent.state.messages);
842
- if (estimate.lastUsageIndex === null) return;
843
- contextTokens = estimate.tokens;
844
- } else contextTokens = calculateContextTokens(assistantMessage.usage);
1158
+ if (assistantMessage.stopReason === "error") return;
1159
+ const contextTokens = calculateContextTokens(assistantMessage.usage);
845
1160
  if (shouldCompact(contextTokens, contextWindow, this.compactionSettings)) {
846
1161
  console.error(`[flue:compaction] Threshold reached — ${contextTokens} tokens used, window ${contextWindow}, reserve ${this.compactionSettings.reserveTokens}, triggering compaction`);
847
1162
  await this.runCompaction("threshold", false);
@@ -849,45 +1164,59 @@ var Session = class Session {
849
1164
  }
850
1165
  async runCompaction(reason, willRetry) {
851
1166
  this.compactionAbortController = new AbortController();
852
- const messagesBefore = this.agent.state.messages.length;
1167
+ const messagesBefore = this.harness.state.messages.length;
853
1168
  try {
854
- const model = this.agent.state.model;
855
- const messages = this.agent.state.messages;
856
- const preparation = prepareCompaction(messages, this.compactionSettings, this.lastCompaction);
1169
+ const model = this.harness.state.model;
1170
+ const contextEntries = this.history.buildContextEntries();
1171
+ const messages = contextEntries.map((entry) => entry.message);
1172
+ const latestCompaction = this.history.getLatestCompaction();
1173
+ const preparation = prepareCompaction(messages, this.compactionSettings, latestCompaction ? {
1174
+ summary: latestCompaction.summary,
1175
+ firstKeptIndex: 1,
1176
+ details: latestCompaction.details
1177
+ } : void 0);
857
1178
  if (!preparation) {
858
1179
  console.error(`[flue:compaction] Nothing to compact (no valid cut point found)`);
859
1180
  return;
860
1181
  }
1182
+ const firstKeptEntry = contextEntries[preparation.firstKeptIndex]?.entry;
1183
+ if (!firstKeptEntry || firstKeptEntry.type !== "message") {
1184
+ console.error(`[flue:compaction] Nothing to compact (first kept message has no entry)`);
1185
+ return;
1186
+ }
861
1187
  console.error(`[flue:compaction] Summarizing ${preparation.messagesToSummarize.length} messages` + (preparation.isSplitTurn ? ` (split turn: ${preparation.turnPrefixMessages.length} prefix messages)` : "") + `, keeping messages from index ${preparation.firstKeptIndex}`);
862
1188
  const estimatedTokens = preparation.tokensBefore;
863
- this.eventCallback?.({
1189
+ this.emit({
864
1190
  type: "compaction_start",
865
1191
  reason,
866
1192
  estimatedTokens
867
1193
  });
868
1194
  const result = await compact(preparation, model, void 0, this.compactionAbortController.signal);
869
1195
  if (this.compactionAbortController.signal.aborted) return;
870
- const newMessages = buildCompactedMessages(messages, result);
871
- this.agent.state.messages = newMessages;
872
- const messagesAfter = newMessages.length;
1196
+ this.history.appendCompaction({
1197
+ summary: result.summary,
1198
+ firstKeptEntryId: firstKeptEntry.id,
1199
+ tokensBefore: result.tokensBefore,
1200
+ details: result.details
1201
+ });
1202
+ this.harness.state.messages = this.history.buildContext();
1203
+ const messagesAfter = this.harness.state.messages.length;
873
1204
  console.error(`[flue:compaction] Complete — messages: ${messagesBefore} → ${messagesAfter}, tokens before: ${result.tokensBefore}`);
874
- this.eventCallback?.({
1205
+ this.emit({
875
1206
  type: "compaction_end",
876
1207
  messagesBefore,
877
1208
  messagesAfter
878
1209
  });
879
- this.lastCompaction = {
880
- summary: result.summary,
881
- firstKeptIndex: 1,
882
- details: result.details
883
- };
884
1210
  await this.save();
885
1211
  if (willRetry) {
886
- const msgs = this.agent.state.messages;
1212
+ const msgs = this.harness.state.messages;
887
1213
  const lastMsg = msgs[msgs.length - 1];
888
- if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") this.agent.state.messages = msgs.slice(0, -1);
1214
+ if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") this.harness.state.messages = msgs.slice(0, -1);
889
1215
  console.error(`[flue:compaction] Retrying after overflow recovery...`);
890
- await this.agent.continue();
1216
+ const beforeRetry = this.harness.state.messages.length;
1217
+ await this.harness.continue();
1218
+ await this.harness.waitForIdle();
1219
+ await this.syncHarnessMessagesSince(beforeRetry, "retry");
891
1220
  }
892
1221
  } catch (error) {
893
1222
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -897,11 +1226,11 @@ var Session = class Session {
897
1226
  }
898
1227
  }
899
1228
  throwIfError(context) {
900
- const errorMsg = this.agent.state.errorMessage;
1229
+ const errorMsg = this.harness.state.errorMessage;
901
1230
  if (errorMsg) throw new Error(`[flue] ${context} failed: ${errorMsg}`);
902
1231
  }
903
1232
  getAssistantText() {
904
- const messages = this.agent.state.messages;
1233
+ const messages = this.harness.state.messages;
905
1234
  for (let i = messages.length - 1; i >= 0; i--) {
906
1235
  const msg = messages[i];
907
1236
  if (msg.role !== "assistant") continue;
@@ -913,6 +1242,13 @@ var Session = class Session {
913
1242
  }
914
1243
  return "";
915
1244
  }
1245
+ getLatestAssistantMessageId() {
1246
+ const path = this.history.getActivePath();
1247
+ for (let i = path.length - 1; i >= 0; i--) {
1248
+ const entry = path[i];
1249
+ if (entry.type === "message" && entry.message.role === "assistant") return entry.id;
1250
+ }
1251
+ }
916
1252
  async extractResultWithRetry(schema) {
917
1253
  const text = this.getAssistantText();
918
1254
  try {
@@ -921,9 +1257,11 @@ var Session = class Session {
921
1257
  if (!(err instanceof ResultExtractionError)) throw err;
922
1258
  if (!err.message.includes("RESULT_START")) throw err;
923
1259
  const followUpPrompt = buildResultExtractionPrompt(schema);
924
- await this.agent.prompt(followUpPrompt);
925
- await this.agent.waitForIdle();
926
- await this.save();
1260
+ const beforeRetry = this.harness.state.messages.length;
1261
+ await this.harness.prompt(followUpPrompt);
1262
+ await this.harness.waitForIdle();
1263
+ await this.syncHarnessMessagesSince(beforeRetry, "retry");
1264
+ await this.checkLatestAssistantForCompaction();
927
1265
  return extractResult(this.getAssistantText(), schema);
928
1266
  }
929
1267
  }
@@ -938,6 +1276,28 @@ function normalizePath(p) {
938
1276
  }
939
1277
  return "/" + result.join("/");
940
1278
  }
1279
+ async function deleteSessionTree(store, storageKey, seen = /* @__PURE__ */ new Set()) {
1280
+ if (seen.has(storageKey)) return;
1281
+ seen.add(storageKey);
1282
+ const data = await store.load(storageKey);
1283
+ const taskSessions = Array.isArray(data?.metadata?.taskSessions) ? data.metadata.taskSessions : [];
1284
+ for (const task of taskSessions) if (typeof task?.storageKey === "string") await deleteSessionTree(store, task.storageKey, seen);
1285
+ await store.delete(storageKey);
1286
+ }
1287
+ function formatShellHistory(command, result, cwdLine, envLine) {
1288
+ const sections = [`<shell_command>\n$ ${command}${cwdLine}${envLine}\n</shell_command>`, `<shell_result exitCode="${result.exitCode}">`];
1289
+ if (result.stdout) sections.push(`<stdout>\n${result.stdout}\n</stdout>`);
1290
+ if (result.stderr) sections.push(`<stderr>\n${result.stderr}\n</stderr>`);
1291
+ sections.push("</shell_result>");
1292
+ return truncateShellHistory(sections.join("\n"));
1293
+ }
1294
+ function truncateShellHistory(text) {
1295
+ if (text.length <= MAX_SHELL_HISTORY_CHARS) return text;
1296
+ return `[Shell output truncated: ${text.length - MAX_SHELL_HISTORY_CHARS} leading characters omitted]\n` + text.slice(text.length - MAX_SHELL_HISTORY_CHARS);
1297
+ }
1298
+ function getErrorMessage(error) {
1299
+ return error instanceof Error ? error.message : String(error);
1300
+ }
941
1301
 
942
1302
  //#endregion
943
- export { Session as n, normalizePath as r, InMemorySessionStore as t };
1303
+ export { assertRoleExists as a, normalizePath as i, Session as n, createScopedEnv as o, deleteSessionTree as r, mergeCommands as s, InMemorySessionStore as t };