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