scream-code 0.10.7 → 0.10.9
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/dist/{app-BZwFbrNj.mjs → app-CwHfTwUZ.mjs} +1234 -160
- package/dist/main.mjs +1 -1
- package/dist/{text-input-dialog-DqBy9bEe.mjs → text-input-dialog-B1ak519Y.mjs} +1 -1
- package/dist/{text-input-dialog-BWp39mq8.mjs → text-input-dialog-DRUaPOEQ.mjs} +29 -3
- package/package.json +2 -2
- package/dist/{src-BMbOMRuY.mjs → src-C8lOjRbK.mjs} +1 -1
|
@@ -7,7 +7,7 @@ import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } fr
|
|
|
7
7
|
import "./suppress-sqlite-warning-C2VB0doZ.mjs";
|
|
8
8
|
import { C as join$1, D as resolve$1, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize, x as dirname$2, y as KnowledgeStore } from "./src-BH9W5k24.mjs";
|
|
9
9
|
import { t as require_base64_js } from "./base64-js-DzVmk6Nb.mjs";
|
|
10
|
-
import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-
|
|
10
|
+
import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-DRUaPOEQ.mjs";
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
13
13
|
import * as fs$1 from "node:fs/promises";
|
|
@@ -28,6 +28,8 @@ import Vt, { appendFileSync, chmodSync, closeSync, constants, createReadStream,
|
|
|
28
28
|
import * as path$8 from "node:path";
|
|
29
29
|
import path, { basename, dirname as dirname$1, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
30
30
|
import { z } from "zod";
|
|
31
|
+
import { exec, execFile, execSync, spawn, spawnSync } from "node:child_process";
|
|
32
|
+
import { promisify } from "node:util";
|
|
31
33
|
import { DatabaseSync } from "node:sqlite";
|
|
32
34
|
import * as nodeOs from "node:os";
|
|
33
35
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -35,7 +37,6 @@ import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
|
35
37
|
import { StringDecoder } from "node:string_decoder";
|
|
36
38
|
import Pi from "assert";
|
|
37
39
|
import ro from "node:assert";
|
|
38
|
-
import { exec, execFile, execSync, spawn, spawnSync } from "node:child_process";
|
|
39
40
|
import * as posixPath from "node:path/posix";
|
|
40
41
|
import * as win32Path from "node:path/win32";
|
|
41
42
|
import { createServer } from "node:http";
|
|
@@ -51,10 +52,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
51
52
|
import { Command, Option } from "commander";
|
|
52
53
|
import { createInterface } from "node:readline/promises";
|
|
53
54
|
import chalk, { chalkStderr } from "chalk";
|
|
54
|
-
import { CombinedAutocompleteProvider, Container, Editor, Image, Input, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, getImageDimensions, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
|
|
55
|
+
import { CombinedAutocompleteProvider, Container, Editor, Image, Input, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, getImageDimensions, isKeyRelease, matchesKey, setTightMode, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
|
|
55
56
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
56
57
|
import { diffWords } from "diff";
|
|
57
|
-
import { promisify } from "node:util";
|
|
58
58
|
import { gt, valid } from "semver";
|
|
59
59
|
import { createInterface as createInterface$1 } from "node:readline";
|
|
60
60
|
//#region ../../packages/agent-core/src/errors/codes.ts
|
|
@@ -10336,7 +10336,7 @@ var require_gaxios = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
10336
10336
|
}
|
|
10337
10337
|
static async #getFetch() {
|
|
10338
10338
|
const hasWindow = typeof window !== "undefined" && !!window;
|
|
10339
|
-
this.#fetch ||= hasWindow ? window.fetch : (await import("./src-
|
|
10339
|
+
this.#fetch ||= hasWindow ? window.fetch : (await import("./src-C8lOjRbK.mjs")).default;
|
|
10340
10340
|
return this.#fetch;
|
|
10341
10341
|
}
|
|
10342
10342
|
/**
|
|
@@ -49870,6 +49870,7 @@ async function generate(provider, systemPrompt, tools, history, callbacks, optio
|
|
|
49870
49870
|
const hasText = message.content.some((p) => p.type === "text" && p.text.trim().length > 0);
|
|
49871
49871
|
const hasToolCalls = message.toolCalls.length > 0;
|
|
49872
49872
|
if (hasThink && !hasText && !hasToolCalls) throw new APIEmptyResponseError(`The API returned a response containing only thinking content without any text or tool calls. This usually indicates the stream was interrupted or the output token budget was exhausted during reasoning. Provider: ${provider.name}, model: ${provider.modelName}. If this persists, reduce reasoning effort or switch to a model with a larger output budget.`);
|
|
49873
|
+
if (!hasText && !hasToolCalls && message.content.some((p) => p.type === "text")) throw new APIEmptyResponseError(`The API returned a response containing only empty or whitespace text. This usually indicates a truncated or malformed stream. Provider: ${provider.name}, model: ${provider.modelName}.`);
|
|
49873
49874
|
if (callbacks?.onToolCall !== void 0) for (const toolCall of message.toolCalls) {
|
|
49874
49875
|
await throwIfAborted$1(options?.signal, stream);
|
|
49875
49876
|
await callbacks.onToolCall(toolCall);
|
|
@@ -50801,6 +50802,14 @@ function resolveGlobalLogPath(homeDir) {
|
|
|
50801
50802
|
//#endregion
|
|
50802
50803
|
//#region ../../packages/agent-core/src/utils/tokens.ts
|
|
50803
50804
|
/**
|
|
50805
|
+
* WeakMap cache for per-message token estimates. Messages are immutable once
|
|
50806
|
+
* settled (streaming `partial` messages never enter compaction paths), so the
|
|
50807
|
+
* cache is safe. Spread copies created by micro-compaction ({ ...msg, content })
|
|
50808
|
+
* are new objects → natural cache miss → re-estimated. Original objects retain
|
|
50809
|
+
* their cached count, avoiding O(n²) re-scans during compaction detection.
|
|
50810
|
+
*/
|
|
50811
|
+
const messageTokenCache = /* @__PURE__ */ new WeakMap();
|
|
50812
|
+
/**
|
|
50804
50813
|
* Estimate token count from text using a character-based heuristic.
|
|
50805
50814
|
* - ASCII (~4 chars per token)
|
|
50806
50815
|
* - CJK and other non-ASCII (~1 char per token)
|
|
@@ -50830,12 +50839,15 @@ function estimateTokensForTools(tools) {
|
|
|
50830
50839
|
return total;
|
|
50831
50840
|
}
|
|
50832
50841
|
function estimateTokensForMessage(message) {
|
|
50842
|
+
const cached = messageTokenCache.get(message);
|
|
50843
|
+
if (cached !== void 0) return cached;
|
|
50833
50844
|
let total = estimateTokens$1(message.role);
|
|
50834
50845
|
for (const part of message.content) total += estimateTokensForContentPart(part);
|
|
50835
50846
|
if (message.toolCalls !== void 0) for (const call of message.toolCalls) {
|
|
50836
50847
|
total += estimateTokens$1(call.name);
|
|
50837
50848
|
total += estimateTokens$1(JSON.stringify(call.arguments));
|
|
50838
50849
|
}
|
|
50850
|
+
messageTokenCache.set(message, total);
|
|
50839
50851
|
return total;
|
|
50840
50852
|
}
|
|
50841
50853
|
function estimateTokensForContentPart(part) {
|
|
@@ -54165,7 +54177,7 @@ function matchRuleSubjects(ruleArgs, subjects, matchesPositivePattern) {
|
|
|
54165
54177
|
}
|
|
54166
54178
|
//#endregion
|
|
54167
54179
|
//#region ../../packages/agent-core/src/tools/background/task-list.md
|
|
54168
|
-
var task_list_default = "List background tasks and their current status.\n\nUse this tool to discover which background tasks exist and where each one\nstands. It is the entry point for inspecting background work: it returns a\ntask ID, status, command, description, and PID for every task it reports,\nplus the exit code and stop reason for tasks that have already finished.\n\nGuidelines:\n\n- After a context compaction, or whenever you are unsure which background\n tasks are running or what their task IDs are, call this tool to\n re-enumerate them instead of guessing a task ID.\n-
|
|
54180
|
+
var task_list_default = "List background tasks and their current status.\n\nUse this tool to discover which background tasks exist and where each one\nstands. It is the entry point for inspecting background work: it returns a\ntask ID, status, command, description, and PID for every task it reports,\nplus the exit code and stop reason for tasks that have already finished.\n\nGuidelines:\n\n- After a context compaction, or whenever you are unsure which background\n tasks are running or what their task IDs are, call this tool to\n re-enumerate them instead of guessing a task ID.\n- Pass `active_only=false` only when you specifically need to see tasks that\n have already finished. With `active_only=false` the result may also include\n `lost` tasks — tasks left over from a previous process that can no longer be\n inspected or controlled; treat them as already terminated.\n- This tool only lists tasks; it does not return their output. Use it first\n to locate the task ID you need, then call `TaskOutput` with that ID to read\n the task's output and details.\n- This tool is read-only and does not change any state, so it is always safe\n to call, including in plan mode.\n";
|
|
54169
54181
|
//#endregion
|
|
54170
54182
|
//#region ../../packages/agent-core/src/tools/background/task-list.ts
|
|
54171
54183
|
/**
|
|
@@ -54739,7 +54751,7 @@ function oneShotJitteredNextCronRunMs(task, idealMs, config = DEFAULT_CRON_JITTE
|
|
|
54739
54751
|
}
|
|
54740
54752
|
//#endregion
|
|
54741
54753
|
//#region ../../packages/agent-core/src/tools/cron/cron-create.md
|
|
54742
|
-
var cron_create_default = "Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.\n\
|
|
54754
|
+
var cron_create_default = "Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.\n\n`0 9 * * *` means 9am local — no timezone conversion needed.\n\n## One-shot tasks (recurring: false)\n\nFor \"remind me at X\" or \"at <time>, do Y\" requests — fire once then auto-delete.\nPin minute/hour/day-of-month/month to specific values:\n \"remind me at 2:30pm today to check the deploy\" → cron: \"30 14 <today_dom> <today_month> *\", recurring: false\n \"tomorrow morning, run the smoke test\" → cron: \"57 8 <tomorrow_dom> <tomorrow_month> *\", recurring: false\n\n## Recurring jobs\n\nFor \"every N minutes\" / \"every hour\" / \"weekdays at 9am\" requests:\n \"*/5 * * * *\" (every 5 min), \"0 * * * *\" (hourly), \"0 9 * * 1-5\" (weekdays at 9am local)\n\n## Avoid the :00 and :30 minute marks when the task allows it\n\nEvery user who asks for \"9am\" gets `0 9`, and every user who asks for \"hourly\" gets `0 *` — which means requests from across the planet land on the API at the same instant. When the user's request is approximate, pick a minute that is NOT 0 or 30:\n \"every morning around 9\" → \"57 8 * * *\" or \"3 9 * * *\" (not \"0 9 * * *\")\n \"hourly\" → \"7 * * * *\" (not \"0 * * * *\")\n \"in an hour or so, remind me to...\" → pick whatever minute you land on, don't round\n\nOnly use minute 0 or 30 when the user names that exact time and clearly means it (\"at 9:00 sharp\", \"at half past\", coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.\n\n## Coalesce semantics\n\nIf the scheduler slept past multiple ideal fire times (laptop closed, long-running turn, etc.), only **one** fire is delivered when it wakes up. The origin carries `coalescedCount` showing how many ideal fires were collapsed into this single delivery. You should treat `coalescedCount > 1` as \"I missed some checks; only the latest state matters\" rather than running the prompt that many times.\n\n## Cron-fire envelope\n\nWhen a cron task fires, the prompt you scheduled is re-injected wrapped in an XML envelope that exposes the fire context:\n\n```\n<cron-fire jobId=\"...\" cron=\"...\" recurring=\"true|false\" coalescedCount=\"N\" stale=\"true|false\">\n<prompt>\nyour original prompt text, verbatim\n</prompt>\n</cron-fire>\n```\n\nThe envelope is parseable. Use `coalescedCount > 1` to know multiple ideal fires were collapsed into a single delivery (treat as \"only the latest state matters\"), and `stale=\"true\"` as a cue that the task is past its 7-day threshold.\n\n## 7-day stale behavior\n\nRecurring tasks that have been alive for more than 7 days fire one\nfinal time with `stale: true` on the envelope, and the system then\nauto-deletes the task. The flag is the model's notice that this is\nthe last delivery. If the schedule is still wanted, call `CronCreate`\nagain with the same `cron` and `prompt` — that resets `createdAt` and\nstarts a fresh 7-day window. One-shot tasks are never marked stale.\n\nBench / acceptance runs can set `SCREAM_CRON_NO_STALE=1` to disable the\njudgment entirely.\n\n## Jitter behavior\n\nAnti-herd jitter is applied deterministically per task id:\n - Recurring: ideal fire time is shifted **forward** by an offset ≤ min(10% of the cron period, 15 minutes). A `*/5 * * * *` task can drift up to 30s; a `0 9 * * *` task can drift up to 15 minutes.\n - One-shot: only when the ideal fire lands on `:00` or `:30` of the hour, the fire is pulled **earlier** by ≤ 90 seconds. Other minutes pass through unchanged.\n\nBench / acceptance tests can set `SCREAM_CRON_NO_JITTER=1` to disable jitter entirely.\n\n## One-shot vs recurring — when to pick which\n\nUse one-shot for \"remind me at X\" style requests, single deadlines, and any task that should not repeat. Use recurring for periodic polling (CI status, build watchers, scheduled reports), workday rituals, and anything the user explicitly described as recurring.\n\n## Session lifetime\n\nCron tasks live in the current scream CLI session. When you exit, they\nare persisted under the session homedir; the next `scream resume` of the\nsame session reloads them and the scheduler resumes from each task's\n`createdAt`. Fire times that fell during the offline window are\ncollapsed into a single delivery via `coalescedCount` (and recurring\ntasks past their 7-day window arrive with `stale: true` as their final\ndelivery).\n\nTasks do **not** carry over into a brand-new session — they are scoped\nto the resumed session id, not to the working directory.\n\n## Returned fields\n\n`id` (8-hex), `humanSchedule` (English summary), `recurring`,\n`nextFireAt` (ISO timestamp or null). `id` is needed by `CronDelete`.\n";
|
|
54743
54755
|
/**
|
|
54744
54756
|
* Hard ceiling on `prompt` byte length (UTF-8). The zod `.max(...)`
|
|
54745
54757
|
* upstream is in code units, which underflows multi-byte input
|
|
@@ -54860,7 +54872,7 @@ function formatOutput(o) {
|
|
|
54860
54872
|
}
|
|
54861
54873
|
//#endregion
|
|
54862
54874
|
//#region ../../packages/agent-core/src/tools/cron/cron-delete.md
|
|
54863
|
-
var cron_delete_default = "Cancel a scheduled cron job by id.\n\nUse this tool to remove a cron task previously scheduled with\n`CronCreate`.
|
|
54875
|
+
var cron_delete_default = "Cancel a scheduled cron job by id.\n\nUse this tool to remove a cron task previously scheduled with\n`CronCreate`. Quote the id verbatim, no prefix.\n\nBehaviour by task kind:\n\n- **Recurring task** (`recurring: true`): stops all future fires\n immediately. The scheduler picks up the deletion on its next tick.\n- **One-shot task** (`recurring: false`): cancels the pending fire if\n it has not happened yet. One-shots that have already fired\n auto-delete themselves, so calling `CronDelete` on a fired one-shot\n returns \"no cron job with id ...\".\n\nNot-found is reported as an error (not a silent no-op) so you can\ncorrect yourself — typically by calling `CronList` to see which ids\nare actually live, rather than re-trying with the same stale id.\n\nRefresh pattern (use when you want a stale recurring schedule to\ncontinue):\n\nStale recurring tasks are auto-deleted by the system after their final\nfire — there is nothing for `CronDelete` to remove at that point. To\nkeep the schedule running, just call `CronCreate` with the same `cron`\nand `prompt`. Use `CronList`'s `prompt` field to recall the original\ntext after a context compaction.\n\n`CronDelete` remains the right call when you want to cancel a task\nthat is still live (recurring not yet stale, or a one-shot still\npending).\n\nGuidelines:\n\n- Cron deletion is irreversible — there is no undo. If you delete the\n wrong task, you must re-create it with `CronCreate`.\n- If the model is unsure which id is current (e.g. after a context\n compaction), call `CronList` first rather than guessing.\n";
|
|
54864
54876
|
//#endregion
|
|
54865
54877
|
//#region ../../packages/agent-core/src/tools/cron/cron-delete.ts
|
|
54866
54878
|
/**
|
|
@@ -55471,7 +55483,7 @@ function filterSubagentsBySpawns$1(subagents, allowedSpawns) {
|
|
|
55471
55483
|
}
|
|
55472
55484
|
//#endregion
|
|
55473
55485
|
//#region ../../packages/agent-core/src/tools/builtin/collaboration/ask-user.md
|
|
55474
|
-
var ask_user_default = "Use this tool when you need to ask the user questions with structured options during execution. This allows you to:\n1. Collect user preferences or requirements before proceeding\n2. Resolve ambiguous or underspecified instructions\n3. Let the user decide between implementation approaches as you work\n4. Present concrete options when multiple valid directions exist\n\n**When NOT to use:**\n- When you can infer the answer from context
|
|
55486
|
+
var ask_user_default = "Use this tool when you need to ask the user questions with structured options during execution. This allows you to:\n1. Collect user preferences or requirements before proceeding\n2. Resolve ambiguous or underspecified instructions\n3. Let the user decide between implementation approaches as you work\n4. Present concrete options when multiple valid directions exist\n\n**When NOT to use:**\n- When you can infer the answer from context - be decisive and proceed\n- Trivial decisions that don't materially affect the outcome\n\nOverusing this tool interrupts the user's flow. Only use it when the user's input genuinely changes your next action.\n\n**Usage notes:**\n- Group related questions to minimize interruptions.\n- If you recommend a specific option, list it first.\n";
|
|
55475
55487
|
//#endregion
|
|
55476
55488
|
//#region ../../packages/agent-core/src/tools/builtin/collaboration/ask-user.ts
|
|
55477
55489
|
/**
|
|
@@ -55719,7 +55731,7 @@ const SetGoalBudgetToolInputSchema = z.object({
|
|
|
55719
55731
|
var SetGoalBudgetTool = class {
|
|
55720
55732
|
agent;
|
|
55721
55733
|
name = "SetGoalBudget";
|
|
55722
|
-
description = "Set a hard budget limit for the current goal.
|
|
55734
|
+
description = "Set a hard budget limit for the current goal. The goal will be blocked when the budget is reached.";
|
|
55723
55735
|
parameters = toInputJsonSchema(SetGoalBudgetToolInputSchema);
|
|
55724
55736
|
constructor(agent) {
|
|
55725
55737
|
this.agent = agent;
|
|
@@ -55791,14 +55803,32 @@ const MAX_GOAL_OBJECTIVE_LENGTH = 4e3;
|
|
|
55791
55803
|
/** Consecutive markBlocked calls with the same reason required before blocking. */
|
|
55792
55804
|
const BLOCKED_STREAK_THRESHOLD = 3;
|
|
55793
55805
|
/** Maximum number of working notes kept per goal. */
|
|
55794
|
-
const MAX_GOAL_NOTES =
|
|
55806
|
+
const MAX_GOAL_NOTES = 60;
|
|
55795
55807
|
/** Maximum characters per note. */
|
|
55796
|
-
const MAX_NOTE_LENGTH =
|
|
55808
|
+
const MAX_NOTE_LENGTH = 400;
|
|
55797
55809
|
const GOAL_CANCELLED_REMINDER = [
|
|
55798
55810
|
"The user cancelled the current goal.",
|
|
55799
55811
|
"Ignore earlier active-goal reminders for that goal.",
|
|
55800
55812
|
"Handle the next user request normally unless the user starts or resumes a goal."
|
|
55801
55813
|
].join(" ");
|
|
55814
|
+
/**
|
|
55815
|
+
* Returns true when any configured budget has less than `threshold` of its
|
|
55816
|
+
* allowance remaining (e.g. 0.2 = under 20% left). Budgets that are not
|
|
55817
|
+
* configured (null) are ignored. Used to steer the model toward convergence
|
|
55818
|
+
* before a hard over-budget block fires.
|
|
55819
|
+
*/
|
|
55820
|
+
function isBudgetNearExhaustion(budget, threshold) {
|
|
55821
|
+
if (budget.turnBudget !== null && budget.remainingTurns !== null && budget.turnBudget > 0) {
|
|
55822
|
+
if (budget.remainingTurns / budget.turnBudget < threshold) return true;
|
|
55823
|
+
}
|
|
55824
|
+
if (budget.tokenBudget !== null && budget.remainingTokens !== null && budget.tokenBudget > 0) {
|
|
55825
|
+
if (budget.remainingTokens / budget.tokenBudget < threshold) return true;
|
|
55826
|
+
}
|
|
55827
|
+
if (budget.wallClockBudgetMs !== null && budget.remainingWallClockMs !== null && budget.wallClockBudgetMs > 0) {
|
|
55828
|
+
if (budget.remainingWallClockMs / budget.wallClockBudgetMs < threshold) return true;
|
|
55829
|
+
}
|
|
55830
|
+
return false;
|
|
55831
|
+
}
|
|
55802
55832
|
const GOAL_COMPLETION_REMINDER_NAME = "goal_completion_summary";
|
|
55803
55833
|
const GOAL_BLOCKED_REMINDER_NAME = "goal_blocked_reason";
|
|
55804
55834
|
var GoalMode = class {
|
|
@@ -55858,6 +55888,7 @@ var GoalMode = class {
|
|
|
55858
55888
|
state.wallClockResumedAt = void 0;
|
|
55859
55889
|
}
|
|
55860
55890
|
if (record.budgetLimits !== void 0) state.budgetLimits = record.budgetLimits;
|
|
55891
|
+
if (record.objective !== void 0) state.objective = record.objective;
|
|
55861
55892
|
}
|
|
55862
55893
|
restoreClear(_record) {
|
|
55863
55894
|
this.state = void 0;
|
|
@@ -55958,6 +55989,23 @@ var GoalMode = class {
|
|
|
55958
55989
|
this.appendGoalUpdate({ budgetLimits: state.budgetLimits });
|
|
55959
55990
|
return this.toSnapshot(state);
|
|
55960
55991
|
}
|
|
55992
|
+
async updateObjective(input, _actor = "user") {
|
|
55993
|
+
const state = this.requireState();
|
|
55994
|
+
const objective = input.objective.trim();
|
|
55995
|
+
if (objective.length === 0) throw new ScreamError(ErrorCodes.GOAL_OBJECTIVE_EMPTY, "Goal objective cannot be empty");
|
|
55996
|
+
if (state.status === "complete") throw new ScreamError(ErrorCodes.GOAL_STATUS_INVALID, "Cannot update a completed goal.");
|
|
55997
|
+
if (state.status === "blocked") throw new ScreamError(ErrorCodes.GOAL_STATUS_INVALID, "Cannot update a blocked goal. Resume it first.");
|
|
55998
|
+
state.objective = objective;
|
|
55999
|
+
const noteContent = `Goal objective updated by user: ${objective}`.slice(0, MAX_NOTE_LENGTH);
|
|
56000
|
+
state.notes.push({
|
|
56001
|
+
content: noteContent,
|
|
56002
|
+
time: Date.now()
|
|
56003
|
+
});
|
|
56004
|
+
if (state.notes.length > MAX_GOAL_NOTES) state.notes = state.notes.slice(-60);
|
|
56005
|
+
this.persistState(state);
|
|
56006
|
+
this.appendGoalUpdate({ objective });
|
|
56007
|
+
return this.toSnapshot(state);
|
|
56008
|
+
}
|
|
55961
56009
|
async cancelGoal(actor = "user") {
|
|
55962
56010
|
const state = this.requireState();
|
|
55963
56011
|
const snapshot = this.toSnapshot(state);
|
|
@@ -56041,7 +56089,7 @@ var GoalMode = class {
|
|
|
56041
56089
|
content: trimmed,
|
|
56042
56090
|
time: Date.now()
|
|
56043
56091
|
});
|
|
56044
|
-
if (state.notes.length > MAX_GOAL_NOTES) state.notes = state.notes.slice(-
|
|
56092
|
+
if (state.notes.length > MAX_GOAL_NOTES) state.notes = state.notes.slice(-60);
|
|
56045
56093
|
this.persistState(state, { silent: true });
|
|
56046
56094
|
return this.toSnapshot(state);
|
|
56047
56095
|
}
|
|
@@ -56187,6 +56235,85 @@ function formatTokens$2(tokens) {
|
|
|
56187
56235
|
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
56188
56236
|
}
|
|
56189
56237
|
//#endregion
|
|
56238
|
+
//#region ../../packages/agent-core/src/tools/builtin/goal/emission-guard.ts
|
|
56239
|
+
/**
|
|
56240
|
+
* Grader emission guard - deduplicates and filters grader (referee) feedback
|
|
56241
|
+
* before it is injected into the agent context.
|
|
56242
|
+
*
|
|
56243
|
+
* Without this guard, a grader that repeatedly returns the same FAIL reason
|
|
56244
|
+
* (e.g. "redo", "not right") causes the identical feedback to be appended to
|
|
56245
|
+
* context on every submission. This wastes context budget and can trap the
|
|
56246
|
+
* agent in a loop where it keeps retrying without new information.
|
|
56247
|
+
*
|
|
56248
|
+
* Inspired by oh-my-pi's `advisor/emission-guard.ts`, but adapted for
|
|
56249
|
+
* scream-code's grader model: the grader is called once per `complete`
|
|
56250
|
+
* submission (not continuously), so per-round limiting is unnecessary. The
|
|
56251
|
+
* guard focuses on normalization, content-free filtering, and exact-text
|
|
56252
|
+
* deduplication keyed by goal objective.
|
|
56253
|
+
*/
|
|
56254
|
+
/** Short phrases that carry no actionable feedback. */
|
|
56255
|
+
const CONTENT_FREE_PHRASES = new Set([
|
|
56256
|
+
"stop",
|
|
56257
|
+
"done",
|
|
56258
|
+
"lgtm",
|
|
56259
|
+
"no issue",
|
|
56260
|
+
"redo",
|
|
56261
|
+
"try again",
|
|
56262
|
+
"not good enough",
|
|
56263
|
+
"fix it",
|
|
56264
|
+
"wrong",
|
|
56265
|
+
"incorrect",
|
|
56266
|
+
"bad",
|
|
56267
|
+
"重做",
|
|
56268
|
+
"不对",
|
|
56269
|
+
"不行",
|
|
56270
|
+
"继续",
|
|
56271
|
+
"再做",
|
|
56272
|
+
"错误",
|
|
56273
|
+
"不行",
|
|
56274
|
+
"不好",
|
|
56275
|
+
"改"
|
|
56276
|
+
]);
|
|
56277
|
+
/** Maximum number of historically seen feedback texts retained per goal. */
|
|
56278
|
+
const MAX_SEEN_PER_GOAL = 16;
|
|
56279
|
+
/** Phrases at or below this length are checked against the content-free set. */
|
|
56280
|
+
function isContentFree(text) {
|
|
56281
|
+
const lower = text.toLowerCase().trim();
|
|
56282
|
+
return CONTENT_FREE_PHRASES.has(lower);
|
|
56283
|
+
}
|
|
56284
|
+
/**
|
|
56285
|
+
* Filters grader feedback before injection into the agent context.
|
|
56286
|
+
*
|
|
56287
|
+
* Returns the denoised feedback text if it should be injected, or `null` if
|
|
56288
|
+
* the feedback is empty, content-free, or an exact duplicate of a previously
|
|
56289
|
+
* seen feedback for the same goal. The caller should skip injection when
|
|
56290
|
+
* `null` is returned.
|
|
56291
|
+
*
|
|
56292
|
+
* @param reason The raw grader FAIL reason.
|
|
56293
|
+
* @param goalKey A stable identifier for the current goal (e.g. the objective
|
|
56294
|
+
* text). Different goals get independent deduplication buckets.
|
|
56295
|
+
*/
|
|
56296
|
+
var GraderEmissionGuard = class {
|
|
56297
|
+
seenByGoal = /* @__PURE__ */ new Map();
|
|
56298
|
+
filter(reason, goalKey) {
|
|
56299
|
+
const normalized = reason.normalize("NFKC").trim();
|
|
56300
|
+
if (normalized.length === 0) return null;
|
|
56301
|
+
if (isContentFree(normalized)) return null;
|
|
56302
|
+
const key = normalized.toLowerCase();
|
|
56303
|
+
const bucket = this.seenByGoal.get(goalKey);
|
|
56304
|
+
if (bucket !== void 0 && bucket.includes(key)) return null;
|
|
56305
|
+
const next = bucket ?? [];
|
|
56306
|
+
next.push(key);
|
|
56307
|
+
if (next.length > MAX_SEEN_PER_GOAL) next.shift();
|
|
56308
|
+
this.seenByGoal.set(goalKey, next);
|
|
56309
|
+
return normalized;
|
|
56310
|
+
}
|
|
56311
|
+
/** Clear the deduplication bucket for a goal (e.g. on goal change). */
|
|
56312
|
+
resetGoal(goalKey) {
|
|
56313
|
+
this.seenByGoal.delete(goalKey);
|
|
56314
|
+
}
|
|
56315
|
+
};
|
|
56316
|
+
//#endregion
|
|
56190
56317
|
//#region ../../packages/agent-core/src/tools/builtin/goal/update-goal.ts
|
|
56191
56318
|
const UpdateGoalToolInputSchema = z.object({
|
|
56192
56319
|
status: z.enum([
|
|
@@ -56198,6 +56325,13 @@ const UpdateGoalToolInputSchema = z.object({
|
|
|
56198
56325
|
reason: z.string().optional().describe("Optional reason for the status change, especially for blocked.")
|
|
56199
56326
|
}).strict();
|
|
56200
56327
|
const MAX_GRADER_OUTPUT_CHARS = 4e3;
|
|
56328
|
+
/** Maximum characters of `git diff --stat HEAD` to append to the grader input. */
|
|
56329
|
+
const MAX_DIFF_STAT_CHARS = 2e3;
|
|
56330
|
+
/**
|
|
56331
|
+
* Module-level emission guard. Deduplicates grader FAIL feedback per goal so
|
|
56332
|
+
* the referee cannot trap the agent in a loop by repeating the same reason.
|
|
56333
|
+
*/
|
|
56334
|
+
const graderEmissionGuard = new GraderEmissionGuard();
|
|
56201
56335
|
function extractRecentOutput(history) {
|
|
56202
56336
|
const parts = [];
|
|
56203
56337
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
@@ -56210,6 +56344,51 @@ function extractRecentOutput(history) {
|
|
|
56210
56344
|
const joined = parts.join("\n\n");
|
|
56211
56345
|
return joined.length > MAX_GRADER_OUTPUT_CHARS ? `${joined.slice(0, MAX_GRADER_OUTPUT_CHARS)}…` : joined;
|
|
56212
56346
|
}
|
|
56347
|
+
/**
|
|
56348
|
+
* Append cross-turn working notes to the output seen by the grader. Notes are
|
|
56349
|
+
* written by the agent across continuation turns, so they provide focused
|
|
56350
|
+
* context (key findings, constraints, partial results) without exposing the
|
|
56351
|
+
* full text of previously rejected outputs.
|
|
56352
|
+
*/
|
|
56353
|
+
function appendGoalNotes(output, notes) {
|
|
56354
|
+
if (notes.length === 0) return output;
|
|
56355
|
+
return `${output}\n\n## Cross-turn working notes\n${notes.map((note) => `• ${note.content}`).join("\n")}`;
|
|
56356
|
+
}
|
|
56357
|
+
const execFileAsync = promisify(execFile);
|
|
56358
|
+
/**
|
|
56359
|
+
* Fetch a concise git diff stat against HEAD for the current working directory.
|
|
56360
|
+
* Using HEAD includes both staged and unstaged changes, so the reviewer sees
|
|
56361
|
+
* every file touched during the turn even if the agent ran `git add`. Returns
|
|
56362
|
+
* `null` when there are no changes. Throws when git is unavailable or the
|
|
56363
|
+
* directory is not a git repository.
|
|
56364
|
+
*/
|
|
56365
|
+
/** Exported for testing only. */
|
|
56366
|
+
async function fetchGitDiffStat(cwd) {
|
|
56367
|
+
const { stdout } = await execFileAsync("git", [
|
|
56368
|
+
"diff",
|
|
56369
|
+
"--stat",
|
|
56370
|
+
"HEAD"
|
|
56371
|
+
], { cwd });
|
|
56372
|
+
const stat = stdout.trim();
|
|
56373
|
+
return stat.length > 0 ? stat : null;
|
|
56374
|
+
}
|
|
56375
|
+
/**
|
|
56376
|
+
* Append a git diff stat to the grader input so the reviewer can correlate the
|
|
56377
|
+
* agent's claims with the actual files changed during the turn. Very large
|
|
56378
|
+
* stats are truncated to avoid overflowing the reviewer's context window. When
|
|
56379
|
+
* git is unavailable, a note is appended so the reviewer knows why no diff is
|
|
56380
|
+
* present.
|
|
56381
|
+
*/
|
|
56382
|
+
async function appendGitDiffStat(output, cwd) {
|
|
56383
|
+
let stat;
|
|
56384
|
+
try {
|
|
56385
|
+
stat = await fetchGitDiffStat(cwd);
|
|
56386
|
+
} catch {
|
|
56387
|
+
return `${output}\n\n## Changes this turn\n(Git diff unavailable — workspace may not be a git repository or git is not installed.)`;
|
|
56388
|
+
}
|
|
56389
|
+
if (stat === null) return output;
|
|
56390
|
+
return `${output}\n\n## Changes this turn\n${stat.length > MAX_DIFF_STAT_CHARS ? `${stat.slice(0, MAX_DIFF_STAT_CHARS)}…\n(diff stat truncated)` : stat}`;
|
|
56391
|
+
}
|
|
56213
56392
|
var UpdateGoalTool = class {
|
|
56214
56393
|
agent;
|
|
56215
56394
|
grader;
|
|
@@ -56256,7 +56435,7 @@ var UpdateGoalTool = class {
|
|
|
56256
56435
|
async handleComplete(goal) {
|
|
56257
56436
|
const goalState = goal.getGoal().goal;
|
|
56258
56437
|
if (!goalState) return { output: "No active goal." };
|
|
56259
|
-
const
|
|
56438
|
+
const outputWithContext = await appendGitDiffStat(appendGoalNotes(extractRecentOutput(this.agent.context.history), goalState.notes), this.agent.config?.cwd ?? "");
|
|
56260
56439
|
try {
|
|
56261
56440
|
await goal.pauseGoal({ reason: "verifying" }, "system");
|
|
56262
56441
|
} catch (error) {
|
|
@@ -56264,7 +56443,7 @@ var UpdateGoalTool = class {
|
|
|
56264
56443
|
}
|
|
56265
56444
|
let rawGrade;
|
|
56266
56445
|
try {
|
|
56267
|
-
rawGrade = await this.grader(goalState.objective, goalState.completionCriterion,
|
|
56446
|
+
rawGrade = await this.grader(goalState.objective, goalState.completionCriterion, outputWithContext);
|
|
56268
56447
|
} catch (error) {
|
|
56269
56448
|
const resumeError = await resumeAfterGrading(goal);
|
|
56270
56449
|
if (resumeError !== void 0) return resumeError;
|
|
@@ -56289,6 +56468,7 @@ var UpdateGoalTool = class {
|
|
|
56289
56468
|
if (grade.pass) {
|
|
56290
56469
|
try {
|
|
56291
56470
|
const completed = await goal.markComplete({}, "model");
|
|
56471
|
+
graderEmissionGuard.resetGoal(goalState.objective);
|
|
56292
56472
|
if (completed === null) return toolError("Failed to mark verified goal complete", goal);
|
|
56293
56473
|
this.agent.context.appendSystemReminder(buildGoalCompletionSummaryPrompt(completed), {
|
|
56294
56474
|
kind: "system_trigger",
|
|
@@ -56302,8 +56482,12 @@ var UpdateGoalTool = class {
|
|
|
56302
56482
|
stopTurn: true
|
|
56303
56483
|
};
|
|
56304
56484
|
}
|
|
56305
|
-
|
|
56306
|
-
|
|
56485
|
+
const denoised = graderEmissionGuard.filter(grade.reason, goalState.objective);
|
|
56486
|
+
if (denoised !== null) {
|
|
56487
|
+
this.appendGradingFeedback(denoised);
|
|
56488
|
+
return { output: `Verification failed: ${denoised}. Continue working.` };
|
|
56489
|
+
}
|
|
56490
|
+
return { output: "Previous verification feedback still applies. Address the earlier feedback and retry." };
|
|
56307
56491
|
}
|
|
56308
56492
|
appendGradingFeedback(reason) {
|
|
56309
56493
|
this.agent.context.appendSystemReminder(buildGradingFeedbackPrompt(reason), {
|
|
@@ -56340,11 +56524,11 @@ function errorMessage$4(error) {
|
|
|
56340
56524
|
}
|
|
56341
56525
|
//#endregion
|
|
56342
56526
|
//#region ../../packages/agent-core/src/tools/builtin/goal/write-goal-note.ts
|
|
56343
|
-
const WriteGoalNoteInputSchema = z.object({ content: z.string().min(1).max(
|
|
56527
|
+
const WriteGoalNoteInputSchema = z.object({ content: z.string().min(1).max(400).describe("A concise note about what you learned, verified, or decided. Notes are injected into future continuation turns so you can build on prior work.") }).strict();
|
|
56344
56528
|
var WriteGoalNoteTool = class {
|
|
56345
56529
|
agent;
|
|
56346
56530
|
name = "WriteGoalNote";
|
|
56347
|
-
description = "Record a working note during goal execution.
|
|
56531
|
+
description = "Record a working note during goal execution. Use this for dead ends you hit, decisions you made, or anything future-you should not re-derive.";
|
|
56348
56532
|
parameters = toInputJsonSchema(WriteGoalNoteInputSchema);
|
|
56349
56533
|
constructor(agent) {
|
|
56350
56534
|
this.agent = agent;
|
|
@@ -57577,6 +57761,16 @@ function parseMemoryMemos(text) {
|
|
|
57577
57761
|
}
|
|
57578
57762
|
return memos;
|
|
57579
57763
|
}
|
|
57764
|
+
/**
|
|
57765
|
+
* Strip injected memory-memo content from text before writing it back to the
|
|
57766
|
+
* store, preventing feedback loops where recalled/compacted content gets
|
|
57767
|
+
* re-stored. Removes fenced ```memory-memo blocks (the format emitted by
|
|
57768
|
+
* compaction and exit-time extraction) and any <memories>...</memories>
|
|
57769
|
+
* wrapper an injector might use.
|
|
57770
|
+
*/
|
|
57771
|
+
function stripMemoryTags(text) {
|
|
57772
|
+
return text.replaceAll(/```memory-memo[\s\S]*?```/gi, "").replaceAll(/<memories>[\s\S]*?<\/memories>/gi, "").trim();
|
|
57773
|
+
}
|
|
57580
57774
|
/** System prompt for exit-time extraction — instructs the LLM how to extract. */
|
|
57581
57775
|
const EXIT_EXTRACTION_SYSTEM_PROMPT = "你是一个任务经验提取助手。任务是从对话记录中识别已完成的任务闭环,提炼出任务经验记录。用对话的主要语言输出(中文对话用中文,英文对话用英文)。只输出指定的 JSON 格式,不要调用任何工具。";
|
|
57582
57776
|
/** Build the user prompt for exit-time extraction, including a conversation sample. */
|
|
@@ -58210,7 +58404,7 @@ const MemoryLookupInputSchema = z.object({
|
|
|
58210
58404
|
var MemoryLookupTool = class {
|
|
58211
58405
|
agent;
|
|
58212
58406
|
name = "MemoryLookup";
|
|
58213
|
-
description = "Search the memory memo store for historical experiences from past user tasks. Call this when the current task may benefit from prior work, when you encounter a repeating error or pattern, or when you are unsure of the best approach. Returns memos ranked by relevance, including the approach taken, the outcome, what failed, what worked, project, and tags.
|
|
58407
|
+
description = "Search the memory memo store for historical experiences from past user tasks. Call this when the current task may benefit from prior work, when you encounter a repeating error or pattern, or when you are unsure of the best approach. Returns memos ranked by relevance, including the approach taken, the outcome, what failed, what worked, project, and tags.";
|
|
58214
58408
|
parameters = toInputJsonSchema(MemoryLookupInputSchema);
|
|
58215
58409
|
constructor(agent) {
|
|
58216
58410
|
this.agent = agent;
|
|
@@ -58312,7 +58506,7 @@ const MemoryWriteInputSchema = z.object({
|
|
|
58312
58506
|
var MemoryWriteTool = class {
|
|
58313
58507
|
agent;
|
|
58314
58508
|
name = "MemoryWrite";
|
|
58315
|
-
description = "Write a new memory memo to the global memory memo store. Call this when the user explicitly asks to save an experience, lesson, or summary to memory, for example \"保存到记忆\", \"保存到备忘录\", \"总结并保存\", \"永久记忆\", \"记录我的记忆\", \"记住这个\", \"添加到记忆\", or \"存入记忆库\".
|
|
58509
|
+
description = "Write a new memory memo to the global memory memo store. Call this when the user explicitly asks to save an experience, lesson, or summary to memory, for example \"保存到记忆\", \"保存到备忘录\", \"总结并保存\", \"永久记忆\", \"记录我的记忆\", \"记住这个\", \"添加到记忆\", or \"存入记忆库\".";
|
|
58316
58510
|
parameters = toInputJsonSchema(MemoryWriteInputSchema);
|
|
58317
58511
|
constructor(agent) {
|
|
58318
58512
|
this.agent = agent;
|
|
@@ -58329,17 +58523,20 @@ var MemoryWriteTool = class {
|
|
|
58329
58523
|
};
|
|
58330
58524
|
const sessionId = this.agent.homedir ? basename$1(dirname$2(dirname$2(this.agent.homedir))) : "unknown";
|
|
58331
58525
|
const sourceSessionTitle = await this.agent.getSessionTitle();
|
|
58332
|
-
const
|
|
58333
|
-
const
|
|
58334
|
-
const
|
|
58526
|
+
const userNeed = stripMemoryTags(args.userNeed);
|
|
58527
|
+
const approach = stripMemoryTags(args.approach);
|
|
58528
|
+
const outcome = stripMemoryTags(args.outcome);
|
|
58529
|
+
const whatFailed = stripMemoryTags(args.whatFailed ?? "");
|
|
58530
|
+
const whatWorked = stripMemoryTags(args.whatWorked ?? "");
|
|
58531
|
+
const tags = normalizeTags(args.tags !== void 0 && args.tags.length > 0 ? args.tags : generateTags(`${userNeed} ${approach}`));
|
|
58335
58532
|
const memo = createMemoryMemo({
|
|
58336
58533
|
sourceSessionId: sessionId,
|
|
58337
58534
|
sourceSessionTitle,
|
|
58338
|
-
userNeed
|
|
58339
|
-
approach
|
|
58340
|
-
outcome
|
|
58341
|
-
whatFailed: whatFailed
|
|
58342
|
-
whatWorked: whatWorked
|
|
58535
|
+
userNeed,
|
|
58536
|
+
approach,
|
|
58537
|
+
outcome,
|
|
58538
|
+
whatFailed: whatFailed.length === 0 ? "none" : whatFailed,
|
|
58539
|
+
whatWorked: whatWorked.length === 0 ? "none" : whatWorked,
|
|
58343
58540
|
tags,
|
|
58344
58541
|
extractionSource: "manual",
|
|
58345
58542
|
projectDir: this.agent.config.cwd
|
|
@@ -58875,8 +59072,7 @@ var LspTool = class {
|
|
|
58875
59072
|
lspRegistry;
|
|
58876
59073
|
name = "LSP";
|
|
58877
59074
|
description = [
|
|
58878
|
-
"Query a language server for code intelligence.",
|
|
58879
|
-
"Use 'references' to find all usages of a symbol, 'definition' to jump to where a symbol is defined, 'diagnostics' to get type errors and warnings for a file, and 'rename' to rename a symbol across all its references.",
|
|
59075
|
+
"Query a language server for code intelligence: find usages, jump to definitions, get diagnostics, or rename a symbol across all references.",
|
|
58880
59076
|
"The language server is started automatically for supported file types (TypeScript/JavaScript, Python, Rust, Go).",
|
|
58881
59077
|
"Rename requires the typescript-language-server (or equivalent) binary on PATH for the file type."
|
|
58882
59078
|
].join(" ");
|
|
@@ -71155,7 +71351,7 @@ function validateSkillPlan(plan, nameHint) {
|
|
|
71155
71351
|
}
|
|
71156
71352
|
//#endregion
|
|
71157
71353
|
//#region ../../packages/agent-core/src/tools/builtin/collaboration/wolfpack.md
|
|
71158
|
-
var wolfpack_default = "Use WolfPack to spawn multiple subagents in parallel for batch operations.\nThis is ideal when processing many independent items (files, checks, searches)\nthat all use the same subagent type and follow a similar pattern.\n\
|
|
71354
|
+
var wolfpack_default = "Use WolfPack to spawn multiple subagents in parallel for batch operations.\nThis is ideal when processing many independent items (files, checks, searches)\nthat all use the same subagent type and follow a similar pattern.\n\nItems must be independent - no subagent depends on another's output.\nIf items depend on each other, use separate Agent calls instead.\n\nChoosing subagent_type for the batch:\n- Batch code review, audit, or bug-finding across files -> reviewer\n- Batch writing, reports, or long-form content -> writer\n- Batch read-only exploration (find files, grep, understand modules) -> explore\n- Batch verification (run build/test/lint per item) -> verify\n- Batch deep debugging or architecture decisions -> oracle\n- Batch planning or design work -> plan\n- General engineering tasks with no specialised match -> coder (default)\n\nExample: review source files for OWASP vulnerabilities by setting items to the file\npaths, subagent_type to \"reviewer\", and prompt_template to the review instruction.\nAll items are processed in parallel.\n";
|
|
71159
71355
|
//#endregion
|
|
71160
71356
|
//#region ../../packages/agent-core/src/tools/builtin/collaboration/wolfpack.ts
|
|
71161
71357
|
/**
|
|
@@ -71353,6 +71549,68 @@ function withTimeout$1(promise, timeoutMs, parentSignal) {
|
|
|
71353
71549
|
});
|
|
71354
71550
|
}
|
|
71355
71551
|
//#endregion
|
|
71552
|
+
//#region ../../packages/agent-core/src/tools/support/scan-cache.ts
|
|
71553
|
+
var FsScanCache = class {
|
|
71554
|
+
cache = /* @__PURE__ */ new Map();
|
|
71555
|
+
ttlMs;
|
|
71556
|
+
maxEntries;
|
|
71557
|
+
constructor({ ttlMs = 1e3, maxEntries = 16 } = {}) {
|
|
71558
|
+
this.ttlMs = ttlMs;
|
|
71559
|
+
this.maxEntries = maxEntries;
|
|
71560
|
+
}
|
|
71561
|
+
key(root, pattern, includeDirs) {
|
|
71562
|
+
return `${root}\0${pattern}\0${String(includeDirs)}`;
|
|
71563
|
+
}
|
|
71564
|
+
/**
|
|
71565
|
+
* Returns the cached output if present and not expired. Expired entries are
|
|
71566
|
+
* deleted on access.
|
|
71567
|
+
*/
|
|
71568
|
+
get(root, pattern, includeDirs) {
|
|
71569
|
+
const k = this.key(root, pattern, includeDirs);
|
|
71570
|
+
const entry = this.cache.get(k);
|
|
71571
|
+
if (entry === void 0) return void 0;
|
|
71572
|
+
if (Date.now() - entry.createdAt > this.ttlMs) {
|
|
71573
|
+
this.cache.delete(k);
|
|
71574
|
+
return;
|
|
71575
|
+
}
|
|
71576
|
+
return entry.output;
|
|
71577
|
+
}
|
|
71578
|
+
/** Cache a scan output. Evicts the oldest entry when at capacity. */
|
|
71579
|
+
set(root, pattern, includeDirs, output) {
|
|
71580
|
+
if (this.cache.size >= this.maxEntries) {
|
|
71581
|
+
let oldestKey;
|
|
71582
|
+
let oldestTime = Infinity;
|
|
71583
|
+
for (const [k, v] of this.cache) if (v.createdAt < oldestTime) {
|
|
71584
|
+
oldestTime = v.createdAt;
|
|
71585
|
+
oldestKey = k;
|
|
71586
|
+
}
|
|
71587
|
+
if (oldestKey !== void 0) this.cache.delete(oldestKey);
|
|
71588
|
+
}
|
|
71589
|
+
this.cache.set(this.key(root, pattern, includeDirs), {
|
|
71590
|
+
output,
|
|
71591
|
+
createdAt: Date.now()
|
|
71592
|
+
});
|
|
71593
|
+
}
|
|
71594
|
+
/** Invalidate all cached entries rooted under the given directory path. */
|
|
71595
|
+
invalidateByRoot(root) {
|
|
71596
|
+
const prefix = `${root}\0`;
|
|
71597
|
+
for (const k of this.cache.keys()) if (k.startsWith(prefix)) this.cache.delete(k);
|
|
71598
|
+
}
|
|
71599
|
+
/** Clear the entire cache (useful for tests). */
|
|
71600
|
+
clear() {
|
|
71601
|
+
this.cache.clear();
|
|
71602
|
+
}
|
|
71603
|
+
/** Number of cached entries (for tests / introspection). */
|
|
71604
|
+
size() {
|
|
71605
|
+
return this.cache.size;
|
|
71606
|
+
}
|
|
71607
|
+
};
|
|
71608
|
+
/**
|
|
71609
|
+
* Module-level singleton shared by Glob (producer) and Edit/Write (invalidator).
|
|
71610
|
+
* Importing this instance ensures all tools reference the same cache.
|
|
71611
|
+
*/
|
|
71612
|
+
const scanCache = new FsScanCache();
|
|
71613
|
+
//#endregion
|
|
71356
71614
|
//#region ../../packages/agent-core/src/tools/builtin/file/conflict-detect.ts
|
|
71357
71615
|
/**
|
|
71358
71616
|
* Detect unresolved git merge conflict markers in read output.
|
|
@@ -71599,7 +71857,7 @@ function hashEditPayload(input) {
|
|
|
71599
71857
|
}
|
|
71600
71858
|
//#endregion
|
|
71601
71859
|
//#region ../../packages/agent-core/src/tools/builtin/file/edit.md
|
|
71602
|
-
var edit_default = "Perform exact string replacements against the text view returned by Read.\n\n-
|
|
71860
|
+
var edit_default = "Perform exact string replacements against the text view returned by Read.\n\n- By default, old_string must occur exactly once. If it matches multiple locations, add surrounding context.\n- Prefer Edit for targeted changes to existing files; use Write only for new files or complete overwrites.\n- To modify a file, always use Edit; do not run a Shell `sed` command for edits.\n- When making several independent changes, issue multiple Edit calls in parallel within a single response; edits to the same file are serialized automatically by a write lock.\n- When several parallel Edit calls target the same file, a write lock serializes them; they apply in the order the calls appear in your response. An edit fails with `old_string not found` if its old_string was taken from text an earlier edit already replaced - base every old_string on the latest Read view and order dependent edits accordingly.\n- Edit refuses if `old_string` lands inside an unresolved merge conflict block, or if `new_string` would introduce `<<<<<<<`/`=======`/`>>>>>>>` markers. To clean up a conflict, include the markers in `old_string` so Edit replaces them.\n";
|
|
71603
71861
|
//#endregion
|
|
71604
71862
|
//#region ../../packages/agent-core/src/tools/builtin/file/edit.ts
|
|
71605
71863
|
const EditInputSchema = z.object({
|
|
@@ -71648,10 +71906,11 @@ var EditTool = class {
|
|
|
71648
71906
|
pathClass: this.jian.pathClass(),
|
|
71649
71907
|
homeDir: this.jian.gethome()
|
|
71650
71908
|
}),
|
|
71651
|
-
execute: () => this.execution(args, path)
|
|
71909
|
+
execute: ({ signal }) => this.execution(args, path, signal)
|
|
71652
71910
|
};
|
|
71653
71911
|
}
|
|
71654
|
-
async execution(args, safePath) {
|
|
71912
|
+
async execution(args, safePath, signal) {
|
|
71913
|
+
signal?.throwIfAborted();
|
|
71655
71914
|
const result = await this.executionCore(args, safePath);
|
|
71656
71915
|
const inputHash = hashEditPayload({
|
|
71657
71916
|
path: args.path,
|
|
@@ -71748,6 +72007,7 @@ var EditTool = class {
|
|
|
71748
72007
|
}
|
|
71749
72008
|
const newContent = replaceOnceLiteral(content, args.old_string, args.new_string);
|
|
71750
72009
|
await this.jian.writeText(safePath, materializeModelText(newContent, modelView.lineEndingStyle));
|
|
72010
|
+
scanCache.clear();
|
|
71751
72011
|
const { notice, hasErrors } = await this.appendDiagnostics(safePath);
|
|
71752
72012
|
const output = `Replaced 1 occurrence in ${args.path}${notice}`;
|
|
71753
72013
|
return hasErrors ? {
|
|
@@ -71766,6 +72026,7 @@ var EditTool = class {
|
|
|
71766
72026
|
}
|
|
71767
72027
|
const newContent = parts.join(args.new_string);
|
|
71768
72028
|
await this.jian.writeText(safePath, materializeModelText(newContent, modelView.lineEndingStyle));
|
|
72029
|
+
scanCache.clear();
|
|
71769
72030
|
const { notice, hasErrors } = await this.appendDiagnostics(safePath);
|
|
71770
72031
|
const output = `Replaced ${String(replacementCount)} occurrences in ${args.path}${notice}`;
|
|
71771
72032
|
return hasErrors ? {
|
|
@@ -71862,7 +72123,7 @@ async function listDirectory(jian, workDir = jian.getcwd()) {
|
|
|
71862
72123
|
}
|
|
71863
72124
|
//#endregion
|
|
71864
72125
|
//#region ../../packages/agent-core/src/tools/builtin/file/glob.md
|
|
71865
|
-
var glob_default = "Find files (and optionally directories) by glob pattern, sorted by modification time (most recent first).\n\nGood patterns:\n- `*.ts` — files in the current directory matching an extension\n- `src/**/*.ts` — recursive with a subdirectory anchor and extension\n- `test_*.py` — files whose name starts with a literal prefix\n\nRejected patterns (no literal anchor — nothing bounds the result set):\n- `**`, `**/*`, `*/*` — pure wildcards. Add an extension or subdirectory to give the walk a concrete target.\n- Anything that starts with `**/` (e.g. `**/*.md`, `**/main/*.py`). The leading `**/` has no literal anchor in front of it. Anchor it with a top-level subdirectory like `src/**/*.md`.\n- `*.{ts,tsx}` — brace expansion is not supported. Issue two calls: `*.ts` and `*.tsx`.\n\nLarge-directory warning — avoid recursing into dependency/build output even with an anchor:\n- `node_modules/**/*.js`, `.venv/**/*.py`, `__pycache__/**`, `target/**` all match technically but\n typically produce thousands of results that truncate at the match cap and waste the caller context.\n Prefer specific subpaths like `node_modules/react/src/**/*.js`.";
|
|
72126
|
+
var glob_default = "Find files (and optionally directories) by glob pattern, sorted by modification time (most recent first).\n\nGood patterns:\n- `*.ts` — files in the current directory matching an extension\n- `src/**/*.ts` — recursive with a subdirectory anchor and extension\n- `test_*.py` — files whose name starts with a literal prefix\n\nRejected patterns (no literal anchor — nothing bounds the result set):\n- `**`, `**/*`, `*/*` — pure wildcards. Add an extension or subdirectory to give the walk a concrete target.\n- Anything that starts with `**/` (e.g. `**/*.md`, `**/main/*.py`). The leading `**/` has no literal anchor in front of it. Anchor it with a top-level subdirectory like `src/**/*.md`.\n- `*.{ts,tsx}` — brace expansion is not supported. Issue two calls: `*.ts` and `*.tsx`.\n\nLarge-directory warning — avoid recursing into dependency/build output even with an anchor:\n- `node_modules/**/*.js`, `.venv/**/*.py`, `__pycache__/**`, `target/**` all match technically but\n typically produce thousands of results that truncate at the {{ MAX_MATCHES }}-match cap and waste the caller context.\n Prefer specific subpaths like `node_modules/react/src/**/*.js`.";
|
|
71866
72127
|
//#endregion
|
|
71867
72128
|
//#region ../../packages/agent-core/src/tools/builtin/file/glob.ts
|
|
71868
72129
|
const GlobInputSchema = z.object({
|
|
@@ -71871,6 +72132,7 @@ const GlobInputSchema = z.object({
|
|
|
71871
72132
|
include_dirs: z.boolean().default(true).optional().describe("Whether to include directories in results. Defaults to true. Set false to return only files.")
|
|
71872
72133
|
});
|
|
71873
72134
|
const MAX_MATCHES = 1e3;
|
|
72135
|
+
const GLOB_DESCRIPTION = renderPrompt(glob_default, { MAX_MATCHES });
|
|
71874
72136
|
/**
|
|
71875
72137
|
* Path-shape hint appended to the tool description only on a Windows
|
|
71876
72138
|
* (`win32` path class) backend. The `path` argument accepts both native
|
|
@@ -71900,7 +72162,7 @@ var GlobTool = class {
|
|
|
71900
72162
|
constructor(jian, workspace) {
|
|
71901
72163
|
this.jian = jian;
|
|
71902
72164
|
this.workspace = workspace;
|
|
71903
|
-
this.description = this.jian.pathClass() === "win32" ?
|
|
72165
|
+
this.description = this.jian.pathClass() === "win32" ? GLOB_DESCRIPTION + WINDOWS_PATH_HINT : GLOB_DESCRIPTION;
|
|
71904
72166
|
}
|
|
71905
72167
|
async resolveExecution(args) {
|
|
71906
72168
|
let path;
|
|
@@ -71924,10 +72186,11 @@ var GlobTool = class {
|
|
|
71924
72186
|
},
|
|
71925
72187
|
approvalRule: literalRulePattern(this.name, args.pattern),
|
|
71926
72188
|
matchesRule: (ruleArgs) => matchesGlobRuleSubject(ruleArgs, args.pattern),
|
|
71927
|
-
execute: () => this.execution(args, searchRoots)
|
|
72189
|
+
execute: ({ signal }) => this.execution(args, searchRoots, signal)
|
|
71928
72190
|
};
|
|
71929
72191
|
}
|
|
71930
|
-
async execution(args, searchRoots) {
|
|
72192
|
+
async execution(args, searchRoots, signal) {
|
|
72193
|
+
signal?.throwIfAborted();
|
|
71931
72194
|
if (startsWithDoubleStarPrefix(args.pattern)) {
|
|
71932
72195
|
let tree;
|
|
71933
72196
|
try {
|
|
@@ -71976,6 +72239,8 @@ var GlobTool = class {
|
|
|
71976
72239
|
}
|
|
71977
72240
|
}
|
|
71978
72241
|
try {
|
|
72242
|
+
const cachedOutput = scanCache.get(searchRoots[0], args.pattern, includeDirs);
|
|
72243
|
+
if (cachedOutput !== void 0) return { output: cachedOutput };
|
|
71979
72244
|
const seen = /* @__PURE__ */ new Set();
|
|
71980
72245
|
const entries = [];
|
|
71981
72246
|
const YIELD_SAFETY_CAP = MAX_MATCHES * 2;
|
|
@@ -71983,6 +72248,7 @@ var GlobTool = class {
|
|
|
71983
72248
|
let truncated = false;
|
|
71984
72249
|
outer: for (const root of searchRoots) for await (const filePath of this.jian.glob(root, args.pattern, { allowedRoots: [root] })) {
|
|
71985
72250
|
yielded++;
|
|
72251
|
+
if (signal && yielded % 128 === 0) signal.throwIfAborted();
|
|
71986
72252
|
if (yielded >= YIELD_SAFETY_CAP) {
|
|
71987
72253
|
truncated = true;
|
|
71988
72254
|
break outer;
|
|
@@ -72011,16 +72277,22 @@ var GlobTool = class {
|
|
|
72011
72277
|
const pathClass = this.jian.pathClass();
|
|
72012
72278
|
const relBase = searchRoots[0] ?? this.workspace.workspaceDir;
|
|
72013
72279
|
const displayLines = paths.map((p) => relativizeIfUnder$1(p, relBase, pathClass));
|
|
72014
|
-
|
|
72015
|
-
|
|
72016
|
-
|
|
72017
|
-
lines
|
|
72018
|
-
|
|
72280
|
+
let output;
|
|
72281
|
+
if (entries.length === 0 && !truncated) output = "No matches found";
|
|
72282
|
+
else {
|
|
72283
|
+
const lines = [];
|
|
72284
|
+
if (truncated) {
|
|
72285
|
+
lines.push(`[Truncated at ${String(MAX_MATCHES)} matches — use a more specific pattern]`);
|
|
72286
|
+
lines.push(`Only the first ${String(MAX_MATCHES)} matches are returned.`);
|
|
72287
|
+
}
|
|
72288
|
+
lines.push(...displayLines);
|
|
72289
|
+
if (!truncated && entries.length === 1e3) lines.push(`Found ${String(entries.length)} matches`);
|
|
72290
|
+
output = lines.join("\n");
|
|
72019
72291
|
}
|
|
72020
|
-
|
|
72021
|
-
|
|
72022
|
-
return { output: lines.join("\n") };
|
|
72292
|
+
scanCache.set(searchRoots[0], args.pattern, includeDirs, output);
|
|
72293
|
+
return { output };
|
|
72023
72294
|
} catch (error) {
|
|
72295
|
+
if (signal?.aborted) throw error;
|
|
72024
72296
|
if (error !== null && typeof error === "object" && "code" in error) {
|
|
72025
72297
|
const code = error.code;
|
|
72026
72298
|
const path = searchRoots[0] ?? this.workspace.workspaceDir;
|
|
@@ -75297,6 +75569,85 @@ function rgUnavailableMessage(cause) {
|
|
|
75297
75569
|
Error: ${cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "unknown error"}\n\nFix options:\n macOS: brew install ripgrep\n Ubuntu: sudo apt-get install ripgrep\n Other: https://github.com/BurntSushi/ripgrep#installation\n\nAlternatively, drop a static rg binary at ${join$1(getShareDir(), "bin", rgBinaryName())}`;
|
|
75298
75570
|
}
|
|
75299
75571
|
//#endregion
|
|
75572
|
+
//#region ../../packages/agent-core/src/tools/support/rg-pattern-sanitize.ts
|
|
75573
|
+
/**
|
|
75574
|
+
* Ripgrep pattern sanitization and error detection.
|
|
75575
|
+
*
|
|
75576
|
+
* Users frequently search for code fragments that contain regex metacharacters
|
|
75577
|
+
* (e.g. `fetchFoo(`, `${platform}`, `a{3`). ripgrep uses the Rust `regex`
|
|
75578
|
+
* crate, which rejects unescaped `{`/`}` that do not form a valid repetition
|
|
75579
|
+
* quantifier (`{N}`, `{N,}`, `{N,M}`). This module pre-cleans such braces so
|
|
75580
|
+
* the common case "search for a literal-ish fragment" succeeds without a
|
|
75581
|
+
* syntax error.
|
|
75582
|
+
*
|
|
75583
|
+
* When sanitization is insufficient (e.g. unbalanced parentheses), the caller
|
|
75584
|
+
* can detect the regex error from ripgrep stderr and retry with
|
|
75585
|
+
* `--fixed-strings`, which treats the pattern as a literal string.
|
|
75586
|
+
*/
|
|
75587
|
+
/** Matches the inner content of a valid repetition quantifier: `N`, `N,`, `N,M`. */
|
|
75588
|
+
const QUANTIFIER_INNER_RE = /^\d+(?:,\d*)?$/;
|
|
75589
|
+
/**
|
|
75590
|
+
* Escape `{` and `}` that cannot form a valid repetition quantifier so they
|
|
75591
|
+
* are treated as literals by the regex engine.
|
|
75592
|
+
*
|
|
75593
|
+
* Already-escaped braces (`\{`, `\}`) are preserved. Valid quantifiers
|
|
75594
|
+
* (`{3}`, `{2,}`, `{1,5}`) are preserved. Everything else is escaped.
|
|
75595
|
+
*
|
|
75596
|
+
* Escaping braces inside character classes (e.g. `[{]`) is also safe:
|
|
75597
|
+
* `\{` inside `[...]` matches a literal `{` just like an unescaped `{` would.
|
|
75598
|
+
*/
|
|
75599
|
+
function sanitizeRgPattern(pattern) {
|
|
75600
|
+
let result = "";
|
|
75601
|
+
let i = 0;
|
|
75602
|
+
while (i < pattern.length) {
|
|
75603
|
+
const ch = pattern[i];
|
|
75604
|
+
if (ch === "\\" && i + 1 < pattern.length) {
|
|
75605
|
+
result += pattern.slice(i, i + 2);
|
|
75606
|
+
i += 2;
|
|
75607
|
+
continue;
|
|
75608
|
+
}
|
|
75609
|
+
if (ch === "{") {
|
|
75610
|
+
const closeIdx = pattern.indexOf("}", i + 1);
|
|
75611
|
+
if (closeIdx !== -1) {
|
|
75612
|
+
const inner = pattern.slice(i + 1, closeIdx);
|
|
75613
|
+
if (QUANTIFIER_INNER_RE.test(inner)) {
|
|
75614
|
+
result += pattern.slice(i, closeIdx + 1);
|
|
75615
|
+
i = closeIdx + 1;
|
|
75616
|
+
continue;
|
|
75617
|
+
}
|
|
75618
|
+
}
|
|
75619
|
+
result += "\\{";
|
|
75620
|
+
i++;
|
|
75621
|
+
continue;
|
|
75622
|
+
}
|
|
75623
|
+
if (ch === "}") {
|
|
75624
|
+
result += "\\}";
|
|
75625
|
+
i++;
|
|
75626
|
+
continue;
|
|
75627
|
+
}
|
|
75628
|
+
result += ch;
|
|
75629
|
+
i++;
|
|
75630
|
+
}
|
|
75631
|
+
return result;
|
|
75632
|
+
}
|
|
75633
|
+
/** Substrings that indicate ripgrep failed to parse the regex pattern. */
|
|
75634
|
+
const REGEX_ERROR_MARKERS = [
|
|
75635
|
+
"regex parse error",
|
|
75636
|
+
"unrecognized escape",
|
|
75637
|
+
"unclosed group",
|
|
75638
|
+
"unbalanced",
|
|
75639
|
+
"unexpected repetition"
|
|
75640
|
+
];
|
|
75641
|
+
/**
|
|
75642
|
+
* Detect whether ripgrep's stderr indicates a regex syntax error (as opposed
|
|
75643
|
+
* to a filesystem or runtime error). Used to decide whether a
|
|
75644
|
+
* `--fixed-strings` fallback retry is worthwhile.
|
|
75645
|
+
*/
|
|
75646
|
+
function isRegexSyntaxError(stderr) {
|
|
75647
|
+
const lower = stderr.toLowerCase();
|
|
75648
|
+
return REGEX_ERROR_MARKERS.some((marker) => lower.includes(marker));
|
|
75649
|
+
}
|
|
75650
|
+
//#endregion
|
|
75300
75651
|
//#region ../../packages/agent-core/src/tools/support/result-builder.ts
|
|
75301
75652
|
const DEFAULT_MAX_CHARS = 5e4;
|
|
75302
75653
|
const DEFAULT_TAIL_CHARS = 2e4;
|
|
@@ -75307,17 +75658,25 @@ var ToolResultBuilder = class {
|
|
|
75307
75658
|
maxChars;
|
|
75308
75659
|
maxTailChars;
|
|
75309
75660
|
maxLineLength;
|
|
75661
|
+
artifactSink;
|
|
75662
|
+
maxFullOutputChars;
|
|
75310
75663
|
buffer = [];
|
|
75311
75664
|
nCharsValue = 0;
|
|
75312
75665
|
truncationHappened = false;
|
|
75313
75666
|
headTruncated = false;
|
|
75314
75667
|
tailBuf = [];
|
|
75315
75668
|
tailCharsValue = 0;
|
|
75669
|
+
fullOutput;
|
|
75670
|
+
fullOutputChars = 0;
|
|
75671
|
+
totalLinesWritten = 0;
|
|
75316
75672
|
constructor(options = {}) {
|
|
75317
75673
|
this.maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
|
|
75318
75674
|
this.maxTailChars = options.maxTailChars ?? DEFAULT_TAIL_CHARS;
|
|
75319
75675
|
this.maxLineLength = options.maxLineLength === void 0 ? DEFAULT_MAX_LINE_LENGTH : options.maxLineLength;
|
|
75320
75676
|
if (this.maxLineLength !== null && this.maxLineLength <= 14) throw new Error("maxLineLength must be greater than the truncation marker length.");
|
|
75677
|
+
this.artifactSink = options.artifactSink;
|
|
75678
|
+
this.maxFullOutputChars = options.maxFullOutputChars ?? 1e6;
|
|
75679
|
+
if (this.artifactSink !== void 0) this.fullOutput = [];
|
|
75321
75680
|
}
|
|
75322
75681
|
get nChars() {
|
|
75323
75682
|
return this.nCharsValue + this.tailCharsValue;
|
|
@@ -75327,12 +75686,19 @@ var ToolResultBuilder = class {
|
|
|
75327
75686
|
if (!this.headTruncated || this.tailCharsValue === 0) return head;
|
|
75328
75687
|
this.trimTail();
|
|
75329
75688
|
const tail = this.tailBuf.join("");
|
|
75330
|
-
|
|
75689
|
+
const separator = head.endsWith("\n") ? "" : "\n";
|
|
75690
|
+
const elided = this.computeElidedLines(head, tail);
|
|
75691
|
+
return `${head}${separator}${elided > 0 ? `[…${String(elided)} lines elided…]\n${TRUNCATION_MARKER}` : TRUNCATION_MARKER}\n${tail}`;
|
|
75331
75692
|
}
|
|
75332
75693
|
write(text) {
|
|
75333
75694
|
if (text.length === 0) return 0;
|
|
75334
75695
|
const lines = text.match(/[^\r\n]*(?:\r\n|[\n\r])|[^\r\n]+/g) ?? [];
|
|
75335
75696
|
if (lines.length === 0) return 0;
|
|
75697
|
+
this.totalLinesWritten += lines.length;
|
|
75698
|
+
if (this.fullOutput !== void 0 && this.fullOutputChars < this.maxFullOutputChars) {
|
|
75699
|
+
this.fullOutput.push(text);
|
|
75700
|
+
this.fullOutputChars += text.length;
|
|
75701
|
+
}
|
|
75336
75702
|
let charsWritten = 0;
|
|
75337
75703
|
for (const originalLine of lines) if (this.nCharsValue < this.maxChars) {
|
|
75338
75704
|
const remainingChars = this.maxChars - this.nCharsValue;
|
|
@@ -75380,11 +75746,36 @@ var ToolResultBuilder = class {
|
|
|
75380
75746
|
this.tailBuf.push(trimmed);
|
|
75381
75747
|
this.tailCharsValue = trimmed.length;
|
|
75382
75748
|
}
|
|
75383
|
-
|
|
75749
|
+
computeElidedLines(head, tail) {
|
|
75750
|
+
if (this.totalLinesWritten === 0) return 0;
|
|
75751
|
+
if (this.fullOutput !== void 0) {
|
|
75752
|
+
const total = countLines(this.fullOutput.join(""));
|
|
75753
|
+
return Math.max(0, total - countLines(head) - countLines(tail));
|
|
75754
|
+
}
|
|
75755
|
+
return Math.max(0, this.totalLinesWritten - countLines(head) - countLines(tail));
|
|
75756
|
+
}
|
|
75757
|
+
async maybeWriteArtifact() {
|
|
75758
|
+
if (this.artifactSink === void 0 || this.fullOutput === void 0) return void 0;
|
|
75759
|
+
if (!this.truncationHappened) return void 0;
|
|
75760
|
+
const full = this.fullOutput.join("");
|
|
75761
|
+
if (full.length === 0) return void 0;
|
|
75762
|
+
try {
|
|
75763
|
+
return await this.artifactSink(full);
|
|
75764
|
+
} catch {
|
|
75765
|
+
return;
|
|
75766
|
+
}
|
|
75767
|
+
}
|
|
75768
|
+
appendArtifactRef(output, ref) {
|
|
75769
|
+
const line = `[full output saved: ${ref}]`;
|
|
75770
|
+
return output.length === 0 ? line : output.endsWith("\n") ? `${output}${line}` : `${output}\n${line}`;
|
|
75771
|
+
}
|
|
75772
|
+
async ok(message = "", options = {}) {
|
|
75384
75773
|
let finalMessage = message;
|
|
75385
75774
|
if (finalMessage.length > 0 && !finalMessage.endsWith(".")) finalMessage += ".";
|
|
75386
75775
|
if (this.truncationHappened) finalMessage = finalMessage.length === 0 ? TRUNCATION_MESSAGE : `${finalMessage} ${TRUNCATION_MESSAGE}`;
|
|
75387
|
-
const
|
|
75776
|
+
const baseOutput = this.toString();
|
|
75777
|
+
const artifactRef = await this.maybeWriteArtifact();
|
|
75778
|
+
const output = artifactRef === void 0 ? baseOutput : this.appendArtifactRef(baseOutput, artifactRef);
|
|
75388
75779
|
return {
|
|
75389
75780
|
isError: false,
|
|
75390
75781
|
output: finalMessage.length > 0 && (this.truncationHappened || output.length === 0) ? output.length === 0 ? finalMessage : output.endsWith("\n") ? `${output}${finalMessage}` : `${output}\n${finalMessage}` : output,
|
|
@@ -75393,9 +75784,11 @@ var ToolResultBuilder = class {
|
|
|
75393
75784
|
brief: options.brief
|
|
75394
75785
|
};
|
|
75395
75786
|
}
|
|
75396
|
-
error(message, options = {}) {
|
|
75787
|
+
async error(message, options = {}) {
|
|
75397
75788
|
const finalMessage = this.truncationHappened ? message.length === 0 ? TRUNCATION_MESSAGE : `${message} ${TRUNCATION_MESSAGE}` : message;
|
|
75398
|
-
const
|
|
75789
|
+
const baseOutput = this.toString();
|
|
75790
|
+
const artifactRef = await this.maybeWriteArtifact();
|
|
75791
|
+
const output = artifactRef === void 0 ? baseOutput : this.appendArtifactRef(baseOutput, artifactRef);
|
|
75399
75792
|
return {
|
|
75400
75793
|
isError: true,
|
|
75401
75794
|
output: finalMessage.length === 0 ? output : output.length === 0 ? finalMessage : output.endsWith("\n") ? `${output}${finalMessage}` : `${output}\n${finalMessage}`,
|
|
@@ -75405,9 +75798,13 @@ var ToolResultBuilder = class {
|
|
|
75405
75798
|
};
|
|
75406
75799
|
}
|
|
75407
75800
|
};
|
|
75801
|
+
function countLines(text) {
|
|
75802
|
+
if (text.length === 0) return 0;
|
|
75803
|
+
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
75804
|
+
}
|
|
75408
75805
|
//#endregion
|
|
75409
75806
|
//#region ../../packages/agent-core/src/tools/builtin/file/grep.md
|
|
75410
|
-
var grep_default = "Search file contents using regular expressions (powered by ripgrep).\n\nUse Grep when the task is to find unknown content or unknown file locations. Do not use shell `grep` or `rg` directly; this tool applies workspace path policy, output limits, and sensitive-file filtering.\nALWAYS use Grep tool instead of running `grep` or `rg` from a shell
|
|
75807
|
+
var grep_default = "Search file contents using regular expressions (powered by ripgrep).\n\nUse Grep when the task is to find unknown content or unknown file locations. Do not use shell `grep` or `rg` directly; this tool applies workspace path policy, output limits, and sensitive-file filtering.\nALWAYS use Grep tool instead of running `grep` or `rg` from a shell - direct shell calls bypass workspace policy, output limits, and sensitive-file filtering.\n\nWrite patterns in ripgrep regex syntax, which differs from POSIX `grep` syntax. For example, braces are special, so escape them as `\\{` to match a literal `{`.\n\nHidden files (dotfiles such as `.gitlab-ci.yml` or `.eslintrc.json`) are searched by default.\n";
|
|
75411
75808
|
//#endregion
|
|
75412
75809
|
//#region ../../packages/agent-core/src/tools/builtin/file/grep.ts
|
|
75413
75810
|
const GrepInputSchema = z.object({
|
|
@@ -75475,11 +75872,12 @@ const SENSITIVE_GLOBS_TO_EXCLUDE = [
|
|
|
75475
75872
|
"**/.gcp/credentials/**"
|
|
75476
75873
|
];
|
|
75477
75874
|
const CONTENT_LINE_RE = /^(.*?)([:-])(\d+)\2/;
|
|
75875
|
+
const GREP_DESCRIPTION = renderPrompt(grep_default, { DEFAULT_HEAD_LIMIT });
|
|
75478
75876
|
var GrepTool = class {
|
|
75479
75877
|
jian;
|
|
75480
75878
|
workspace;
|
|
75481
75879
|
name = "Grep";
|
|
75482
|
-
description =
|
|
75880
|
+
description = GREP_DESCRIPTION;
|
|
75483
75881
|
parameters = toInputJsonSchema(GrepInputSchema);
|
|
75484
75882
|
constructor(jian, workspace) {
|
|
75485
75883
|
this.jian = jian;
|
|
@@ -75536,6 +75934,15 @@ var GrepTool = class {
|
|
|
75536
75934
|
runResult = await runRipgrepOnce(this.jian, buildRgArgs(rgPath, args, searchPaths, true), signal);
|
|
75537
75935
|
if (runResult.kind === "tool-error") return runResult.result;
|
|
75538
75936
|
}
|
|
75937
|
+
if (runResult.exitCode !== 0 && runResult.exitCode !== 1 && !runResult.timedOut && isRegexSyntaxError(runResult.stderrText)) {
|
|
75938
|
+
const fallback = await runRipgrepOnce(this.jian, buildRgArgs(rgPath, args, searchPaths, false, true), signal);
|
|
75939
|
+
if (fallback.kind === "tool-error") return fallback.result;
|
|
75940
|
+
if (shouldRetryRipgrepEagain(fallback)) {
|
|
75941
|
+
const retry = await runRipgrepOnce(this.jian, buildRgArgs(rgPath, args, searchPaths, true, true), signal);
|
|
75942
|
+
if (retry.kind === "tool-error") return retry.result;
|
|
75943
|
+
if (retry.exitCode === 0 || retry.exitCode === 1) runResult = retry;
|
|
75944
|
+
} else if (fallback.exitCode === 0 || fallback.exitCode === 1) runResult = fallback;
|
|
75945
|
+
}
|
|
75539
75946
|
const { exitCode, stderrText, bufferTruncated, stderrTruncated, timedOut } = runResult;
|
|
75540
75947
|
let { stdoutText } = runResult;
|
|
75541
75948
|
if (exitCode !== 0 && exitCode !== 1 && !timedOut) return {
|
|
@@ -75594,7 +76001,7 @@ var GrepTool = class {
|
|
|
75594
76001
|
const combined = visibleBody === "" && messages.length === 0 ? emptyResultMessage : messages.length > 0 ? visibleBody === "" ? messages.join("\n") : `${visibleBody}\n${messages.join("\n")}` : visibleBody;
|
|
75595
76002
|
const builder = new ToolResultBuilder();
|
|
75596
76003
|
builder.write(combined);
|
|
75597
|
-
const result = builder.ok(sideChannelMessages.join("\n"));
|
|
76004
|
+
const result = await builder.ok(sideChannelMessages.join("\n"));
|
|
75598
76005
|
const display = buildSearchResultsDisplay(args, limited, mode, contentIncludesLineNumbers);
|
|
75599
76006
|
if (display === void 0) return result;
|
|
75600
76007
|
if (result.isError === true) return result;
|
|
@@ -75790,10 +76197,11 @@ async function mapWithConcurrency(items, concurrency, signal, mapper) {
|
|
|
75790
76197
|
if (signal.aborted) throw new GrepAbortedError();
|
|
75791
76198
|
return results;
|
|
75792
76199
|
}
|
|
75793
|
-
function buildRgArgs(rgPath, args, searchPaths, singleThreaded = false) {
|
|
76200
|
+
function buildRgArgs(rgPath, args, searchPaths, singleThreaded = false, literal = false) {
|
|
75794
76201
|
const cmd = [rgPath];
|
|
75795
76202
|
if (singleThreaded) cmd.push("-j", "1");
|
|
75796
76203
|
cmd.push("--hidden");
|
|
76204
|
+
if (literal) cmd.push("--fixed-strings");
|
|
75797
76205
|
const mode = args.output_mode ?? "files_with_matches";
|
|
75798
76206
|
if (mode !== "content") cmd.push("--max-columns", String(RG_MAX_COLUMNS));
|
|
75799
76207
|
cmd.push("--null");
|
|
@@ -75816,7 +76224,8 @@ function buildRgArgs(rgPath, args, searchPaths, singleThreaded = false) {
|
|
|
75816
76224
|
if (args.multiline) cmd.push("-U", "--multiline-dotall");
|
|
75817
76225
|
if (args.include_ignored) cmd.push("--no-ignore");
|
|
75818
76226
|
for (const glob of SENSITIVE_GLOBS_TO_EXCLUDE) cmd.push("--glob", `!${glob}`);
|
|
75819
|
-
|
|
76227
|
+
const pattern = literal ? args.pattern : sanitizeRgPattern(args.pattern);
|
|
76228
|
+
cmd.push("--", pattern, ...searchPaths);
|
|
75820
76229
|
return cmd;
|
|
75821
76230
|
}
|
|
75822
76231
|
function splitRgLines(text) {
|
|
@@ -76530,7 +76939,7 @@ async function partitionExistingPaths(paths, jian, workspace) {
|
|
|
76530
76939
|
}
|
|
76531
76940
|
//#endregion
|
|
76532
76941
|
//#region ../../packages/agent-core/src/tools/builtin/file/read.md
|
|
76533
|
-
var read_default = "Read a text file from the local filesystem.\n\nIf the user provides a concrete file path to a text file, call Read directly. Do not `Glob`, `ls`, or otherwise pre-check known text file paths; missing or invalid file paths return errors you can handle. Do not use Read for directories; use `ls` via Bash for a known directory, or Glob when you need files/directories matching a pattern. Use `Grep` only when the task is to search for unknown content or locations.\n\nWhen you need several files, prefer to read them in parallel: emit multiple `Read` calls in a single response instead of reading one file per turn.\n\n-
|
|
76942
|
+
var read_default = "Read a text file from the local filesystem.\n\nIf the user provides a concrete file path to a text file, call Read directly. Do not `Glob`, `ls`, or otherwise pre-check known text file paths; missing or invalid file paths return errors you can handle. Do not use Read for directories; use `ls` via Bash for a known directory, or Glob when you need files/directories matching a pattern. Use `Grep` only when the task is to search for unknown content or locations.\n\nWhen you need several files, prefer to read them in parallel: emit multiple `Read` calls in a single response instead of reading one file per turn.\n\n- Returns up to {{ MAX_BYTES_KB }} KB per call; lines longer than {{ MAX_LINE_LENGTH }} chars are truncated mid-line.\n- Sensitive files (`.env` files, credential stores, SSH keys, and similar secrets) are refused to protect secrets; do not attempt to read them.\n- Only UTF-8 text files can be read. Non-UTF-8 encodings, binary files, and files containing NUL bytes are refused; use `ReadMediaFile` for images or video, and Bash or an MCP tool for other binary formats.\n- Output format: `<line-number>\\t<content>` per line.\n- A `<system>...</system>` status block is appended after the file content; it summarizes how much was read (line and byte counts, truncation, line-ending notes) and is not part of the file itself. The status block includes an `Anchor: <hash>` value that can be passed to `Edit.anchor` to verify the file has not changed before editing.\n- Pure CRLF files are displayed with LF line endings; `Edit` matches this output and preserves CRLF when writing back.\n- Mixed or lone carriage-return line endings are shown as `\\r` and require exact `Edit.old_string` escapes.\n- After a successful `Edit`/`Write`, do not re-read solely to prove the write landed. When the task depends on an exact file, API, or output shape, inspect the final external contract before finishing.\n";
|
|
76534
76943
|
//#endregion
|
|
76535
76944
|
//#region ../../packages/agent-core/src/tools/builtin/file/read.ts
|
|
76536
76945
|
const MAX_LINES = 1e3;
|
|
@@ -76886,7 +77295,7 @@ var ReadTool = class {
|
|
|
76886
77295
|
if (input.maxLinesReached) parts.push(`Max ${String(MAX_LINES)} lines reached.`);
|
|
76887
77296
|
else if (input.maxBytesReached) parts.push(`Max ${String(MAX_BYTES)} bytes reached.`);
|
|
76888
77297
|
else if (lineCount < input.requestedLines) parts.push("End of file reached.");
|
|
76889
|
-
if (input.truncatedLineNumbers.length > 0) parts.push(`Lines [${input.truncatedLineNumbers.join(", ")}] were truncated.`);
|
|
77298
|
+
if (input.truncatedLineNumbers.length > 0) parts.push(`Lines [${input.truncatedLineNumbers.join(", ")}] exceed the ${String(MAX_LINE_LENGTH)}-char limit and were truncated. Re-read with line_offset and n_lines=1 to view individual long lines in full.`);
|
|
76890
77299
|
if (input.lineEndingStyle === "mixed") parts.push("Mixed or lone carriage-return line endings are shown as \\r. Use exact \\r\\n or \\r escapes in Edit.old_string for those lines.");
|
|
76891
77300
|
parts.push(`Anchor: ${input.anchor}`);
|
|
76892
77301
|
return parts.join(" ");
|
|
@@ -77285,7 +77694,7 @@ var ReadMediaFileTool = class {
|
|
|
77285
77694
|
};
|
|
77286
77695
|
//#endregion
|
|
77287
77696
|
//#region ../../packages/agent-core/src/tools/builtin/file/write.md
|
|
77288
|
-
var write_default = "
|
|
77697
|
+
var write_default = "Write does not preserve or infer the previous line-ending style: \\n stays LF, \\r\\n stays CRLF. Use Edit for targeted changes to existing files. When the content is very large, you can split it across multiple calls: write the first chunk with overwrite, then add the remaining chunks with append. Write refuses content containing `<<<<<<<`/`=======`/`>>>>>>>` merge conflict markers - resolve conflicts first.\n";
|
|
77289
77698
|
//#endregion
|
|
77290
77699
|
//#region ../../packages/agent-core/src/tools/builtin/file/write.ts
|
|
77291
77700
|
/** Mask isolating the file-type bits of a stat mode. */
|
|
@@ -77333,10 +77742,11 @@ var WriteTool = class {
|
|
|
77333
77742
|
pathClass: this.jian.pathClass(),
|
|
77334
77743
|
homeDir: this.jian.gethome()
|
|
77335
77744
|
}),
|
|
77336
|
-
execute: () => this.execution(args, path)
|
|
77745
|
+
execute: ({ signal }) => this.execution(args, path, signal)
|
|
77337
77746
|
};
|
|
77338
77747
|
}
|
|
77339
|
-
async execution(args, safePath) {
|
|
77748
|
+
async execution(args, safePath, signal) {
|
|
77749
|
+
signal?.throwIfAborted();
|
|
77340
77750
|
const parentError = await this.checkParentDirectory(safePath);
|
|
77341
77751
|
if (parentError !== void 0) return {
|
|
77342
77752
|
isError: true,
|
|
@@ -77351,6 +77761,7 @@ var WriteTool = class {
|
|
|
77351
77761
|
const mode = args.mode ?? "overwrite";
|
|
77352
77762
|
if (mode === "append") await this.jian.writeText(safePath, args.content, { mode: "a" });
|
|
77353
77763
|
else await this.jian.writeText(safePath, args.content);
|
|
77764
|
+
scanCache.clear();
|
|
77354
77765
|
const bytesWritten = Buffer.byteLength(args.content, "utf8");
|
|
77355
77766
|
const { notice, hasErrors } = await this.appendDiagnostics(safePath);
|
|
77356
77767
|
const output = `${mode === "append" ? "Appended" : "Wrote"} ${String(bytesWritten)} bytes to ${args.path}${notice}`;
|
|
@@ -77400,7 +77811,7 @@ var WriteTool = class {
|
|
|
77400
77811
|
};
|
|
77401
77812
|
//#endregion
|
|
77402
77813
|
//#region ../../packages/agent-core/src/tools/builtin/planning/enter-plan-mode.md
|
|
77403
|
-
var enter_plan_mode_default = "Use this tool proactively when you're about to start a non-trivial implementation task.\nGetting user sign-off on your approach via ExitPlanMode before writing code prevents wasted effort.\n\n## Planning Modes\n\
|
|
77814
|
+
var enter_plan_mode_default = "Use this tool proactively when you're about to start a non-trivial implementation task.\nGetting user sign-off on your approach via ExitPlanMode before writing code prevents wasted effort.\n\n## Planning Modes\n\n- **Normal plan**: You investigate the codebase, design a single approach, write it to the plan file, and present it for approval.\n- **Fusion plan**: Invoke the FusionPlan tool to spawn parallel planning subagents, each exploring a different angle, then synthesize their outputs into one plan. Fusion plan may take longer but tends to surface risks and alternatives you might miss.\n\n### When to choose which mode\n\nPrefer **normal plan** when:\n1. The user gave specific, detailed instructions.\n2. The change is small or localized (1-3 files, single concern).\n3. You are confident about the codebase structure and the right approach.\n4. Speed matters more than exploring alternatives.\n\nPrefer **fusion plan** when:\n1. The task is open-ended or ambiguous (e.g. \"improve performance\", \"refactor auth\").\n2. Multiple valid architectures or approaches exist.\n3. The change spans more than 3-5 files or touches core abstractions.\n4. You are unfamiliar with the relevant code paths and want parallel exploration.\n5. The user explicitly asked for a thorough plan or mentioned comparing options.\n\nIf unsure, choose normal plan for small fixes and fusion plan for larger design tasks.\n\n## When to Use\n\nUse this tool when ANY of these conditions apply:\n\n1. New Feature Implementation - e.g. \"Add a caching layer to the API\"\n2. Multiple Valid Approaches - e.g. \"Optimize database queries\" (indexing vs rewrite vs caching)\n3. Code Modifications - e.g. \"Refactor auth module to support OAuth\"\n4. Architectural Decisions - e.g. \"Add WebSocket support\"\n5. Multi-File Changes - involves more than 2-3 files\n6. Unclear Requirements - need exploration to understand scope\n7. User Preferences Matter - if user input would materially change the implementation approach, use EnterPlanMode to structure the decision\n\nPermission mode notes:\n- EnterPlanMode enters plan mode automatically without an approval prompt in all permission modes.\n- In yolo and manual modes, ExitPlanMode still presents the plan to the user for approval.\n- In auto permission mode, do not use AskUserQuestion; make the best decision from available context.\n- In auto permission mode, ExitPlanMode exits plan mode without asking the user.\n- Use EnterPlanMode only when planning itself adds value.\n\nWhen NOT to use:\n- Single-line or few-line fixes (typos, obvious bugs, small tweaks)\n- User gave very specific, detailed instructions\n- Pure research/exploration tasks\n\n## What Happens in Plan Mode\nIn plan mode, you will:\n1. Identify 2-3 key questions about the codebase that are critical to your plan. If you are not confident about the codebase structure or relevant code paths, use `Agent(subagent_type=\"explore\")` to investigate these questions first - this is strongly recommended for non-trivial tasks.\n2. Explore the codebase using Glob, Grep, Read, and other read-only tools for any remaining quick lookups. Use Bash only when needed; Bash follows the normal permission mode and rules.\n3. Design an implementation approach based on your findings (or, for fusion plan, call FusionPlan to generate the synthesized plan).\n4. Review the generated plan, fill in any gaps, and ensure it matches the user's intent.\n5. Present your plan to the user via ExitPlanMode for approval\n\nFor fusion plan, the FusionPlan tool performs the parallel exploration and synthesis for you; you should still review the result before exiting plan mode.\n";
|
|
77404
77815
|
//#endregion
|
|
77405
77816
|
//#region ../../packages/agent-core/src/tools/builtin/planning/enter-plan-mode.ts
|
|
77406
77817
|
const EnterPlanModeInputSchema = z.object({ mode: z.enum(["normal", "fusion"]).default("normal").describe("Planning strategy. 'normal' (default): you design the plan. 'fusion': the host spawns parallel planning subagents and synthesizes a single plan for you to review. Use 'fusion' for ambiguous, large, or multi-approach tasks.") }).strict();
|
|
@@ -77485,7 +77896,7 @@ function enteredPlanModeMessage(mode, planPath) {
|
|
|
77485
77896
|
}
|
|
77486
77897
|
//#endregion
|
|
77487
77898
|
//#region ../../packages/agent-core/src/tools/builtin/planning/exit-plan-mode.md
|
|
77488
|
-
var exit_plan_mode_default = "Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.\n\n## How This Tool Works\n- You should have already written your plan to the plan file specified in the plan mode reminder.\n- This tool does NOT take the plan content as a parameter - it reads the plan from the file you wrote.\n- The user will see the contents of your plan file when they review it. In auto permission mode, the tool reads the file and exits plan mode without asking the user.\n\n## When to Use\nOnly use this tool for tasks that require planning implementation steps. For research tasks (searching files, reading code, understanding the codebase), do NOT use this tool.\n\n## Multiple Approaches\nIf your plan contains multiple alternative approaches:\n-
|
|
77899
|
+
var exit_plan_mode_default = "Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.\n\n## How This Tool Works\n- You should have already written your plan to the plan file specified in the plan mode reminder.\n- This tool does NOT take the plan content as a parameter - it reads the plan from the file you wrote.\n- The user will see the contents of your plan file when they review it. In auto permission mode, the tool reads the file and exits plan mode without asking the user.\n\n## When to Use\nOnly use this tool for tasks that require planning implementation steps. For research tasks (searching files, reading code, understanding the codebase), do NOT use this tool.\n\n## Multiple Approaches\nIf your plan contains multiple alternative approaches:\n- If you recommend one option, append \"(Recommended)\" to its label.\n- In yolo and manual modes, the user will see all options alongside Reject and Revise choices.\n- Passing a single option is allowed and is equivalent to a plain plan approval (no approach choice is surfaced to the user).\n\n## Before Using\n- In auto permission mode, do NOT use AskUserQuestion; make the best decision from available context.\n- In auto permission mode, this tool exits plan mode without asking the user.\n- In yolo and manual modes, this tool still presents the plan to the user for approval.\n- If auto permission mode is not active and you have unresolved questions, use AskUserQuestion first.\n- If auto permission mode is not active and you have multiple approaches and haven't narrowed down yet, consider using AskUserQuestion first to let the user choose, then write a plan for the chosen approach only.\n- Once your plan is finalized, use THIS tool to request approval.\n- Do NOT use AskUserQuestion to ask \"Is this plan OK?\" or \"Should I proceed?\" - that is exactly what ExitPlanMode does.\n- If rejected, revise based on feedback and call ExitPlanMode again.\n";
|
|
77489
77900
|
//#endregion
|
|
77490
77901
|
//#region ../../packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts
|
|
77491
77902
|
const RESERVED_OPTION_LABELS = new Set([
|
|
@@ -77814,6 +78225,30 @@ function truncateUtf8$1(input, maxBytes) {
|
|
|
77814
78225
|
var bash_default = "Execute a `{{ SHELL_NAME }}` command. Use this for shell semantics — pipes, env, processes, git, package managers, build/test runners, anything genuinely interactive or multi-step.\n\n**Translate these to a dedicated tool instead:**\n- `cat` / `head` / `tail` (known path) → `Read`\n- `sed` / `awk` (in-place edit) → `Edit`\n- `echo > file` / `cat <<EOF` → `Write`\n- `find` / recursive `ls` to locate files by name pattern → `Glob` (plain `ls <known-directory>` is fine for listing a directory)\n- `grep` / `rg` (search file contents) → `Grep`\n- `echo` / `printf` (talk to the user) → just output text directly\n\nThe dedicated tools render in the per-tool permission UI and keep raw stdout out of the conversation; that is why they are worth reaching for whenever one fits.\n\n**Output:**\nThe stdout and stderr will be combined and returned as a string. The output may be truncated if it is too long. If the command failed, the output will end with a `Command failed with exit code: N` line stating the non-zero exit code.\n\nIf `run_in_background=true`, the command will be started as a background task and this tool will return a task ID instead of waiting for command completion. When doing that, you must provide a short `description`. Background commands default to a {{ DEFAULT_BACKGROUND_TIMEOUT_S }}s timeout and `timeout` is capped at {{ MAX_BACKGROUND_TIMEOUT_S }}s; set `disable_timeout=true` only when the task should run without a timeout. You will be automatically notified when the task completes. Use `TaskOutput` for a non-blocking status/output snapshot, and only set `block=true` when you explicitly want to wait for completion. Use `TaskStop` only if the task must be cancelled. If a human user wants to inspect background tasks themselves, point them to the `/tasks` command, which opens an interactive panel; it has no subcommands.\n\n**Guidelines for safety and security:**\n- Each shell tool call will be executed in a fresh shell environment. The shell variables, current working directory changes, and the shell history is not preserved between calls.\n- The tool call will return after the command is finished. You shall not use this tool to execute an interactive command or a command that may run forever. For possibly long-running foreground commands, set the `timeout` argument in seconds. Foreground commands default to {{ DEFAULT_TIMEOUT_S }}s and allow up to {{ MAX_TIMEOUT_S }}s.\n- Avoid using `..` to access files or directories outside of the working directory.\n- Avoid modifying files outside of the working directory unless explicitly instructed to do so.\n- Never run commands that require superuser privileges unless explicitly instructed to do so.\n\n**Guidelines for efficiency:**\n- For multiple related commands, use `&&` to chain them in a single call, e.g. `cd /path && ls -la`\n- Use `;` to run commands sequentially regardless of success/failure\n- Use `||` for conditional execution (run second command only if first fails)\n- Use pipe operations (`|`) and redirections (`>`, `>>`) to chain input and output between commands\n- Always quote file paths containing spaces with double quotes (e.g., cd \"/path with spaces/\")\n- Compose multi-step logic in a single call with `if` / `case` / `for` / `while` control flows.\n- Prefer `run_in_background=true` for long-running builds, tests, watchers, or servers when you need the conversation to continue before the command finishes.\n\n**Commands available:**\nThe following common command categories are usually available. Availability still depends on the host, so when in doubt run `which <command>` first to confirm a command exists before relying on it.\n- Navigation and inspection: `ls`, `pwd`, `cd`, `stat`, `file`, `du`, `df`, `tree`\n- File and directory management: `cp`, `mv`, `rm`, `mkdir`, `touch`, `ln`, `chmod`, `chown`\n- Text and data processing: `wc`, `sort`, `uniq`, `cut`, `tr`, `diff`, `xargs`\n- Archives and compression: `tar`, `gzip`, `gunzip`, `zip`, `unzip`\n- Networking and transfer: `curl`, `wget`, `ping`, `ssh`, `scp`\n- Version control: `git`\n- Process and system: `ps`, `kill`, `top`, `env`, `date`, `uname`, `whoami`\n- Language and package toolchains: `node`, `npm`, `pnpm`, `yarn`, `python`, `pip` (use whichever the project actually relies on)\n";
|
|
77815
78226
|
//#endregion
|
|
77816
78227
|
//#region ../../packages/agent-core/src/tools/builtin/shell/bash.ts
|
|
78228
|
+
/**
|
|
78229
|
+
* BashTool — execute shell commands.
|
|
78230
|
+
*
|
|
78231
|
+
* Invokes bash (POSIX) according to an injected `Environment`. On Windows
|
|
78232
|
+
* the shell is Git Bash; the path is resolved by `detectEnvironment`.
|
|
78233
|
+
*
|
|
78234
|
+
* Dependencies injected via constructor:
|
|
78235
|
+
* - `Jian` — shell execution abstraction (exec / execWithEnv)
|
|
78236
|
+
* - `cwd` — default working directory for commands
|
|
78237
|
+
* - `Environment` — cross-platform probe (shellName / shellPath)
|
|
78238
|
+
* - `BackgroundProcessManager?` — optional: required iff run_in_background=true
|
|
78239
|
+
*
|
|
78240
|
+
* Execution goes through Jian, never directly via node:child_process.
|
|
78241
|
+
*
|
|
78242
|
+
* Hardening:
|
|
78243
|
+
* - `args.timeout` (seconds) and the ambient `signal` both drive
|
|
78244
|
+
* `Promise.race`; fire-a-kill on either edge.
|
|
78245
|
+
* - stdin is closed immediately so interactive commands (`cat`, `read`,
|
|
78246
|
+
* `python -c 'input()'`) receive EOF instead of hanging.
|
|
78247
|
+
* - Two-phase kill: SIGTERM → 5s grace → SIGKILL (Jian honours this
|
|
78248
|
+
* contract cross-platform).
|
|
78249
|
+
* - stdout/stderr stream into ToolResultBuilder; excess is replaced with a
|
|
78250
|
+
* truncation marker so a runaway command cannot OOM the host.
|
|
78251
|
+
*/
|
|
77817
78252
|
const MS_PER_SECOND = 1e3;
|
|
77818
78253
|
const DEFAULT_TIMEOUT_S = 60;
|
|
77819
78254
|
const MAX_TIMEOUT_S = 300;
|
|
@@ -78064,7 +78499,11 @@ var BashTool = class {
|
|
|
78064
78499
|
killProc();
|
|
78065
78500
|
}, timeoutMs);
|
|
78066
78501
|
try {
|
|
78067
|
-
const builder = new ToolResultBuilder()
|
|
78502
|
+
const builder = new ToolResultBuilder({ artifactSink: async (fullOutput) => {
|
|
78503
|
+
const artifactPath = join(tmpdir(), `scream-bash-output-${randomUUID()}.log`);
|
|
78504
|
+
await writeFile(artifactPath, fullOutput, "utf8");
|
|
78505
|
+
return artifactPath;
|
|
78506
|
+
} });
|
|
78068
78507
|
const [, exitCode] = await Promise.all([Promise.all([readStreamIntoBuilder(proc.stdout, builder), readStreamIntoBuilder(proc.stderr, builder)]), proc.wait()]);
|
|
78069
78508
|
if (timedOut) {
|
|
78070
78509
|
const timeoutLabel = timeoutMs % 1e3 === 0 ? `${String(timeoutMs / 1e3)}s` : `${String(timeoutMs)}ms`;
|
|
@@ -79655,7 +80094,8 @@ const DEFAULT_CONFIG = {
|
|
|
79655
80094
|
minContentTokens: 100,
|
|
79656
80095
|
minContextUsageRatio: .5,
|
|
79657
80096
|
truncatedMarker: "[Old tool result content cleared]",
|
|
79658
|
-
uselessMarker: "[Uneventful result elided]"
|
|
80097
|
+
uselessMarker: "[Uneventful result elided]",
|
|
80098
|
+
noMatchesMarker: "[no matches]"
|
|
79659
80099
|
};
|
|
79660
80100
|
/**
|
|
79661
80101
|
* Compute the cutoff index: everything at index < cutoff is eligible for
|
|
@@ -79676,30 +80116,107 @@ function computeCutoff(messages, config) {
|
|
|
79676
80116
|
}
|
|
79677
80117
|
return cutoff;
|
|
79678
80118
|
}
|
|
80119
|
+
/** Tool names whose results are file reads eligible for supersede pruning. */
|
|
80120
|
+
const READ_TOOL_NAMES = new Set(["Read", "ReadGroup"]);
|
|
80121
|
+
/** Tool names whose empty results can be elided as a no-match marker. */
|
|
80122
|
+
const SEARCH_TOOL_NAMES = new Set(["Grep", "Glob"]);
|
|
80123
|
+
/** Exact tool-result texts that indicate a zero-match search result. */
|
|
80124
|
+
const ZERO_MATCH_TEXTS = new Set(["No matches found", "No non-sensitive matches found"]);
|
|
80125
|
+
/**
|
|
80126
|
+
* Parse a tool call's arguments JSON. Returns undefined for null or malformed
|
|
80127
|
+
* JSON - persisted history can carry truncated arguments that must not crash
|
|
80128
|
+
* compaction. `ToolCall.arguments` is a JSON string (or null), never a parsed
|
|
80129
|
+
* object, so every consumer must go through this helper.
|
|
80130
|
+
*/
|
|
80131
|
+
function parseToolCallArguments$1(argumentsJson) {
|
|
80132
|
+
if (argumentsJson === null || argumentsJson === void 0) return void 0;
|
|
80133
|
+
try {
|
|
80134
|
+
const parsed = JSON.parse(argumentsJson);
|
|
80135
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
80136
|
+
} catch {
|
|
80137
|
+
return;
|
|
80138
|
+
}
|
|
80139
|
+
}
|
|
79679
80140
|
/**
|
|
79680
|
-
*
|
|
79681
|
-
*
|
|
79682
|
-
*
|
|
80141
|
+
* Extract the file paths targeted by a read tool call. `Read` carries a
|
|
80142
|
+
* single `path`; `ReadGroup` carries a `paths` array. Returns an empty array
|
|
80143
|
+
* for non-read tools or calls whose arguments don't yield usable paths.
|
|
80144
|
+
*/
|
|
80145
|
+
function extractReadFilePaths(name, args) {
|
|
80146
|
+
if (args === void 0) return [];
|
|
80147
|
+
if (name === "Read") {
|
|
80148
|
+
const path = typeof args["path"] === "string" ? args["path"] : void 0;
|
|
80149
|
+
return path !== void 0 && path.length > 0 ? [path] : [];
|
|
80150
|
+
}
|
|
80151
|
+
if (name === "ReadGroup") {
|
|
80152
|
+
const paths = args["paths"];
|
|
80153
|
+
if (!Array.isArray(paths)) return [];
|
|
80154
|
+
return paths.filter((p) => typeof p === "string" && p.length > 0);
|
|
80155
|
+
}
|
|
80156
|
+
return [];
|
|
80157
|
+
}
|
|
80158
|
+
/** Concatenate the `text` parts of a message's content into a single string. */
|
|
80159
|
+
function extractTextContent(content) {
|
|
80160
|
+
let text = "";
|
|
80161
|
+
for (const part of content) if (typeof part === "object" && part !== null && part.type === "text") text += part.text;
|
|
80162
|
+
return text;
|
|
80163
|
+
}
|
|
80164
|
+
/** Build a toolCallId -> tool-name map by scanning assistant messages. */
|
|
80165
|
+
function buildToolCallNameMap(messages) {
|
|
80166
|
+
const names = /* @__PURE__ */ new Map();
|
|
80167
|
+
for (const msg of messages) {
|
|
80168
|
+
if (msg.role !== "assistant") continue;
|
|
80169
|
+
for (const tc of msg.toolCalls) names.set(tc.id, tc.name);
|
|
80170
|
+
}
|
|
80171
|
+
return names;
|
|
80172
|
+
}
|
|
80173
|
+
/**
|
|
80174
|
+
* Whether a tool result is a zero-match Grep/Glob result eligible for elision.
|
|
80175
|
+
* Only exact-match the canonical empty-result texts so results carrying extra
|
|
80176
|
+
* information (sensitive-file filter notices, pagination notices, errors) are
|
|
80177
|
+
* preserved verbatim.
|
|
80178
|
+
*/
|
|
80179
|
+
function isZeroMatchSearchResult(toolName, content) {
|
|
80180
|
+
if (toolName === void 0 || !SEARCH_TOOL_NAMES.has(toolName)) return false;
|
|
80181
|
+
const text = extractTextContent(content).trim();
|
|
80182
|
+
return ZERO_MATCH_TEXTS.has(text);
|
|
80183
|
+
}
|
|
80184
|
+
/**
|
|
80185
|
+
* Walk the message list and find Read/ReadGroup tool calls whose file paths
|
|
80186
|
+
* were superseded by a later read of the same path. Returns a map from the
|
|
80187
|
+
* superseded tool call's ID to the list of file paths covered by the newer
|
|
80188
|
+
* read (a ReadGroup can cover several).
|
|
79683
80189
|
*
|
|
79684
|
-
* Only considers tool results before the cutoff line
|
|
79685
|
-
* protected and their results are kept verbatim.
|
|
80190
|
+
* Only considers tool results before the cutoff line - newer reads are
|
|
80191
|
+
* protected and their results are kept verbatim. The comparison uses the raw
|
|
80192
|
+
* path strings from the tool arguments; path canonicalization would need
|
|
80193
|
+
* workspace context the compaction layer doesn't have, so the same file read
|
|
80194
|
+
* via two different spellings is treated as two different files (safe: it
|
|
80195
|
+
* just misses a supersede opportunity rather than dropping a distinct result).
|
|
79686
80196
|
*/
|
|
79687
80197
|
function findSupersededPaths(messages, cutoff) {
|
|
79688
80198
|
const superseded = /* @__PURE__ */ new Map();
|
|
79689
|
-
const
|
|
80199
|
+
const latestReadByPath = /* @__PURE__ */ new Map();
|
|
79690
80200
|
for (let i = 0; i < messages.length; i++) {
|
|
79691
80201
|
const msg = messages[i];
|
|
79692
80202
|
if (msg === void 0) continue;
|
|
79693
|
-
if (msg.role
|
|
79694
|
-
|
|
79695
|
-
|
|
79696
|
-
|
|
79697
|
-
|
|
79698
|
-
|
|
79699
|
-
|
|
79700
|
-
|
|
79701
|
-
|
|
79702
|
-
|
|
80203
|
+
if (msg.role !== "assistant" || msg.toolCalls.length === 0) continue;
|
|
80204
|
+
for (const tc of msg.toolCalls) {
|
|
80205
|
+
if (!READ_TOOL_NAMES.has(tc.name)) continue;
|
|
80206
|
+
const args = parseToolCallArguments$1(tc.arguments);
|
|
80207
|
+
const paths = extractReadFilePaths(tc.name, args);
|
|
80208
|
+
if (paths.length === 0) continue;
|
|
80209
|
+
for (const filePath of paths) {
|
|
80210
|
+
const prev = latestReadByPath.get(filePath);
|
|
80211
|
+
if (prev !== void 0 && prev.index < cutoff) {
|
|
80212
|
+
const existing = superseded.get(prev.toolCallId);
|
|
80213
|
+
if (existing === void 0) superseded.set(prev.toolCallId, [filePath]);
|
|
80214
|
+
else if (!existing.includes(filePath)) superseded.set(prev.toolCallId, [...existing, filePath]);
|
|
80215
|
+
}
|
|
80216
|
+
latestReadByPath.set(filePath, {
|
|
80217
|
+
toolCallId: tc.id,
|
|
80218
|
+
index: i
|
|
80219
|
+
});
|
|
79703
80220
|
}
|
|
79704
80221
|
}
|
|
79705
80222
|
}
|
|
@@ -79748,25 +80265,33 @@ var MicroCompaction = class {
|
|
|
79748
80265
|
const nextCutoff = computeCutoff(history, config);
|
|
79749
80266
|
if (nextCutoff <= this.cutoff) return;
|
|
79750
80267
|
const { beforeTokens, afterTokens } = this.measureEffect(history, nextCutoff);
|
|
79751
|
-
|
|
80268
|
+
const reclaimTokens = beforeTokens - afterTokens;
|
|
80269
|
+
if (reclaimTokens < config.pruneMinReclaimTokens) return;
|
|
80270
|
+
if (estimateTokensForMessages(history.slice(nextCutoff)) * 1.15 > reclaimTokens) return;
|
|
79752
80271
|
this.apply(nextCutoff);
|
|
79753
80272
|
}
|
|
79754
80273
|
/**
|
|
79755
80274
|
* Apply micro-compaction to a message list: replace old tool results
|
|
79756
|
-
* before the cutoff line with truncated markers. Read results
|
|
79757
|
-
* that were re-read later get a supersede marker
|
|
79758
|
-
* the old content is stale.
|
|
79759
|
-
* elided
|
|
79760
|
-
*
|
|
80275
|
+
* before the cutoff line with truncated markers. Read/ReadGroup results
|
|
80276
|
+
* for files that were re-read later get a supersede marker (listing the
|
|
80277
|
+
* covered paths) so the model knows the old content is stale. Zero-match
|
|
80278
|
+
* Grep/Glob results are elided to a short `[no matches]` notice regardless
|
|
80279
|
+
* of size. Tool results explicitly marked useless are elided with a short
|
|
80280
|
+
* notice regardless of size, since they carry no actionable information.
|
|
79761
80281
|
*/
|
|
79762
80282
|
compact(messages) {
|
|
79763
80283
|
const config = this.config;
|
|
79764
80284
|
const superseded = findSupersededPaths(messages, this.cutoff);
|
|
80285
|
+
const toolNames = buildToolCallNameMap(messages);
|
|
79765
80286
|
const result = [];
|
|
79766
80287
|
let i = 0;
|
|
79767
80288
|
for (const msg of messages) {
|
|
79768
|
-
const
|
|
79769
|
-
const
|
|
80289
|
+
const isOld = i < this.cutoff;
|
|
80290
|
+
const toolCallId = msg.toolCallId;
|
|
80291
|
+
const isTool = msg.role === "tool" && toolCallId !== void 0;
|
|
80292
|
+
const isUseless = isOld && isTool && msg.useless === true;
|
|
80293
|
+
const isZeroMatch = isOld && isTool && toolCallId !== void 0 && isZeroMatchSearchResult(toolNames.get(toolCallId), msg.content);
|
|
80294
|
+
const isOversizedTruncatable = isOld && isTool && estimateTokensForMessages([msg]) >= config.minContentTokens;
|
|
79770
80295
|
if (isUseless) result.push({
|
|
79771
80296
|
...msg,
|
|
79772
80297
|
content: [{
|
|
@@ -79774,8 +80299,16 @@ var MicroCompaction = class {
|
|
|
79774
80299
|
text: config.uselessMarker
|
|
79775
80300
|
}]
|
|
79776
80301
|
});
|
|
80302
|
+
else if (isZeroMatch) result.push({
|
|
80303
|
+
...msg,
|
|
80304
|
+
content: [{
|
|
80305
|
+
type: "text",
|
|
80306
|
+
text: config.noMatchesMarker
|
|
80307
|
+
}]
|
|
80308
|
+
});
|
|
79777
80309
|
else if (isOversizedTruncatable) {
|
|
79778
|
-
const
|
|
80310
|
+
const paths = toolCallId !== void 0 ? superseded.get(toolCallId) : void 0;
|
|
80311
|
+
const marker = paths !== void 0 && paths.length > 0 ? `[Superseded by a newer read of ${paths.join(", ")}]` : config.truncatedMarker;
|
|
79779
80312
|
result.push({
|
|
79780
80313
|
...msg,
|
|
79781
80314
|
content: [{
|
|
@@ -79800,20 +80333,28 @@ var MicroCompaction = class {
|
|
|
79800
80333
|
measureEffect(messages, cutoff) {
|
|
79801
80334
|
let markerTokenCount;
|
|
79802
80335
|
let uselessMarkerTokenCount;
|
|
80336
|
+
let noMatchesMarkerTokenCount;
|
|
79803
80337
|
let truncatedToolResultCount = 0;
|
|
79804
80338
|
let beforeTokens = 0;
|
|
79805
80339
|
let afterTokens = 0;
|
|
80340
|
+
const toolNames = buildToolCallNameMap(messages);
|
|
79806
80341
|
for (let i = 0; i < messages.length && i < cutoff; i++) {
|
|
79807
80342
|
const message = messages[i];
|
|
79808
80343
|
if (message?.role !== "tool" || message.toolCallId === void 0) continue;
|
|
79809
80344
|
const contentTokens = estimateTokensForMessages([message]);
|
|
79810
80345
|
const isUseless = message.useless === true;
|
|
79811
|
-
|
|
80346
|
+
const isZeroMatch = isZeroMatchSearchResult(toolNames.get(message.toolCallId), message.content);
|
|
80347
|
+
if (!isUseless && !isZeroMatch && contentTokens < this.config.minContentTokens) continue;
|
|
79812
80348
|
if (isUseless) {
|
|
79813
80349
|
uselessMarkerTokenCount ??= estimateTokens$1(this.config.uselessMarker);
|
|
79814
80350
|
truncatedToolResultCount += 1;
|
|
79815
80351
|
beforeTokens += contentTokens;
|
|
79816
80352
|
afterTokens += uselessMarkerTokenCount;
|
|
80353
|
+
} else if (isZeroMatch) {
|
|
80354
|
+
noMatchesMarkerTokenCount ??= estimateTokens$1(this.config.noMatchesMarker);
|
|
80355
|
+
truncatedToolResultCount += 1;
|
|
80356
|
+
beforeTokens += contentTokens;
|
|
80357
|
+
afterTokens += noMatchesMarkerTokenCount;
|
|
79817
80358
|
} else {
|
|
79818
80359
|
markerTokenCount ??= estimateTokens$1(this.config.truncatedMarker);
|
|
79819
80360
|
truncatedToolResultCount += 1;
|
|
@@ -80822,6 +81363,71 @@ var ConfigState = class {
|
|
|
80822
81363
|
}
|
|
80823
81364
|
};
|
|
80824
81365
|
//#endregion
|
|
81366
|
+
//#region ../../packages/agent-core/src/agent/context/prefix-fingerprint.ts
|
|
81367
|
+
/**
|
|
81368
|
+
* Deterministic non-crypto string hash (djb2). Fast and sufficient for change
|
|
81369
|
+
* detection; not used for any security purpose. Returns a compact base36
|
|
81370
|
+
* string so an array of fingerprints stays small.
|
|
81371
|
+
*/
|
|
81372
|
+
function hashString(input) {
|
|
81373
|
+
let hash = 5381;
|
|
81374
|
+
for (let i = 0; i < input.length; i++) hash = (hash << 5) + hash + (input.codePointAt(i) ?? 0) | 0;
|
|
81375
|
+
return (hash >>> 0).toString(36);
|
|
81376
|
+
}
|
|
81377
|
+
/**
|
|
81378
|
+
* Stable JSON serialization that sorts object keys so key insertion order
|
|
81379
|
+
* can't affect the fingerprint. Only used for media parts whose payload is
|
|
81380
|
+
* already a stable base64/url string; text/think parts have a dedicated
|
|
81381
|
+
* fast path.
|
|
81382
|
+
*/
|
|
81383
|
+
function stableJson(value) {
|
|
81384
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
81385
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
81386
|
+
const obj = value;
|
|
81387
|
+
return `{${Object.keys(obj).toSorted().map((k) => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(",")}}`;
|
|
81388
|
+
}
|
|
81389
|
+
function serializeContentPart(part) {
|
|
81390
|
+
switch (part.type) {
|
|
81391
|
+
case "text": return `text:${part.text}`;
|
|
81392
|
+
case "think": return `think:${part.think}${part.encrypted !== void 0 ? `\u0003${part.encrypted}` : ""}`;
|
|
81393
|
+
default: return `${part.type}:${stableJson(part)}`;
|
|
81394
|
+
}
|
|
81395
|
+
}
|
|
81396
|
+
function serializeToolCall(tc) {
|
|
81397
|
+
return `function|${tc.id}|${tc.name}|${tc.arguments ?? ""}`;
|
|
81398
|
+
}
|
|
81399
|
+
/**
|
|
81400
|
+
* Serialize a message to a deterministic string covering all
|
|
81401
|
+
* provider-visible bytes. Two messages with the same serialization produce
|
|
81402
|
+
* identical provider bytes (modulo serialization the provider adapter
|
|
81403
|
+
* normalizes). Separator bytes (\x00-\x03) are used so concatenated fields
|
|
81404
|
+
* can't alias.
|
|
81405
|
+
*/
|
|
81406
|
+
function serializeMessage$1(message) {
|
|
81407
|
+
const content = message.content.map(serializeContentPart).join("");
|
|
81408
|
+
const toolCalls = message.toolCalls.map(serializeToolCall).join("");
|
|
81409
|
+
return `${message.role}\u0000${message.name ?? ""}\u0000${message.toolCallId ?? ""}\u0000${content}\u0000${toolCalls}`;
|
|
81410
|
+
}
|
|
81411
|
+
/** Per-message fingerprint. Equal fingerprints => equal provider bytes. */
|
|
81412
|
+
function messageFingerprint(message) {
|
|
81413
|
+
return hashString(serializeMessage$1(message));
|
|
81414
|
+
}
|
|
81415
|
+
/**
|
|
81416
|
+
* Longest common prefix length by provider-visible bytes. This is the number
|
|
81417
|
+
* of leading messages a provider prompt cache could reuse from the previous
|
|
81418
|
+
* call. When this is less than the previous call's message count, an early
|
|
81419
|
+
* message mutated and the cache broke from that index.
|
|
81420
|
+
*
|
|
81421
|
+
* `prev` is the array of per-message fingerprints captured last call;
|
|
81422
|
+
* `current` is the live messages this call.
|
|
81423
|
+
*/
|
|
81424
|
+
function stablePrefixLength(prev, current) {
|
|
81425
|
+
const n = Math.min(prev.length, current.length);
|
|
81426
|
+
let i = 0;
|
|
81427
|
+
for (; i < n; i++) if (prev[i] !== messageFingerprint(current[i])) break;
|
|
81428
|
+
return i;
|
|
81429
|
+
}
|
|
81430
|
+
//#endregion
|
|
80825
81431
|
//#region ../../packages/agent-core/src/agent/context/types.ts
|
|
80826
81432
|
const USER_PROMPT_ORIGIN = { kind: "user" };
|
|
80827
81433
|
//#endregion
|
|
@@ -80844,6 +81450,18 @@ var ContextMemory = class {
|
|
|
80844
81450
|
openSteps = /* @__PURE__ */ new Map();
|
|
80845
81451
|
pendingToolResultIds = /* @__PURE__ */ new Set();
|
|
80846
81452
|
deferredMessages = [];
|
|
81453
|
+
/**
|
|
81454
|
+
* Per-message fingerprints captured from the last message list handed to
|
|
81455
|
+
* the LLM via {@link messagesForLLM}. Used to measure prefix stability
|
|
81456
|
+
* across calls: a provider prompt cache only hits when the leading
|
|
81457
|
+
* messages are byte-identical to the previous request, so the length of
|
|
81458
|
+
* the matching prefix here approximates the cacheable prefix length.
|
|
81459
|
+
*
|
|
81460
|
+
* Reset on {@link clear}; a compaction naturally produces a 0-length
|
|
81461
|
+
* stable prefix (the summary replaces the head), which is the correct
|
|
81462
|
+
* cache-break signal rather than a reset.
|
|
81463
|
+
*/
|
|
81464
|
+
lastSentFingerprints = [];
|
|
80847
81465
|
constructor(agent) {
|
|
80848
81466
|
this.agent = agent;
|
|
80849
81467
|
}
|
|
@@ -80893,6 +81511,7 @@ var ContextMemory = class {
|
|
|
80893
81511
|
this.openSteps.clear();
|
|
80894
81512
|
this.pendingToolResultIds.clear();
|
|
80895
81513
|
this.deferredMessages = [];
|
|
81514
|
+
this.lastSentFingerprints = [];
|
|
80896
81515
|
this.agent.injection.onContextClear();
|
|
80897
81516
|
this.agent.emitStatusUpdated();
|
|
80898
81517
|
}
|
|
@@ -80937,6 +81556,19 @@ var ContextMemory = class {
|
|
|
80937
81556
|
}
|
|
80938
81557
|
if (!this.agent.records.restoring && (stoppedAtBoundary || removedUserCount < count)) {}
|
|
80939
81558
|
}
|
|
81559
|
+
/**
|
|
81560
|
+
* Apply a full compaction summary.
|
|
81561
|
+
*
|
|
81562
|
+
* Prefix-stability note: this is a **replaceHead** operation, not a
|
|
81563
|
+
* replaceTail. The first `compactedCount` messages are collapsed into a
|
|
81564
|
+
* single summary message; the trailing recent messages are preserved
|
|
81565
|
+
* verbatim. This necessarily breaks the provider prompt cache for the
|
|
81566
|
+
* whole prefix (the summary is new content), which is inherent to
|
|
81567
|
+
* summarization and cannot be avoided. After compaction the new prefix
|
|
81568
|
+
* `[summary, ...tail]` is stable again until the next compaction or
|
|
81569
|
+
* micro-compaction cutoff advance, so subsequent append-only steps resume
|
|
81570
|
+
* hitting the cache.
|
|
81571
|
+
*/
|
|
80940
81572
|
applyCompaction(summary) {
|
|
80941
81573
|
this.agent.records.logRecord({
|
|
80942
81574
|
type: "context.apply_compaction",
|
|
@@ -80980,6 +81612,48 @@ var ContextMemory = class {
|
|
|
80980
81612
|
this.agent.microCompaction.detect();
|
|
80981
81613
|
return project(this.agent.microCompaction.compact(this.history));
|
|
80982
81614
|
}
|
|
81615
|
+
/**
|
|
81616
|
+
* Build the message list for an LLM call, with prefix-stability
|
|
81617
|
+
* observation.
|
|
81618
|
+
*
|
|
81619
|
+
* This is the LLM-bound counterpart of the {@link messages} getter: it
|
|
81620
|
+
* runs the same detect + compact + project pipeline, then fingerprints
|
|
81621
|
+
* the result and logs how much of the prefix survived since the last
|
|
81622
|
+
* call. A stable prefix length equal to the previous message count means
|
|
81623
|
+
* the provider prompt cache should hit; a smaller value means an early
|
|
81624
|
+
* message mutated (compaction summary, micro-compaction truncation, or a
|
|
81625
|
+
* projection repair) and the cache broke from that index.
|
|
81626
|
+
*
|
|
81627
|
+
* Behavior is otherwise identical to the getter - this is observation
|
|
81628
|
+
* only, it does not alter the messages returned.
|
|
81629
|
+
*/
|
|
81630
|
+
messagesForLLM() {
|
|
81631
|
+
this.agent.microCompaction.detect();
|
|
81632
|
+
const messages = project(this.agent.microCompaction.compact(this.history));
|
|
81633
|
+
this.observePrefixStability(messages);
|
|
81634
|
+
return messages;
|
|
81635
|
+
}
|
|
81636
|
+
/**
|
|
81637
|
+
* Compare the projected messages against the last LLM-bound batch and
|
|
81638
|
+
* log the stable-prefix length. Pure observation: no state that affects
|
|
81639
|
+
* message content is mutated, only the fingerprint baseline used by the
|
|
81640
|
+
* next call's comparison.
|
|
81641
|
+
*/
|
|
81642
|
+
observePrefixStability(messages) {
|
|
81643
|
+
const prev = this.lastSentFingerprints;
|
|
81644
|
+
const stable = stablePrefixLength(prev, messages);
|
|
81645
|
+
this.lastSentFingerprints = messages.map(messageFingerprint);
|
|
81646
|
+
if (prev.length === 0) return;
|
|
81647
|
+
const appended = messages.length - prev.length;
|
|
81648
|
+
if (stable >= prev.length) return;
|
|
81649
|
+
this.agent.log.debug("prefix-stability: provider prompt cache prefix broke", {
|
|
81650
|
+
stablePrefixLength: stable,
|
|
81651
|
+
prevMessageCount: prev.length,
|
|
81652
|
+
currentMessageCount: messages.length,
|
|
81653
|
+
appendedSinceLast: appended,
|
|
81654
|
+
breakIndex: stable
|
|
81655
|
+
});
|
|
81656
|
+
}
|
|
80983
81657
|
appendLoopEvent(event) {
|
|
80984
81658
|
this.agent.records.logRecord({
|
|
80985
81659
|
type: "context.append_loop_event",
|
|
@@ -81089,22 +81763,38 @@ function toolResultOutputForModel(result) {
|
|
|
81089
81763
|
}, ...truncateContentParts(output)];
|
|
81090
81764
|
return truncateContentParts(output);
|
|
81091
81765
|
}
|
|
81092
|
-
/** Truncate a plain-text tool output that exceeds MAX_TOOL_RESULT_TOKENS.
|
|
81766
|
+
/** Truncate a plain-text tool output that exceeds MAX_TOOL_RESULT_TOKENS.
|
|
81767
|
+
* Tail-biased: keeps 25% head + 75% tail so error messages and test
|
|
81768
|
+
* failures (usually at the end) survive truncation. */
|
|
81093
81769
|
function truncateToolOutput(text) {
|
|
81094
81770
|
if (estimateTokens$1(text) <= MAX_TOOL_RESULT_TOKENS) return text;
|
|
81095
81771
|
const budget = MAX_TOOL_RESULT_TOKENS - estimateTokens$1(TOOL_TRUNCATION_NOTICE);
|
|
81096
81772
|
if (budget <= 0) return TOOL_TRUNCATION_NOTICE.trim();
|
|
81097
|
-
|
|
81098
|
-
|
|
81773
|
+
const headBudget = Math.floor(budget * .25);
|
|
81774
|
+
const tailBudget = budget - headBudget;
|
|
81775
|
+
let head = "";
|
|
81776
|
+
let headTokens = 0;
|
|
81099
81777
|
for (const ch of text) {
|
|
81100
81778
|
const chTokens = ch.codePointAt(0) <= 127 ? 1 / 4 : 1;
|
|
81101
|
-
if (
|
|
81102
|
-
|
|
81103
|
-
|
|
81779
|
+
if (headTokens + chTokens > headBudget) break;
|
|
81780
|
+
head += ch;
|
|
81781
|
+
headTokens += chTokens;
|
|
81782
|
+
}
|
|
81783
|
+
const reversed = [...text].toReversed();
|
|
81784
|
+
const tailChars = [];
|
|
81785
|
+
let tailTokens = 0;
|
|
81786
|
+
for (const ch of reversed) {
|
|
81787
|
+
const chTokens = ch.codePointAt(0) <= 127 ? 1 / 4 : 1;
|
|
81788
|
+
if (tailTokens + chTokens > tailBudget) break;
|
|
81789
|
+
tailChars.push(ch);
|
|
81790
|
+
tailTokens += chTokens;
|
|
81104
81791
|
}
|
|
81105
|
-
|
|
81792
|
+
const tail = tailChars.toReversed().join("");
|
|
81793
|
+
const notice = `\n[content truncated - ~${Math.max(0, Math.round(estimateTokens$1(text) - headTokens - tailTokens))} tokens omitted]\n`;
|
|
81794
|
+
return head + notice + tail;
|
|
81106
81795
|
}
|
|
81107
|
-
/** Truncate oversized text parts in a ContentPart array.
|
|
81796
|
+
/** Truncate oversized text parts in a ContentPart array.
|
|
81797
|
+
* Tail-biased: keeps 25% head + 75% tail. */
|
|
81108
81798
|
function truncateContentParts(parts) {
|
|
81109
81799
|
let totalTokens = 0;
|
|
81110
81800
|
for (const p of parts) if (p.type === "text") totalTokens += estimateTokens$1(p.text);
|
|
@@ -81114,39 +81804,83 @@ function truncateContentParts(parts) {
|
|
|
81114
81804
|
type: "text",
|
|
81115
81805
|
text: TOOL_TRUNCATION_NOTICE.trim()
|
|
81116
81806
|
}];
|
|
81117
|
-
const
|
|
81118
|
-
|
|
81119
|
-
|
|
81807
|
+
const headBudget = Math.floor(budget * .25);
|
|
81808
|
+
const tailBudget = budget - headBudget;
|
|
81809
|
+
const headParts = [];
|
|
81810
|
+
let headUsed = 0;
|
|
81811
|
+
let headEnd = 0;
|
|
81812
|
+
for (let i = 0; i < parts.length; i++) {
|
|
81813
|
+
const p = parts[i];
|
|
81120
81814
|
if (p.type !== "text") {
|
|
81121
|
-
|
|
81815
|
+
headParts.push(p);
|
|
81816
|
+
headEnd = i + 1;
|
|
81122
81817
|
continue;
|
|
81123
81818
|
}
|
|
81124
81819
|
const partTokens = estimateTokens$1(p.text);
|
|
81125
|
-
if (
|
|
81126
|
-
|
|
81127
|
-
|
|
81820
|
+
if (headUsed + partTokens <= headBudget) {
|
|
81821
|
+
headParts.push(p);
|
|
81822
|
+
headUsed += partTokens;
|
|
81823
|
+
headEnd = i + 1;
|
|
81128
81824
|
} else {
|
|
81129
|
-
const remaining =
|
|
81130
|
-
|
|
81131
|
-
|
|
81132
|
-
|
|
81133
|
-
const
|
|
81134
|
-
|
|
81135
|
-
|
|
81136
|
-
|
|
81137
|
-
|
|
81138
|
-
|
|
81139
|
-
|
|
81140
|
-
|
|
81141
|
-
|
|
81825
|
+
const remaining = headBudget - headUsed;
|
|
81826
|
+
if (remaining > 0) {
|
|
81827
|
+
let kept = "";
|
|
81828
|
+
let t = 0;
|
|
81829
|
+
for (const ch of p.text) {
|
|
81830
|
+
const chTokens = ch.codePointAt(0) <= 127 ? 1 / 4 : 1;
|
|
81831
|
+
if (t + chTokens > remaining) break;
|
|
81832
|
+
kept += ch;
|
|
81833
|
+
t += chTokens;
|
|
81834
|
+
}
|
|
81835
|
+
if (kept.length > 0) headParts.push({
|
|
81836
|
+
type: "text",
|
|
81837
|
+
text: kept
|
|
81838
|
+
});
|
|
81839
|
+
}
|
|
81142
81840
|
break;
|
|
81143
81841
|
}
|
|
81144
81842
|
}
|
|
81145
|
-
|
|
81146
|
-
|
|
81147
|
-
|
|
81148
|
-
|
|
81149
|
-
|
|
81843
|
+
const tailParts = [];
|
|
81844
|
+
let tailUsed = 0;
|
|
81845
|
+
for (let i = parts.length - 1; i >= headEnd; i--) {
|
|
81846
|
+
const p = parts[i];
|
|
81847
|
+
if (p.type !== "text") {
|
|
81848
|
+
tailParts.unshift(p);
|
|
81849
|
+
continue;
|
|
81850
|
+
}
|
|
81851
|
+
const partTokens = estimateTokens$1(p.text);
|
|
81852
|
+
if (tailUsed + partTokens <= tailBudget) {
|
|
81853
|
+
tailParts.unshift(p);
|
|
81854
|
+
tailUsed += partTokens;
|
|
81855
|
+
} else {
|
|
81856
|
+
const remaining = tailBudget - tailUsed;
|
|
81857
|
+
if (remaining > 0) {
|
|
81858
|
+
const chars = [...p.text];
|
|
81859
|
+
let kept = "";
|
|
81860
|
+
let t = 0;
|
|
81861
|
+
for (let j = chars.length - 1; j >= 0; j--) {
|
|
81862
|
+
const chTokens = chars[j].codePointAt(0) <= 127 ? 1 / 4 : 1;
|
|
81863
|
+
if (t + chTokens > remaining) break;
|
|
81864
|
+
kept = chars[j] + kept;
|
|
81865
|
+
t += chTokens;
|
|
81866
|
+
}
|
|
81867
|
+
if (kept.length > 0) tailParts.unshift({
|
|
81868
|
+
type: "text",
|
|
81869
|
+
text: kept
|
|
81870
|
+
});
|
|
81871
|
+
}
|
|
81872
|
+
break;
|
|
81873
|
+
}
|
|
81874
|
+
}
|
|
81875
|
+
const omitted = Math.max(0, Math.round(totalTokens - headUsed - tailUsed));
|
|
81876
|
+
return [
|
|
81877
|
+
...headParts,
|
|
81878
|
+
{
|
|
81879
|
+
type: "text",
|
|
81880
|
+
text: `[content truncated - ~${omitted} tokens omitted]`
|
|
81881
|
+
},
|
|
81882
|
+
...tailParts
|
|
81883
|
+
];
|
|
81150
81884
|
}
|
|
81151
81885
|
function isEmptyOutputText(output) {
|
|
81152
81886
|
return output.length === 0 || output.trim() === TOOL_OUTPUT_EMPTY_TEXT;
|
|
@@ -81616,6 +82350,8 @@ function buildGoalReminder(goal) {
|
|
|
81616
82350
|
lines.push("Goal mode is iterative. Keep the self-audit brief each turn. Do not explore unrelated interpretations once the goal can be decided. If the objective is simple, already answered, impossible, unsafe, or contradictory, do not run another goal turn. Explain briefly if useful, then call UpdateGoal with `complete` or `blocked` in the same turn. Otherwise, self-audit against the objective and any completion criteria above, then do one coherent slice of work toward the objective. Use multiple turns when the task naturally has multiple phases. Call UpdateGoal with `complete` only when all required work is done, any stated validation has passed, and there is no useful next action. Do not mark complete after only producing a plan, summary, first pass, or partial result. If an external condition or required user input prevents progress, or the objective cannot be completed as stated, call UpdateGoal with `blocked`. Otherwise keep working — after your turn ends you will be prompted to continue. Call UpdateGoal as soon as the goal is genuinely done or cannot proceed; don't keep going once there is nothing left to do.");
|
|
81617
82351
|
lines.push("");
|
|
81618
82352
|
lines.push("When you call UpdateGoal with `complete`, an independent reviewer will verify that the completion criteria are met. In your final response before calling UpdateGoal, provide a structured summary: what was done, which files changed, the verification command and result, and any remaining work or blockers. Do not rely on the UpdateGoal argument alone; the reviewer and the user must see this summary in your natural-language reply.");
|
|
82353
|
+
lines.push("");
|
|
82354
|
+
lines.push("Important: before calling UpdateGoal with `complete`, always call WriteGoalNote first to summarize the key findings, constraints, or partial results from this turn. The reviewer will see these notes together with your final output, so include anything that helps them evaluate cross-turn context. Keep the note concise and actionable.");
|
|
81619
82355
|
return lines.join("\n");
|
|
81620
82356
|
}
|
|
81621
82357
|
function maxBudgetFraction(goal) {
|
|
@@ -97329,6 +98065,13 @@ function buildGraderPrompt(objective, criteria, output) {
|
|
|
97329
98065
|
"## Agent Output",
|
|
97330
98066
|
output || "(no output captured)",
|
|
97331
98067
|
"",
|
|
98068
|
+
"The Agent Output section contains the following optional parts, in order:",
|
|
98069
|
+
"1. The agent's natural-language summary of what was done this turn.",
|
|
98070
|
+
"2. \"## Cross-turn working notes\" — short notes the agent recorded using the WriteGoalNote tool across continuation turns. These notes capture key findings, constraints, decisions, and partial results. Treat them as supplementary context when evaluating whether the acceptance criteria are met; they are not the deliverable itself.",
|
|
98071
|
+
"3. \"## Changes this turn\" — a git diff stat (or a note if git is unavailable) showing which files were modified. Use it to verify that the claimed work has an actual code footprint.",
|
|
98072
|
+
"",
|
|
98073
|
+
"If the Agent Output does not contain a \"## Cross-turn working notes\" section, add a non-blocking issue: \"No cross-turn working notes were provided. Use WriteGoalNote to record key findings, constraints, and partial results across turns.\" This issue alone must not cause a FAIL.",
|
|
98074
|
+
"",
|
|
97332
98075
|
"Evaluate each dimension independently against the acceptance criteria, then decide overall PASS/FAIL.",
|
|
97333
98076
|
"When FAIL, list every specific issue with an actionable fix direction so the agent knows exactly what to address next.",
|
|
97334
98077
|
"Respond with JSON:",
|
|
@@ -98019,6 +98762,11 @@ const GOAL_CONTINUATION_ORIGIN = {
|
|
|
98019
98762
|
kind: "system_trigger",
|
|
98020
98763
|
name: "goal_continuation"
|
|
98021
98764
|
};
|
|
98765
|
+
const GOAL_BUDGET_STEER_PROMPT = "Budget nearly exhausted. Wrap up immediately: verify your work, run tests, and call UpdateGoal with status \"complete\" or \"blocked\". Do not start any new work.";
|
|
98766
|
+
const GOAL_BUDGET_STEER_ORIGIN = {
|
|
98767
|
+
kind: "system_trigger",
|
|
98768
|
+
name: "goal_budget_steer"
|
|
98769
|
+
};
|
|
98022
98770
|
var TurnFlow = class {
|
|
98023
98771
|
agent;
|
|
98024
98772
|
steerBuffer = [];
|
|
@@ -98183,6 +98931,14 @@ var TurnFlow = class {
|
|
|
98183
98931
|
}
|
|
98184
98932
|
}
|
|
98185
98933
|
await this.agent.goal.incrementTurn();
|
|
98934
|
+
const budgetSnapshot = this.agent.goal.getGoal().goal;
|
|
98935
|
+
if (budgetSnapshot !== null && budgetSnapshot.status === "active" && (budgetSnapshot.budget.overBudget || isBudgetNearExhaustion(budgetSnapshot.budget, .2))) {
|
|
98936
|
+
turnInput = [{
|
|
98937
|
+
type: "text",
|
|
98938
|
+
text: GOAL_BUDGET_STEER_PROMPT
|
|
98939
|
+
}];
|
|
98940
|
+
turnOrigin = GOAL_BUDGET_STEER_ORIGIN;
|
|
98941
|
+
}
|
|
98186
98942
|
const end = await this.runOneTurn(turnId, turnInput, turnOrigin, signal, false);
|
|
98187
98943
|
if (end.event.reason === "cancelled") {
|
|
98188
98944
|
await this.agent.goal.pauseOnInterrupt({ reason: "Paused after interruption" });
|
|
@@ -98374,7 +99130,7 @@ var TurnFlow = class {
|
|
|
98374
99130
|
turnId: String(turnId),
|
|
98375
99131
|
signal,
|
|
98376
99132
|
llm: this.agent.llm,
|
|
98377
|
-
buildMessages: () => this.agent.context.
|
|
99133
|
+
buildMessages: () => this.agent.context.messagesForLLM(),
|
|
98378
99134
|
dispatchEvent: this.buildDispatchEvent(turnId),
|
|
98379
99135
|
tools: this.agent.tools.loopTools,
|
|
98380
99136
|
log: this.agent.log,
|
|
@@ -99509,6 +100265,9 @@ var Agent = class {
|
|
|
99509
100265
|
if (status === "paused") return this.goal.pauseGoal({}, "user");
|
|
99510
100266
|
return this.goal.resumeGoal({}, "user");
|
|
99511
100267
|
},
|
|
100268
|
+
updateGoalObjective: async (payload) => {
|
|
100269
|
+
return this.goal.updateObjective({ objective: payload.objective }, "user");
|
|
100270
|
+
},
|
|
99512
100271
|
cancelGoal: async () => {
|
|
99513
100272
|
return this.goal.cancelGoal("user");
|
|
99514
100273
|
},
|
|
@@ -119565,6 +120324,9 @@ var SessionAPIImpl = class {
|
|
|
119565
120324
|
updateGoalStatus({ agentId, ...payload }) {
|
|
119566
120325
|
return this.getAgent(agentId).updateGoalStatus(payload);
|
|
119567
120326
|
}
|
|
120327
|
+
updateGoalObjective({ agentId, ...payload }) {
|
|
120328
|
+
return this.getAgent(agentId).updateGoalObjective(payload);
|
|
120329
|
+
}
|
|
119568
120330
|
cancelGoal({ agentId, ...payload }) {
|
|
119569
120331
|
return this.getAgent(agentId).cancelGoal(payload);
|
|
119570
120332
|
}
|
|
@@ -121307,6 +122069,9 @@ var ScreamCore = class {
|
|
|
121307
122069
|
updateGoalStatus({ sessionId, ...payload }) {
|
|
121308
122070
|
return this.sessionApi(sessionId).updateGoalStatus(payload);
|
|
121309
122071
|
}
|
|
122072
|
+
updateGoalObjective({ sessionId, ...payload }) {
|
|
122073
|
+
return this.sessionApi(sessionId).updateGoalObjective(payload);
|
|
122074
|
+
}
|
|
121310
122075
|
cancelGoal({ sessionId, ...payload }) {
|
|
121311
122076
|
return this.sessionApi(sessionId).cancelGoal(payload);
|
|
121312
122077
|
}
|
|
@@ -121760,6 +122525,13 @@ var SDKRpcClient = class {
|
|
|
121760
122525
|
status: input.status
|
|
121761
122526
|
});
|
|
121762
122527
|
}
|
|
122528
|
+
async updateGoalObjective(input) {
|
|
122529
|
+
return (await this.getRpc()).updateGoalObjective({
|
|
122530
|
+
sessionId: input.sessionId,
|
|
122531
|
+
agentId: this.interactiveAgentId,
|
|
122532
|
+
objective: input.objective
|
|
122533
|
+
});
|
|
122534
|
+
}
|
|
121763
122535
|
async cancelGoal(input) {
|
|
121764
122536
|
return (await this.getRpc()).cancelGoal({
|
|
121765
122537
|
sessionId: input.sessionId,
|
|
@@ -122520,6 +123292,13 @@ var Session = class {
|
|
|
122520
123292
|
status
|
|
122521
123293
|
});
|
|
122522
123294
|
}
|
|
123295
|
+
async updateGoalObjective(objective) {
|
|
123296
|
+
this.ensureOpen();
|
|
123297
|
+
return this.rpc.updateGoalObjective({
|
|
123298
|
+
sessionId: this.id,
|
|
123299
|
+
objective
|
|
123300
|
+
});
|
|
123301
|
+
}
|
|
122523
123302
|
async cancelGoal() {
|
|
122524
123303
|
this.ensureOpen();
|
|
122525
123304
|
return this.rpc.cancelGoal({ sessionId: this.id });
|
|
@@ -122979,7 +123758,7 @@ function optionalBuildString(value) {
|
|
|
122979
123758
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
122980
123759
|
}
|
|
122981
123760
|
const SCREAM_BUILD_INFO = {
|
|
122982
|
-
version: optionalBuildString("0.10.
|
|
123761
|
+
version: optionalBuildString("0.10.9"),
|
|
122983
123762
|
channel: optionalBuildString(""),
|
|
122984
123763
|
commit: optionalBuildString(""),
|
|
122985
123764
|
buildTarget: optionalBuildString("darwin-arm64")
|
|
@@ -126197,9 +126976,15 @@ function formatGoalDuration(ms) {
|
|
|
126197
126976
|
const seconds = totalSeconds % 60;
|
|
126198
126977
|
return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
|
|
126199
126978
|
}
|
|
126200
|
-
/** Build the footer goal badge: `GOAL 3m · 7 turns`.
|
|
126201
|
-
|
|
126202
|
-
|
|
126979
|
+
/** Build the footer goal badge: `GOAL 3m · 7 turns`.
|
|
126980
|
+
*
|
|
126981
|
+
* The TUI only receives `goal.updated` events on state changes, so we keep a
|
|
126982
|
+
* local base timestamp and add the elapsed time since the last snapshot to
|
|
126983
|
+
* produce a live wall-clock reading between sparse events.
|
|
126984
|
+
*/
|
|
126985
|
+
function formatGoalBadge(goal) {
|
|
126986
|
+
const elapsedSinceSnapshot = Date.now() - goal.wallClockBaseAt;
|
|
126987
|
+
return `GOAL ${formatGoalDuration(goal.wallClockMs + Math.max(0, elapsedSinceSnapshot))} · ${goal.turnsUsed} turns`;
|
|
126203
126988
|
}
|
|
126204
126989
|
const CONTEXT_WARNING_PERCENT_THRESHOLD = 60;
|
|
126205
126990
|
const CONTEXT_ERROR_PERCENT_THRESHOLD = 90;
|
|
@@ -126270,6 +127055,30 @@ function formatFooterGitBadge(status, colors) {
|
|
|
126270
127055
|
if (status.pullRequest === null) return base;
|
|
126271
127056
|
return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
|
|
126272
127057
|
}
|
|
127058
|
+
/**
|
|
127059
|
+
* Middle-truncate a (possibly ANSI-colored) string to `maxWidth` visible
|
|
127060
|
+
* columns, keeping a head and a tail fragment joined by `ellipsis`. The
|
|
127061
|
+
* remaining budget is split roughly in half so both the start and the end of
|
|
127062
|
+
* the original content stay visible - e.g. `GOAL 3m · 7 turns` becomes
|
|
127063
|
+
* `GOAL… turns` (head `GOAL`, tail `turns`). ANSI styling is preserved via
|
|
127064
|
+
* sliceByColumn, which carries the active SGR state into the tail; a reset is
|
|
127065
|
+
* emitted around the ellipsis so a colour opened in the head never bleeds
|
|
127066
|
+
* across it. Exported for unit testing.
|
|
127067
|
+
*/
|
|
127068
|
+
function truncateMiddle(line, maxWidth, ellipsis) {
|
|
127069
|
+
if (maxWidth <= 0) return "";
|
|
127070
|
+
const textWidth = visibleWidth(line);
|
|
127071
|
+
if (textWidth <= maxWidth) return line;
|
|
127072
|
+
const ellipsisWidth = visibleWidth(ellipsis);
|
|
127073
|
+
if (ellipsisWidth >= maxWidth) return truncateToWidth(line, maxWidth, ellipsis);
|
|
127074
|
+
const keepWidth = maxWidth - ellipsisWidth;
|
|
127075
|
+
const headWidth = Math.max(0, Math.ceil(keepWidth / 2));
|
|
127076
|
+
const tailWidth = keepWidth - headWidth;
|
|
127077
|
+
const head = headWidth > 0 ? sliceByColumn(line, 0, headWidth) : "";
|
|
127078
|
+
const tail = tailWidth > 0 ? sliceByColumn(line, textWidth - tailWidth, tailWidth) : "";
|
|
127079
|
+
const RESET = "\x1B[0m";
|
|
127080
|
+
return head + RESET + ellipsis + RESET + tail;
|
|
127081
|
+
}
|
|
126273
127082
|
var FooterComponent = class {
|
|
126274
127083
|
state;
|
|
126275
127084
|
colors;
|
|
@@ -126295,15 +127104,17 @@ var FooterComponent = class {
|
|
|
126295
127104
|
this.onGitStatusChange = onGitStatusChange;
|
|
126296
127105
|
this.gitCacheWorkDir = state.workDir;
|
|
126297
127106
|
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
127107
|
+
this.#restartStatusTimer(state.streamingPhase, state.goalActive);
|
|
126298
127108
|
}
|
|
126299
127109
|
setState(state) {
|
|
126300
127110
|
const previousPhase = this.state?.streamingPhase;
|
|
127111
|
+
const previousGoalActive = this.state?.goalActive;
|
|
126301
127112
|
if (state.workDir !== this.gitCacheWorkDir) {
|
|
126302
127113
|
this.gitCacheWorkDir = state.workDir;
|
|
126303
127114
|
this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
|
|
126304
127115
|
}
|
|
126305
127116
|
this.state = state;
|
|
126306
|
-
if (state.streamingPhase !== previousPhase) this.#restartStatusTimer(state.streamingPhase);
|
|
127117
|
+
if (state.streamingPhase !== previousPhase || state.goalActive !== previousGoalActive) this.#restartStatusTimer(state.streamingPhase, state.goalActive);
|
|
126307
127118
|
}
|
|
126308
127119
|
setColors(colors) {
|
|
126309
127120
|
this.colors = colors;
|
|
@@ -126331,9 +127142,9 @@ var FooterComponent = class {
|
|
|
126331
127142
|
dispose() {
|
|
126332
127143
|
this.#stopStatusTimer();
|
|
126333
127144
|
}
|
|
126334
|
-
#restartStatusTimer(phase) {
|
|
127145
|
+
#restartStatusTimer(phase, goalActive) {
|
|
126335
127146
|
this.#stopStatusTimer();
|
|
126336
|
-
if (phase === "idle") return;
|
|
127147
|
+
if (phase === "idle" && !goalActive) return;
|
|
126337
127148
|
const intervalMs = 1e3 / 60;
|
|
126338
127149
|
this.statusTimer = setInterval(() => {
|
|
126339
127150
|
this.ui.requestRender();
|
|
@@ -126354,8 +127165,7 @@ var FooterComponent = class {
|
|
|
126354
127165
|
}
|
|
126355
127166
|
if (state.wolfpackMode) left.push(chalk.hex(colors.wolfpackMode).bold(t("badge.wolfpack")));
|
|
126356
127167
|
if (state.goalActive && state.goal) {
|
|
126357
|
-
const
|
|
126358
|
-
const goalLabel = formatGoalBadge(g.wallClockMs, g.turnsUsed);
|
|
127168
|
+
const goalLabel = formatGoalBadge(state.goal);
|
|
126359
127169
|
left.push(chalk.hex(colors.primary).bold(goalLabel));
|
|
126360
127170
|
}
|
|
126361
127171
|
const model = shortenModel(modelDisplayName(state));
|
|
@@ -126377,13 +127187,21 @@ var FooterComponent = class {
|
|
|
126377
127187
|
}
|
|
126378
127188
|
const rightWidth = visibleWidth(rightText);
|
|
126379
127189
|
const gap = 3;
|
|
127190
|
+
const ellipsis = chalk.hex(colors.textDim)("…");
|
|
126380
127191
|
let line1;
|
|
126381
127192
|
if (leftWidth + gap + rightWidth <= width) {
|
|
126382
127193
|
const pad = width - leftWidth - rightWidth;
|
|
126383
127194
|
line1 = leftLine + " ".repeat(pad) + rightText;
|
|
126384
|
-
} else
|
|
126385
|
-
|
|
126386
|
-
|
|
127195
|
+
} else {
|
|
127196
|
+
const targetLeft = width - gap - rightWidth;
|
|
127197
|
+
if (targetLeft > 0) {
|
|
127198
|
+
const shrunkLeft = truncateMiddle(leftLine, targetLeft, ellipsis);
|
|
127199
|
+
const pad = width - visibleWidth(shrunkLeft) - rightWidth;
|
|
127200
|
+
line1 = shrunkLeft + " ".repeat(pad) + rightText;
|
|
127201
|
+
} else if (leftWidth <= width) line1 = leftLine + " ".repeat(width - leftWidth);
|
|
127202
|
+
else line1 = truncateMiddle(leftLine, width, ellipsis);
|
|
127203
|
+
}
|
|
127204
|
+
return [truncateToWidth(line1, width, "…")];
|
|
126387
127205
|
}
|
|
126388
127206
|
};
|
|
126389
127207
|
//#endregion
|
|
@@ -126731,10 +127549,63 @@ function createThemeStyles(colors) {
|
|
|
126731
127549
|
//#endregion
|
|
126732
127550
|
//#region src/tui/theme/pi-tui-theme.ts
|
|
126733
127551
|
const HEADING_HASH_PREFIX = /^((?:\u001B\[[0-9;]*m)*)#{1,6}[ \t]+/;
|
|
127552
|
+
/**
|
|
127553
|
+
* Map cli-highlight syntax tokens onto the ColorPalette so code blocks follow
|
|
127554
|
+
* the active theme instead of cli-highlight's built-in colors. cli-highlight
|
|
127555
|
+
* takes one formatter function per token; tokens not listed here fall back to
|
|
127556
|
+
* its DEFAULT_THEME.
|
|
127557
|
+
*/
|
|
127558
|
+
function createCodeHighlightTheme(colors) {
|
|
127559
|
+
const keyword = chalk.hex(colors.primary);
|
|
127560
|
+
const str = chalk.hex(colors.success);
|
|
127561
|
+
const comment = chalk.hex(colors.textDim);
|
|
127562
|
+
const num = chalk.hex(colors.warning);
|
|
127563
|
+
const fn = chalk.hex(colors.primary);
|
|
127564
|
+
const cls = chalk.hex(colors.accent);
|
|
127565
|
+
const text = chalk.hex(colors.text);
|
|
127566
|
+
return {
|
|
127567
|
+
keyword,
|
|
127568
|
+
built_in: fn,
|
|
127569
|
+
type: cls,
|
|
127570
|
+
literal: num,
|
|
127571
|
+
number: num,
|
|
127572
|
+
regexp: str,
|
|
127573
|
+
string: str,
|
|
127574
|
+
subst: str,
|
|
127575
|
+
symbol: num,
|
|
127576
|
+
class: cls,
|
|
127577
|
+
function: fn,
|
|
127578
|
+
title: fn,
|
|
127579
|
+
params: text,
|
|
127580
|
+
comment,
|
|
127581
|
+
doctag: comment,
|
|
127582
|
+
meta: chalk.hex(colors.textMuted),
|
|
127583
|
+
"meta-keyword": keyword,
|
|
127584
|
+
"meta-string": str,
|
|
127585
|
+
section: keyword,
|
|
127586
|
+
tag: cls,
|
|
127587
|
+
name: fn,
|
|
127588
|
+
"builtin-name": fn,
|
|
127589
|
+
attr: num,
|
|
127590
|
+
attribute: num,
|
|
127591
|
+
variable: text,
|
|
127592
|
+
bullet: num,
|
|
127593
|
+
code: str,
|
|
127594
|
+
emphasis: (s) => chalk.italic(s),
|
|
127595
|
+
strong: (s) => chalk.bold(s),
|
|
127596
|
+
formula: text,
|
|
127597
|
+
link: chalk.hex(colors.mdLink),
|
|
127598
|
+
quote: chalk.hex(colors.mdQuote),
|
|
127599
|
+
addition: chalk.hex(colors.diffAdded),
|
|
127600
|
+
deletion: chalk.hex(colors.diffRemoved),
|
|
127601
|
+
default: text
|
|
127602
|
+
};
|
|
127603
|
+
}
|
|
126734
127604
|
function createMarkdownTheme(colors) {
|
|
126735
127605
|
const stripHash = (text) => text.replace(HEADING_HASH_PREFIX, "$1");
|
|
126736
127606
|
const muted = chalk.hex(colors.textMuted);
|
|
126737
127607
|
const border = chalk.hex(colors.border);
|
|
127608
|
+
const codeTheme = createCodeHighlightTheme(colors);
|
|
126738
127609
|
return {
|
|
126739
127610
|
heading: (text) => chalk.bold.hex(colors.text)(stripHash(text)),
|
|
126740
127611
|
link: (text) => chalk.hex(colors.mdLink)(text),
|
|
@@ -126756,7 +127627,8 @@ function createMarkdownTheme(colors) {
|
|
|
126756
127627
|
try {
|
|
126757
127628
|
return highlight(code, {
|
|
126758
127629
|
language,
|
|
126759
|
-
ignoreIllegals: true
|
|
127630
|
+
ignoreIllegals: true,
|
|
127631
|
+
theme: codeTheme
|
|
126760
127632
|
}).split("\n");
|
|
126761
127633
|
} catch {
|
|
126762
127634
|
return code.split("\n");
|
|
@@ -128185,6 +129057,10 @@ function buildEmptyGoalLines(colors) {
|
|
|
128185
129057
|
value(t("goalpanel.no_goal")),
|
|
128186
129058
|
"",
|
|
128187
129059
|
`${muted("/goal")} ${value(t("goalpanel.goal_placeholder"))} ${muted(t("goalpanel.create_goal"))}`,
|
|
129060
|
+
`${muted("/goal setup")} ${muted(t("goalpanel.setup_goal"))}`,
|
|
129061
|
+
`${muted("/goal update")} ${value("<new objective>")} ${muted(t("goalpanel.update_goal"))}`,
|
|
129062
|
+
`${muted("/goal status")} ${muted(t("goalpanel.status_goal"))}`,
|
|
129063
|
+
`${muted("/goal replace")} ${value("<new objective>")} ${muted(t("goalpanel.replace_goal"))}`,
|
|
128188
129064
|
`${muted("/goal pause")} ${muted(t("goalpanel.pause_goal"))}`,
|
|
128189
129065
|
`${muted("/goal resume")} ${muted(t("goalpanel.resume_goal"))}`,
|
|
128190
129066
|
`${muted("/goaloff")} ${muted(t("goalpanel.cancel_goal"))}`
|
|
@@ -128247,6 +129123,19 @@ function parseGoalCommand(rawArgs) {
|
|
|
128247
129123
|
const tokens = args.split(/\s+/);
|
|
128248
129124
|
const first = tokens[0];
|
|
128249
129125
|
if (first !== void 0 && CONTROL_SUBCOMMANDS.has(first) && tokens.length === 1) return { kind: first };
|
|
129126
|
+
if (first === "setup") return { kind: "setup" };
|
|
129127
|
+
if (first === "update") {
|
|
129128
|
+
const objective = tokens.slice(1).join(" ").trim();
|
|
129129
|
+
if (objective.length === 0) return {
|
|
129130
|
+
kind: "error",
|
|
129131
|
+
severity: "hint",
|
|
129132
|
+
message: t("goal.need_desc")
|
|
129133
|
+
};
|
|
129134
|
+
return {
|
|
129135
|
+
kind: "update",
|
|
129136
|
+
objective
|
|
129137
|
+
};
|
|
129138
|
+
}
|
|
128250
129139
|
let index = 0;
|
|
128251
129140
|
let replace = false;
|
|
128252
129141
|
if (tokens[index] === "replace") {
|
|
@@ -128276,6 +129165,9 @@ async function handleGoalCommand(host, args) {
|
|
|
128276
129165
|
case "status":
|
|
128277
129166
|
await showGoalStatus(host);
|
|
128278
129167
|
return;
|
|
129168
|
+
case "setup":
|
|
129169
|
+
await guidedGoalSetup(host);
|
|
129170
|
+
return;
|
|
128279
129171
|
case "pause":
|
|
128280
129172
|
await pauseGoal(host);
|
|
128281
129173
|
return;
|
|
@@ -128285,6 +129177,9 @@ async function handleGoalCommand(host, args) {
|
|
|
128285
129177
|
case "off":
|
|
128286
129178
|
await handleGoalOffCommand(host);
|
|
128287
129179
|
return;
|
|
129180
|
+
case "update":
|
|
129181
|
+
await updateGoalObjective(host, parsed);
|
|
129182
|
+
return;
|
|
128288
129183
|
case "create":
|
|
128289
129184
|
await createGoal(host, parsed);
|
|
128290
129185
|
return;
|
|
@@ -128302,8 +129197,51 @@ async function createGoal(host, parsed) {
|
|
|
128302
129197
|
}
|
|
128303
129198
|
await showGoalConfigWizard(host, session, parsed.objective, parsed.replace);
|
|
128304
129199
|
}
|
|
129200
|
+
const GOAL_REFINER_SYSTEM_PROMPT = "You are a goal refiner. Given a brief task description, produce a single clear, actionable objective sentence (max 200 chars). Do not add explanations, quotes, or prefixes.";
|
|
129201
|
+
/**
|
|
129202
|
+
* Guided goal creation: collect a brief task description, refine it via the
|
|
129203
|
+
* LLM into a single objective sentence, let the user confirm/edit, then enter
|
|
129204
|
+
* the standard configuration wizard. Falls back to the raw description if the
|
|
129205
|
+
* LLM call fails.
|
|
129206
|
+
*/
|
|
129207
|
+
async function guidedGoalSetup(host) {
|
|
129208
|
+
const session = host.session;
|
|
129209
|
+
if (session === void 0) {
|
|
129210
|
+
host.showError(t("error.no_session"));
|
|
129211
|
+
return;
|
|
129212
|
+
}
|
|
129213
|
+
if (detectGoalConflict(host.state.appState, "enable_goal") === "goal_active") {
|
|
129214
|
+
host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
|
|
129215
|
+
return;
|
|
129216
|
+
}
|
|
129217
|
+
const { TextInputDialogComponent } = await import("./text-input-dialog-B1ak519Y.mjs");
|
|
129218
|
+
const initialDesc = await promptText(host, TextInputDialogComponent, {
|
|
129219
|
+
title: t("goal.setup_title_initial"),
|
|
129220
|
+
subtitle: t("goal.setup_desc_hint"),
|
|
129221
|
+
placeholder: t("goal.setup_desc_placeholder"),
|
|
129222
|
+
allowEmpty: false
|
|
129223
|
+
});
|
|
129224
|
+
if (initialDesc === void 0) return;
|
|
129225
|
+
host.showStatus(t("goal.setup_refining"));
|
|
129226
|
+
let objective;
|
|
129227
|
+
try {
|
|
129228
|
+
objective = (await session.generateText(GOAL_REFINER_SYSTEM_PROMPT, initialDesc)).trim();
|
|
129229
|
+
if (objective.length === 0) objective = initialDesc;
|
|
129230
|
+
} catch {
|
|
129231
|
+
objective = initialDesc;
|
|
129232
|
+
}
|
|
129233
|
+
const confirmed = await promptText(host, TextInputDialogComponent, {
|
|
129234
|
+
title: t("goal.setup_title_confirm"),
|
|
129235
|
+
subtitle: t("goal.setup_confirm_hint"),
|
|
129236
|
+
placeholder: objective,
|
|
129237
|
+
initialValue: objective,
|
|
129238
|
+
allowEmpty: true
|
|
129239
|
+
});
|
|
129240
|
+
if (confirmed === void 0) return;
|
|
129241
|
+
await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
|
|
129242
|
+
}
|
|
128305
129243
|
async function showGoalConfigWizard(host, session, objective, replace) {
|
|
128306
|
-
const { TextInputDialogComponent } = await import("./text-input-dialog-
|
|
129244
|
+
const { TextInputDialogComponent } = await import("./text-input-dialog-B1ak519Y.mjs");
|
|
128307
129245
|
const turnInput = await promptNumber(host, TextInputDialogComponent, {
|
|
128308
129246
|
title: t("goal.wizard_title", { objective }),
|
|
128309
129247
|
subtitle: t("goal.budget_turns_hint"),
|
|
@@ -128373,6 +129311,27 @@ function promptNumber(host, TextInputDialogComponent, opts) {
|
|
|
128373
129311
|
host.mountEditorReplacement(dialog);
|
|
128374
129312
|
});
|
|
128375
129313
|
}
|
|
129314
|
+
/** Prompt user for free-form text. Returns undefined on cancel. */
|
|
129315
|
+
function promptText(host, TextInputDialogComponent, opts) {
|
|
129316
|
+
return new Promise((resolve) => {
|
|
129317
|
+
const dialog = new TextInputDialogComponent((result) => {
|
|
129318
|
+
host.restoreEditor();
|
|
129319
|
+
if (result.kind !== "ok") {
|
|
129320
|
+
resolve(void 0);
|
|
129321
|
+
return;
|
|
129322
|
+
}
|
|
129323
|
+
resolve(result.value.trim());
|
|
129324
|
+
}, {
|
|
129325
|
+
title: opts.title,
|
|
129326
|
+
subtitle: opts.subtitle,
|
|
129327
|
+
placeholder: opts.placeholder,
|
|
129328
|
+
initialValue: opts.initialValue,
|
|
129329
|
+
allowEmpty: opts.allowEmpty,
|
|
129330
|
+
colors: host.state.theme.colors
|
|
129331
|
+
});
|
|
129332
|
+
host.mountEditorReplacement(dialog);
|
|
129333
|
+
});
|
|
129334
|
+
}
|
|
128376
129335
|
async function pauseGoal(host) {
|
|
128377
129336
|
const session = host.session;
|
|
128378
129337
|
if (session === void 0) {
|
|
@@ -128413,6 +129372,24 @@ async function resumeGoal(host) {
|
|
|
128413
129372
|
host.showError(t("goal.resume_failed", { msg: message }));
|
|
128414
129373
|
}
|
|
128415
129374
|
}
|
|
129375
|
+
async function updateGoalObjective(host, parsed) {
|
|
129376
|
+
const session = host.session;
|
|
129377
|
+
if (session === void 0) {
|
|
129378
|
+
host.showError(t("error.no_session"));
|
|
129379
|
+
return;
|
|
129380
|
+
}
|
|
129381
|
+
try {
|
|
129382
|
+
if ((await session.getGoal()).goal === null) {
|
|
129383
|
+
host.showStatus(t("goal.no_active"));
|
|
129384
|
+
return;
|
|
129385
|
+
}
|
|
129386
|
+
await session.updateGoalObjective(parsed.objective);
|
|
129387
|
+
host.showStatus(t("goal.updated", { objective: parsed.objective }));
|
|
129388
|
+
} catch (error) {
|
|
129389
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129390
|
+
host.showError(t("goal.update_failed", { msg: message }));
|
|
129391
|
+
}
|
|
129392
|
+
}
|
|
128416
129393
|
async function handleGoalOffCommand(host) {
|
|
128417
129394
|
const session = host.session;
|
|
128418
129395
|
if (session === void 0) {
|
|
@@ -129442,6 +130419,12 @@ function easeSpeedRatio(ratio) {
|
|
|
129442
130419
|
}
|
|
129443
130420
|
//#endregion
|
|
129444
130421
|
//#region src/tui/components/messages/thinking.ts
|
|
130422
|
+
/** gpt-5 reasoning summaries contain empty HTML comment padding sentinels
|
|
130423
|
+
* like `<!-- -->`. Strip them to keep the thinking display clean. */
|
|
130424
|
+
const EMPTY_COMMENT_RE = /<!--\s*-->/g;
|
|
130425
|
+
function filterThinkingNoise(text) {
|
|
130426
|
+
return text.replace(EMPTY_COMMENT_RE, "");
|
|
130427
|
+
}
|
|
129445
130428
|
var ThinkingComponent = class {
|
|
129446
130429
|
text;
|
|
129447
130430
|
color;
|
|
@@ -129480,7 +130463,7 @@ var ThinkingComponent = class {
|
|
|
129480
130463
|
this.textComponent.setText(this.styled(trimmed));
|
|
129481
130464
|
}
|
|
129482
130465
|
styled(text) {
|
|
129483
|
-
return chalk.hex(this.color).italic(text);
|
|
130466
|
+
return chalk.hex(this.color).italic(filterThinkingNoise(text));
|
|
129484
130467
|
}
|
|
129485
130468
|
finalize() {
|
|
129486
130469
|
if (this.mode === "finalized") return;
|
|
@@ -129657,6 +130640,68 @@ function makeDiffStyles(colors) {
|
|
|
129657
130640
|
meta: (s) => chalk.hex(colors.diffMeta)(s)
|
|
129658
130641
|
};
|
|
129659
130642
|
}
|
|
130643
|
+
const ANSI_RE = /\u001B\[[0-9;]*m/;
|
|
130644
|
+
const ANSI_RESET_RE = /\u001B\[(?:0|39)m/g;
|
|
130645
|
+
/**
|
|
130646
|
+
* Extract the leading ANSI SGR prefix a chalk-style color function applies,
|
|
130647
|
+
* e.g. `\x1b[38;2;121;235;0m` for a truecolor hex. Returns `''` when the
|
|
130648
|
+
* function emits no color (chalk level 0 / non-color environment).
|
|
130649
|
+
*/
|
|
130650
|
+
function extractAnsiPrefix(fn) {
|
|
130651
|
+
const sentinel = "\0";
|
|
130652
|
+
const styled = fn(sentinel);
|
|
130653
|
+
const idx = styled.indexOf(sentinel);
|
|
130654
|
+
return idx >= 0 ? styled.slice(0, idx) : "";
|
|
130655
|
+
}
|
|
130656
|
+
/** Visualize leading whitespace: tabs as `->`, leading spaces as `·`. */
|
|
130657
|
+
function visualizeIndent(line) {
|
|
130658
|
+
let i = 0;
|
|
130659
|
+
let visual = "";
|
|
130660
|
+
while (i < line.length) {
|
|
130661
|
+
const ch = line[i];
|
|
130662
|
+
if (ch === " ") {
|
|
130663
|
+
visual += "->";
|
|
130664
|
+
i++;
|
|
130665
|
+
continue;
|
|
130666
|
+
}
|
|
130667
|
+
if (ch === " ") {
|
|
130668
|
+
visual += "·";
|
|
130669
|
+
i++;
|
|
130670
|
+
continue;
|
|
130671
|
+
}
|
|
130672
|
+
break;
|
|
130673
|
+
}
|
|
130674
|
+
return {
|
|
130675
|
+
text: visual + line.slice(i),
|
|
130676
|
+
indentEnd: visual.length
|
|
130677
|
+
};
|
|
130678
|
+
}
|
|
130679
|
+
/**
|
|
130680
|
+
* Render a diff line's code with leading-whitespace visualization (tabs as
|
|
130681
|
+
* `->`, spaces as `·`, dimmed) and optional syntax highlighting. Highlighting
|
|
130682
|
+
* runs on the raw code first; indent visualization then runs on the
|
|
130683
|
+
* highlighted string - leading whitespace carries no token color, so it is
|
|
130684
|
+
* still plain and can be replaced and dimmed without disturbing the tokens.
|
|
130685
|
+
*
|
|
130686
|
+
* When highlighting is on and produced token colors, the diff line color is
|
|
130687
|
+
* layered as the base foreground: syntax tokens override it, but every ANSI
|
|
130688
|
+
* reset re-injects the diff color so non-token spans (punctuation, operators,
|
|
130689
|
+
* whitespace) stay green/red instead of falling back to the terminal default.
|
|
130690
|
+
* When highlighting is off (streaming) or produced no token colors, the code
|
|
130691
|
+
* part is colored with the diff line color instead.
|
|
130692
|
+
*/
|
|
130693
|
+
function renderDiffCode(code, colorFn, highlight, lang) {
|
|
130694
|
+
const { text, indentEnd } = visualizeIndent(highlight ? highlightLines(code, lang)[0] ?? code : code);
|
|
130695
|
+
const indent = text.slice(0, indentEnd);
|
|
130696
|
+
const rest = text.slice(indentEnd);
|
|
130697
|
+
const dimIndent = indent.length > 0 ? chalk.dim(indent) : indent;
|
|
130698
|
+
if (highlight && ANSI_RE.test(rest)) {
|
|
130699
|
+
const prefix = extractAnsiPrefix(colorFn);
|
|
130700
|
+
if (prefix.length > 0) return dimIndent + prefix + rest.replace(ANSI_RESET_RE, (m) => m + prefix) + "\x1B[39m";
|
|
130701
|
+
return dimIndent + rest;
|
|
130702
|
+
}
|
|
130703
|
+
return dimIndent + colorFn(rest);
|
|
130704
|
+
}
|
|
129660
130705
|
/**
|
|
129661
130706
|
* Compute word-level diff between two single lines and highlight the changed
|
|
129662
130707
|
* words with `chalk.inverse()`. Only the first removed/added part has its
|
|
@@ -129763,6 +130808,8 @@ function computeDiffLines(oldText, newText, oldStart = 1, newStart = 1, isIncomp
|
|
|
129763
130808
|
}
|
|
129764
130809
|
function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, oldStart, newStart, maxLines) {
|
|
129765
130810
|
const s = makeDiffStyles(colors);
|
|
130811
|
+
const lang = langFromPath(path);
|
|
130812
|
+
const doHighlight = !isIncomplete && lang !== void 0;
|
|
129766
130813
|
const changedLines = computeDiffLines(oldText, newText, oldStart ?? 1, newStart ?? 1, isIncomplete).filter((l) => l.kind !== "context");
|
|
129767
130814
|
const added = changedLines.filter((l) => l.kind === "add").length;
|
|
129768
130815
|
const removed = changedLines.filter((l) => l.kind === "delete").length;
|
|
@@ -129787,7 +130834,7 @@ function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, o
|
|
|
129787
130834
|
const line = shown[i];
|
|
129788
130835
|
const marker = line.kind === "add" ? "+" : "-";
|
|
129789
130836
|
const color = line.kind === "add" ? s.add : s.del;
|
|
129790
|
-
output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + color(marker
|
|
130837
|
+
output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + color(`${marker} `) + renderDiffCode(line.code, color, doHighlight, lang));
|
|
129791
130838
|
i += 1;
|
|
129792
130839
|
}
|
|
129793
130840
|
const hidden = changedLines.length - shown.length;
|
|
@@ -129838,11 +130885,11 @@ function buildClusters(diffLines, contextLines) {
|
|
|
129838
130885
|
removedCount: removed
|
|
129839
130886
|
};
|
|
129840
130887
|
}
|
|
129841
|
-
function formatDiffRow(line, s) {
|
|
130888
|
+
function formatDiffRow(line, s, doHighlight, lang) {
|
|
129842
130889
|
const gutter = s.gutter(String(line.lineNum).padStart(4) + " ");
|
|
129843
|
-
if (line.kind === "add") return gutter + s.add("+ " + line.code);
|
|
129844
|
-
if (line.kind === "delete") return gutter + s.del("- " + line.code);
|
|
129845
|
-
return gutter + " " + line.code;
|
|
130890
|
+
if (line.kind === "add") return gutter + s.add("+ ") + renderDiffCode(line.code, s.add, doHighlight, lang);
|
|
130891
|
+
if (line.kind === "delete") return gutter + s.del("- ") + renderDiffCode(line.code, s.del, doHighlight, lang);
|
|
130892
|
+
return gutter + " " + renderDiffCode(line.code, (x) => x, doHighlight, lang);
|
|
129846
130893
|
}
|
|
129847
130894
|
/**
|
|
129848
130895
|
* Render a diff with surrounding context, eliding unchanged middle
|
|
@@ -129855,6 +130902,8 @@ function formatDiffRow(line, s) {
|
|
|
129855
130902
|
*/
|
|
129856
130903
|
function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
|
|
129857
130904
|
const s = makeDiffStyles(colors);
|
|
130905
|
+
const lang = langFromPath(path);
|
|
130906
|
+
const doHighlight = !(opts.isIncomplete ?? false) && lang !== void 0;
|
|
129858
130907
|
const contextLines = opts.contextLines ?? 3;
|
|
129859
130908
|
const maxLines = opts.maxLines;
|
|
129860
130909
|
const diffLines = computeDiffLines(oldText, newText, 1, 1, opts.isIncomplete ?? false);
|
|
@@ -129911,7 +130960,7 @@ function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
|
|
|
129911
130960
|
i += 2;
|
|
129912
130961
|
continue;
|
|
129913
130962
|
}
|
|
129914
|
-
output.push(formatDiffRow(line, s));
|
|
130963
|
+
output.push(formatDiffRow(line, s, doHighlight, lang));
|
|
129915
130964
|
body++;
|
|
129916
130965
|
if (line.kind !== "context") shownChanges++;
|
|
129917
130966
|
prevEnd = i;
|
|
@@ -130077,34 +131126,43 @@ function truncateTailBytes(text, maxBytes) {
|
|
|
130077
131126
|
}
|
|
130078
131127
|
/**
|
|
130079
131128
|
* Component that renders tool output with wrap-aware line truncation.
|
|
130080
|
-
* Uses pi-tui's Text component to compute actual visual wrapped lines,
|
|
130081
|
-
*
|
|
130082
|
-
*
|
|
131129
|
+
* Uses pi-tui's Text component to compute actual visual wrapped lines, then
|
|
131130
|
+
* caps at `maxLines`. When collapsed the TAIL is shown (newest output, where
|
|
131131
|
+
* command errors land) with an expand hint at the top; when expanded the full
|
|
131132
|
+
* output is shown with a collapse hint at the top. Handles long single-line
|
|
131133
|
+
* output (e.g. JSON blobs) that would otherwise wrap to dozens of visual rows.
|
|
130083
131134
|
*/
|
|
130084
131135
|
var TruncatedOutputComponent = class {
|
|
130085
131136
|
textComponent;
|
|
130086
131137
|
expanded;
|
|
130087
131138
|
maxLines;
|
|
130088
131139
|
hintFormatter;
|
|
131140
|
+
collapseHintFormatter;
|
|
130089
131141
|
constructor(output, options) {
|
|
130090
131142
|
this.expanded = options.expanded;
|
|
130091
131143
|
this.maxLines = options.maxLines ?? PREVIEW_LINES;
|
|
130092
131144
|
this.hintFormatter = options.hintFormatter;
|
|
131145
|
+
this.collapseHintFormatter = options.collapseHintFormatter;
|
|
130093
131146
|
const tint = options.isError ? chalk.hex(options.colors.error) : chalk.dim;
|
|
130094
131147
|
const cleaned = trimTrailingEmptyLines(output.split("\n")).join("\n");
|
|
130095
|
-
const
|
|
130096
|
-
|
|
131148
|
+
const stripped = options.isError ? cleaned.replaceAll(/\u001B\[[0-9;]*m/g, "") : cleaned;
|
|
131149
|
+
const tinted = (options.maxBytes === void 0 ? stripped : truncateTailBytes(stripped, options.maxBytes)).split("\n").map((line) => tint(line)).join("\n");
|
|
131150
|
+
this.textComponent = new Text(tinted, 2, 0);
|
|
130097
131151
|
}
|
|
130098
131152
|
invalidate() {
|
|
130099
131153
|
this.textComponent.invalidate();
|
|
130100
131154
|
}
|
|
130101
131155
|
render(width) {
|
|
130102
131156
|
const contentLines = this.textComponent.render(width);
|
|
130103
|
-
if (
|
|
130104
|
-
|
|
131157
|
+
if (contentLines.length <= this.maxLines) return contentLines;
|
|
131158
|
+
if (this.expanded) {
|
|
131159
|
+
const collapseHint = this.collapseHintFormatter ? this.collapseHintFormatter() : t("shell.collapse_hint");
|
|
131160
|
+
return [chalk.dim(collapseHint), ...contentLines];
|
|
131161
|
+
}
|
|
130105
131162
|
const remaining = contentLines.length - this.maxLines;
|
|
130106
|
-
const
|
|
130107
|
-
|
|
131163
|
+
const tail = contentLines.slice(-this.maxLines);
|
|
131164
|
+
const expandHint = this.hintFormatter ? this.hintFormatter(remaining) : t("shell.more_lines", { count: String(remaining) });
|
|
131165
|
+
return [chalk.dim(expandHint), ...tail];
|
|
130108
131166
|
}
|
|
130109
131167
|
};
|
|
130110
131168
|
const renderTruncated = (_toolCall, result, ctx) => {
|
|
@@ -130121,7 +131179,7 @@ var ShellExecutionComponent = class extends Container {
|
|
|
130121
131179
|
constructor(options) {
|
|
130122
131180
|
super();
|
|
130123
131181
|
if (options.showCommand === true) this.addCommandPreview(options.command ?? "", options.commandPreviewLines);
|
|
130124
|
-
if (options.result !== void 0) this.addResultPreview(options.result, options.colors, options.expanded ?? false, options.resultPreviewLines ??
|
|
131182
|
+
if (options.result !== void 0) this.addResultPreview(options.result, options.colors, options.expanded ?? false, options.resultPreviewLines ?? 15);
|
|
130125
131183
|
}
|
|
130126
131184
|
addCommandPreview(command, previewLines) {
|
|
130127
131185
|
if (command.length === 0) return;
|
|
@@ -130140,7 +131198,8 @@ var ShellExecutionComponent = class extends Container {
|
|
|
130140
131198
|
colors,
|
|
130141
131199
|
maxLines: previewLines,
|
|
130142
131200
|
maxBytes: MAX_SHELL_OUTPUT_BYTES,
|
|
130143
|
-
hintFormatter: (remaining) => t("shell.more_lines", { count: String(remaining) })
|
|
131201
|
+
hintFormatter: (remaining) => t("shell.more_lines", { count: String(remaining) }),
|
|
131202
|
+
collapseHintFormatter: () => t("shell.collapse_hint")
|
|
130144
131203
|
}));
|
|
130145
131204
|
}
|
|
130146
131205
|
};
|
|
@@ -138363,6 +139422,8 @@ var SessionEventHandler = class {
|
|
|
138363
139422
|
this.host.showNotice(title, detail);
|
|
138364
139423
|
}
|
|
138365
139424
|
handleStepRetrying(event) {
|
|
139425
|
+
this.host.streamingUI.resetLiveText();
|
|
139426
|
+
this.host.streamingUI.resetToolUi();
|
|
138366
139427
|
this.host.setAppState({ reconnectAttempt: event.nextAttempt });
|
|
138367
139428
|
}
|
|
138368
139429
|
maybeShowDebugTiming(event) {
|
|
@@ -138652,7 +139713,8 @@ var SessionEventHandler = class {
|
|
|
138652
139713
|
goal: {
|
|
138653
139714
|
objective: snapshot.objective,
|
|
138654
139715
|
turnsUsed: snapshot.turnsUsed ?? 0,
|
|
138655
|
-
wallClockMs: snapshot.wallClockMs ?? 0
|
|
139716
|
+
wallClockMs: snapshot.wallClockMs ?? 0,
|
|
139717
|
+
wallClockBaseAt: Date.now()
|
|
138656
139718
|
},
|
|
138657
139719
|
goalActive: snapshot.status === "active"
|
|
138658
139720
|
});
|
|
@@ -144729,7 +145791,8 @@ var SessionManager = class {
|
|
|
144729
145791
|
goal: goal ? {
|
|
144730
145792
|
objective: goal.objective,
|
|
144731
145793
|
turnsUsed: goal.turnsUsed ?? 0,
|
|
144732
|
-
wallClockMs: goal.wallClockMs ?? 0
|
|
145794
|
+
wallClockMs: goal.wallClockMs ?? 0,
|
|
145795
|
+
wallClockBaseAt: Date.now()
|
|
144733
145796
|
} : null,
|
|
144734
145797
|
goalActive: goal?.status === "active",
|
|
144735
145798
|
goalContinuationCount: 0
|
|
@@ -146975,6 +148038,7 @@ var ScreamTUI = class {
|
|
|
146975
148038
|
deferUserMessages = false;
|
|
146976
148039
|
aborted = false;
|
|
146977
148040
|
isShuttingDown = false;
|
|
148041
|
+
tightModeHandler = null;
|
|
146978
148042
|
reverseRpcDisposers = [];
|
|
146979
148043
|
startupNotice;
|
|
146980
148044
|
updatePrefetched;
|
|
@@ -147071,6 +148135,11 @@ var ScreamTUI = class {
|
|
|
147071
148135
|
this.inputController.setupAutocomplete();
|
|
147072
148136
|
}
|
|
147073
148137
|
async start() {
|
|
148138
|
+
this.tightModeHandler = () => {
|
|
148139
|
+
setTightMode((process.stdout.columns ?? 80) < 60);
|
|
148140
|
+
};
|
|
148141
|
+
this.tightModeHandler();
|
|
148142
|
+
process.stdout.on("resize", this.tightModeHandler);
|
|
147074
148143
|
this.lifecycleController.installSignalHandlers();
|
|
147075
148144
|
try {
|
|
147076
148145
|
const shouldReplayHistory = await this.initMainTui();
|
|
@@ -147148,6 +148217,11 @@ var ScreamTUI = class {
|
|
|
147148
148217
|
async stop(exitCode) {
|
|
147149
148218
|
if (this.isShuttingDown) return;
|
|
147150
148219
|
this.isShuttingDown = true;
|
|
148220
|
+
if (this.tightModeHandler !== null) {
|
|
148221
|
+
process.stdout.off("resize", this.tightModeHandler);
|
|
148222
|
+
this.tightModeHandler = null;
|
|
148223
|
+
}
|
|
148224
|
+
setTightMode(false);
|
|
147151
148225
|
this.lifecycleController.stopCcConnectPolling();
|
|
147152
148226
|
this.lifecycleController.uninstallSignalHandlers();
|
|
147153
148227
|
this.aborted = true;
|