@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.
- package/README.md +155 -20
- package/dist/{agent-BYG0nVbQ.mjs → agent-BB4lwAd5.mjs} +24 -3
- package/dist/client.d.mts +4 -3
- package/dist/client.mjs +45 -26
- package/dist/cloudflare/index.d.mts +4 -9
- package/dist/cloudflare/index.mjs +32 -21
- package/dist/{command-helpers-C8SHLdaA.d.mts → command-helpers-DdAfbnom.d.mts} +1 -1
- package/dist/index.d.mts +48 -5
- package/dist/index.mjs +746 -179
- package/dist/internal.d.mts +17 -3
- package/dist/internal.mjs +37 -4
- package/dist/mcp-BVF-sOBZ.d.mts +22 -0
- package/dist/mcp-DOgMtp8y.mjs +285 -0
- package/dist/node/index.d.mts +2 -2
- package/dist/node/index.mjs +1 -1
- package/dist/sandbox.d.mts +4 -3
- package/dist/sandbox.mjs +58 -28
- package/dist/{session-CiAMTsLZ.mjs → session-DukL3zwF.mjs} +629 -269
- package/dist/{types-C0nqbu6Z.d.mts → types-T8pE1xIS.d.mts} +156 -53
- package/package.json +12 -4
- /package/dist/{command-helpers-CxRhK1my.mjs → command-helpers-hTZKWK13.mjs} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as loadSkillByPath, n as createTools,
|
|
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
|
|
684
|
+
var Session = class {
|
|
498
685
|
id;
|
|
499
686
|
metadata;
|
|
500
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.
|
|
521
|
-
|
|
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
|
-
|
|
529
|
-
this.
|
|
530
|
-
const
|
|
531
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
850
|
+
const shellResult = {
|
|
648
851
|
stdout: result.stdout,
|
|
649
852
|
stderr: result.stderr,
|
|
650
853
|
exitCode: result.exitCode
|
|
651
854
|
};
|
|
652
|
-
|
|
653
|
-
this.
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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.
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
738
|
-
|
|
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
|
|
752
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
811
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1148
|
+
const messages = this.harness.state.messages;
|
|
834
1149
|
const lastMsg = messages[messages.length - 1];
|
|
835
|
-
if (lastMsg && lastMsg.role === "assistant")
|
|
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
|
-
|
|
840
|
-
|
|
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.
|
|
1167
|
+
const messagesBefore = this.harness.state.messages.length;
|
|
853
1168
|
try {
|
|
854
|
-
const model = this.
|
|
855
|
-
const
|
|
856
|
-
const
|
|
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.
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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.
|
|
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.
|
|
1212
|
+
const msgs = this.harness.state.messages;
|
|
887
1213
|
const lastMsg = msgs[msgs.length - 1];
|
|
888
|
-
if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
925
|
-
await this.
|
|
926
|
-
await this.
|
|
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,
|
|
1303
|
+
export { assertRoleExists as a, normalizePath as i, Session as n, createScopedEnv as o, deleteSessionTree as r, mergeCommands as s, InMemorySessionStore as t };
|