jeopi-cli 16.4.1 → 16.4.3
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/CHANGELOG.md +80 -0
- package/dist/cli.js +3179 -3140
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
- package/dist/types/internal-urls/types.d.ts +7 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/auth-broker-config.d.ts +2 -2
- package/dist/types/tools/bash-skill-urls.d.ts +3 -2
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +22 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/autolearn/controller.ts +14 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +63 -13
- package/src/cli/usage-error.ts +7 -0
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/config/model-resolver.ts +24 -6
- package/src/config/settings-schema.ts +3 -3
- package/src/edit/index.ts +10 -76
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
- package/src/eval/executor-base.ts +84 -7
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/memory-protocol.ts +13 -8
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +7 -0
- package/src/irc/bus.ts +48 -18
- package/src/lsp/client.ts +7 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +73 -2
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/controllers/event-controller.ts +17 -30
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/workflow.ts +5 -4
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/browser.md +5 -5
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +237 -50
- package/src/session/auth-broker-config.ts +4 -4
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/executor.ts +8 -0
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -22
- package/src/tools/browser/cmux/cmux-tab.ts +49 -50
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +44 -5
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +280 -133
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/glob.ts +20 -6
- package/src/tools/grep.ts +1 -0
- package/src/tools/image-gen.ts +3 -4
- package/src/tools/irc.ts +27 -13
- package/src/tools/read.ts +14 -7
- package/src/tools/write.ts +22 -5
- package/src/tools/yield.ts +29 -1
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +68 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
package/src/modes/workflow.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import workflowNotice from "../prompts/system/workflow-notice.md" with { type: "text" };
|
|
2
2
|
import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
|
|
3
|
+
import { magicKeywordRegex } from "./magic-keyword-boundary";
|
|
3
4
|
import { keywordInProse } from "./markdown-prose";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -14,15 +15,15 @@ import { keywordInProse } from "./markdown-prose";
|
|
|
14
15
|
* "workflowz.ts" never do.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
|
-
// Detection: lowercase keyword flanked by whitespace or a string edge.
|
|
18
|
-
const WORKFLOW_WORD =
|
|
18
|
+
// Detection: lowercase keyword flanked by prose punctuation, whitespace, or a string edge.
|
|
19
|
+
const WORKFLOW_WORD = magicKeywordRegex("workflowz");
|
|
19
20
|
|
|
20
21
|
/** Hidden system notice appended after a user message that mentions "workflowz". */
|
|
21
22
|
export const WORKFLOW_NOTICE: string = workflowNotice.trim();
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Whether `text` contains the standalone keyword "workflowz"
|
|
25
|
-
* (lowercase,
|
|
26
|
+
* (lowercase, prose-delimited) in prose — never inside a code block, inline
|
|
26
27
|
* code span, or XML/HTML section.
|
|
27
28
|
*/
|
|
28
29
|
export function containsWorkflow(text: string): boolean {
|
|
@@ -36,7 +37,7 @@ export function containsWorkflow(text: string): boolean {
|
|
|
36
37
|
*/
|
|
37
38
|
export const highlightWorkflow: KeywordHighlighter = createGradientHighlighter({
|
|
38
39
|
probe: /workflowz/,
|
|
39
|
-
highlight:
|
|
40
|
+
highlight: magicKeywordRegex("workflowz", "g"),
|
|
40
41
|
stops: 14,
|
|
41
42
|
hue: t => 30 + t * 120,
|
|
42
43
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<system-reminder>
|
|
2
2
|
Task delegation is enabled — subagents are the default for this request.
|
|
3
3
|
|
|
4
|
-
Explore and settle the approach FIRST. Once the design is settled, you MUST fan the work out to `{{toolRefs.task}}` subagents instead of implementing it yourself.{{#if taskBatch}} Batch independent slices into ONE parallel `{{toolRefs.task}}` call; never serialize work that can run concurrently.{{/if}}
|
|
4
|
+
Explore and settle the approach FIRST — scoping, top-level decomposition, and cross-slice contracts are YOUR job; NEVER spawn a subagent to produce the overall plan (per-slice design travels with its executor). Once the design is settled, you MUST fan the work out to `{{toolRefs.task}}` subagents instead of implementing it yourself.{{#if taskBatch}} Batch independent slices into ONE parallel `{{toolRefs.task}}` call; never serialize work that can run concurrently.{{/if}}
|
|
5
5
|
|
|
6
|
-
Work alone
|
|
6
|
+
Work alone for: a single-file edit under ~30 lines, a direct answer requiring no code changes, a command the user explicitly asked you to run, or when only ONE runnable slice exists — a lone subagent is a lossy handoff, not parallelism.
|
|
7
7
|
</system-reminder>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<system-notice cause="fork">
|
|
2
|
+
The conversation above belongs to your parent session.
|
|
3
|
+
You are a fork created solely to handle the user's request below.
|
|
4
|
+
|
|
5
|
+
Your parent agent is still working on the original task — that responsibility is
|
|
6
|
+
NOT yours. You have no obligations from the prior conversation.
|
|
7
|
+
|
|
8
|
+
- Focus EXCLUSIVELY on the user's immediate request. Nothing else.
|
|
9
|
+
- NEVER continue, follow up on, or intervene in anything discussed before this
|
|
10
|
+
message. Those belong to the parent session.
|
|
11
|
+
- Your parent is CONCURRENTLY editing this same working directory. Files may
|
|
12
|
+
change between your reads, look mid-refactor, or fail to compile. That is the
|
|
13
|
+
parent's live work — NEVER fix, audit, or build on it, even if it looks broken.
|
|
14
|
+
- Any todo list, plan, or unfinished checklist from the prior conversation is
|
|
15
|
+
the parent's. NEVER resume or update it.
|
|
16
|
+
- After addressing the user's request, STOP. Do not work on ANY OTHER TASK.
|
|
17
|
+
</system-notice>
|
|
@@ -5,7 +5,7 @@ Drives real Chromium tab; full puppeteer access via JS.
|
|
|
5
5
|
- Three actions:
|
|
6
6
|
- `open` — acquire/reuse named tab (`name` defaults `"main"`). Optional `url` (navigate once ready), `viewport`, `dialogs: "accept" | "dismiss"` (auto-handle `alert`/`confirm`/`beforeunload`; else page hangs till you wire `page.on('dialog', …)`).
|
|
7
7
|
- `close` — release tab by `name`, or all with `all: true`. `kill: true` also kills spawned-app process trees.
|
|
8
|
-
- `run` — execute JS in existing tab. `code` = async function body; `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. Return value JSON-stringified into result; `display(value)` accumulates text/images.
|
|
8
|
+
- `run` — execute JS in existing tab. `code` = async function body; `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. Return value JSON-stringified into result; `display(value)` accumulates text/images. `wait(ms)` sleeps; `wait(fn, { timeout?, interval? })` polls `fn` (sync or async) until truthy and resolves with that value (default 30s timeout, 100ms interval; named error on timeout) — use it instead of in-page polling Promises inside `tab.evaluate`.
|
|
9
9
|
- Tabs survive `run` calls and in-process subagents — open once, reuse.
|
|
10
10
|
- Browser kinds (`app` on `open`):
|
|
11
11
|
- default (no `app`) → headless Chromium with stealth patches.
|
|
@@ -17,9 +17,9 @@ Drives real Chromium tab; full puppeteer access via JS.
|
|
|
17
17
|
- `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot: `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Ids stable until next observe/goto.
|
|
18
18
|
- `tab.ariaSnapshot(selector?, { depth?, boxes? })` — Playwright-format ARIA-tree YAML (nested roles + accessible names + `/url`/`/placeholder`), scoped to `selector` or the whole document. Every node carries a `[ref=eN]` id; `[cursor=pointer]` flags clickables. Captures dense, hierarchical structure/text that `observe()`'s flat list flattens away. Refs renumber from e1 each call and stay valid until the next `ariaSnapshot()`.
|
|
19
19
|
- `tab.ref("e5")` — `[ref=eN]` from the last ariaSnapshot → element handle with the common action methods (`.click()`, `.type()`, `.fill()`, `.hover()`, `.evaluate()`, …); the primary way to act on a ref. For convenience `aria-ref=e5` also works inline in `tab.click`/`type`/`fill`/`waitFor`/`scrollIntoView` (e.g. `tab.click("aria-ref=e5")`).
|
|
20
|
-
- `tab.id(n)` — id from last observe →
|
|
20
|
+
- `tab.id(n)` — id from last observe → element handle with the same action methods (`.click()`, `.type()`, `.fill()`, …).
|
|
21
21
|
- `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)`.
|
|
22
|
-
- `tab.waitFor(selector, { timeout? })` / `tab.waitForSelector(selector, { timeout?, visible?, hidden? })` — wait until attached (optionally visible/hidden); returns
|
|
22
|
+
- `tab.waitFor(selector, { timeout? })` / `tab.waitForSelector(selector, { timeout?, visible?, hidden? })` — wait until attached (optionally visible/hidden); returns an action-method handle.
|
|
23
23
|
- `tab.drag(from, to)` — endpoints: selector (center-to-center) or `{ x, y }` viewport point (canvases, sliders).
|
|
24
24
|
- `tab.scrollIntoView(selector)` — center in viewport; before clicking off-screen elements.
|
|
25
25
|
- `tab.select(selector, …values)` — set `<select>` option(s); returns selection. `tab.fill` NEVER works for selects.
|
|
@@ -27,10 +27,10 @@ Drives real Chromium tab; full puppeteer access via JS.
|
|
|
27
27
|
- `tab.waitForUrl(pattern, { timeout? })` — substring or `RegExp` (matches SPA pushState nav); returns matched URL.
|
|
28
28
|
- `tab.waitForResponse(pattern, { timeout? })` — substring, `RegExp`, or `(response) => boolean`; returns puppeteer `HTTPResponse` (`.text()`/`.json()`/`.status()`/`.headers()`).
|
|
29
29
|
- `tab.waitForNavigation({ waitUntil?, timeout? })` — resolves on the next navigation. Start it BEFORE the click/submit that triggers it; after `tab.goto` (which already waits) use `tab.waitForUrl`/`tab.waitForSelector` instead.
|
|
30
|
-
- `tab.evaluate(fn, …args)` —
|
|
30
|
+
- `tab.evaluate(fn, …args)` — run ad-hoc code in the page's MAIN world. DOM and page-defined globals (`window.myFlag`) are visible; mutations affect the page.
|
|
31
31
|
- `tab.screenshot({ selector?, fullPage?, save?, silent? })` — capture + attach for viewing (`silent: true` skips). Pass `save` only when a later step needs the file.
|
|
32
32
|
- `tab.extract(format = "markdown")` — readable page content (`"markdown"` | `"text"`); throws when nothing readable.
|
|
33
|
-
- Selectors: CSS + puppeteer handlers `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`; also Playwright-style `p-aria/…`, `p-text/…`. Playwright-only engines/pseudos (`:has-text()`, `:visible`, …) are rejected — use `text/…` or `aria/…`. A stalled action/wait fails fast with a named `tab.<op
|
|
33
|
+
- Selectors: CSS + puppeteer handlers `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`; also Playwright-style `p-aria/…`, `p-text/…`. Playwright-only engines/pseudos (`:has-text()`, `:visible`, …) are rejected — use `text/…` or `aria/…`. A stalled action/wait fails fast with a named `tab.<op>` error carrying a match-count diagnosis, never the whole-cell timeout; a selector matching nothing fails in ~2s (pass an explicit `{ timeout }` to `waitFor`/`waitForSelector` to wait out slow-appearing elements).
|
|
34
34
|
</instruction>
|
|
35
35
|
|
|
36
36
|
<critical>
|
package/src/sdk.ts
CHANGED
|
@@ -847,6 +847,7 @@ function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
|
|
|
847
847
|
isIdle: ctx.isIdle,
|
|
848
848
|
hasQueuedMessages: ctx.hasPendingMessages,
|
|
849
849
|
abort: ctx.abort,
|
|
850
|
+
localProtocolOptions: ctx.localProtocolOptions,
|
|
850
851
|
};
|
|
851
852
|
}
|
|
852
853
|
|
|
@@ -2053,6 +2054,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2053
2054
|
modelRegistry,
|
|
2054
2055
|
() => (hasSession ? createSessionMemoryRuntimeContext(session, agentDir, cwd) : undefined),
|
|
2055
2056
|
settings,
|
|
2057
|
+
localProtocolOptions,
|
|
2056
2058
|
);
|
|
2057
2059
|
|
|
2058
2060
|
credentialDisabledTarget = extensionRunner;
|
|
@@ -2071,6 +2073,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2071
2073
|
session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
2072
2074
|
},
|
|
2073
2075
|
settings,
|
|
2076
|
+
localProtocolOptions,
|
|
2074
2077
|
autoApprove: options.autoApprove ?? false,
|
|
2075
2078
|
});
|
|
2076
2079
|
const toolContextStore = new ToolContextStore(getSessionContext);
|
|
@@ -173,7 +173,7 @@ import {
|
|
|
173
173
|
resolveModelOverride,
|
|
174
174
|
resolveModelRoleValue,
|
|
175
175
|
} from "../config/model-resolver";
|
|
176
|
-
import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
176
|
+
import { getKnownRoleIds, MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
177
177
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
178
178
|
import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
|
|
179
179
|
import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
|
|
@@ -387,6 +387,64 @@ const MID_RUN_TODO_NUDGE_MUTATING_TOOLS: Record<string, true> = {
|
|
|
387
387
|
write: true,
|
|
388
388
|
ast_edit: true,
|
|
389
389
|
};
|
|
390
|
+
const MARKDOWN_PROMPT_PREFIX_RE = /^(?:>\s*)?(?:(?:[-*+]|\d+[.)])\s+)*/;
|
|
391
|
+
const PROMPT_LABEL_RE = /^(?:q(?:uestion)?|ask)\s*\d*\s*[:.)-]\s*/i;
|
|
392
|
+
const QUESTION_PROMPT_RE =
|
|
393
|
+
/^(?:what|which|when|where|why|how|who|whom|whose|do|does|did|can|could|would|will|should|is|are|am|may|shall)\b/i;
|
|
394
|
+
const USER_DIRECTED_PROMPT_RE = /\b(?:you|your|we|our)\b/i;
|
|
395
|
+
const USER_RESPONSE_CUE_RE =
|
|
396
|
+
/^(?:please\s+)?(?:confirm|reply|choose|pick|decide|advise)\b|^(?:please\s+)?answer\b|^(?:please\s+)?(?:let\s+me\s+know|tell\s+me)\b/i;
|
|
397
|
+
|
|
398
|
+
function assistantText(message: AssistantMessage): string {
|
|
399
|
+
return message.content
|
|
400
|
+
.filter((content): content is TextContent => content.type === "text")
|
|
401
|
+
.map(content => content.text)
|
|
402
|
+
.join("\n")
|
|
403
|
+
.trim();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
interface PromptLine {
|
|
407
|
+
text: string;
|
|
408
|
+
hadPromptLabel: boolean;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function promptLine(line: string): PromptLine {
|
|
412
|
+
const withoutMarkdownPrefix = line.trim().replace(MARKDOWN_PROMPT_PREFIX_RE, "").trim();
|
|
413
|
+
const withoutPromptLabel = withoutMarkdownPrefix.replace(PROMPT_LABEL_RE, "").trim();
|
|
414
|
+
return {
|
|
415
|
+
text: withoutPromptLabel,
|
|
416
|
+
hadPromptLabel: withoutPromptLabel !== withoutMarkdownPrefix,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isQuestionPromptLine(line: string): boolean {
|
|
421
|
+
const candidate = promptLine(line);
|
|
422
|
+
if (!/[??]\s*$/.test(candidate.text)) return false;
|
|
423
|
+
return (
|
|
424
|
+
candidate.hadPromptLabel ||
|
|
425
|
+
QUESTION_PROMPT_RE.test(candidate.text) ||
|
|
426
|
+
USER_DIRECTED_PROMPT_RE.test(candidate.text)
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function isResponseCueLine(line: string): boolean {
|
|
431
|
+
const candidate = promptLine(line)
|
|
432
|
+
.text.replace(/[.!?。!?]+$/, "")
|
|
433
|
+
.trim();
|
|
434
|
+
return USER_RESPONSE_CUE_RE.test(candidate);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function isAwaitingUserAnswer(message: AssistantMessage): boolean {
|
|
438
|
+
const text = assistantText(message);
|
|
439
|
+
if (!text) return false;
|
|
440
|
+
const tailLines = text
|
|
441
|
+
.split(/\r?\n/)
|
|
442
|
+
.map(line => line.trim())
|
|
443
|
+
.filter(Boolean)
|
|
444
|
+
.slice(-6);
|
|
445
|
+
const lastLine = tailLines.at(-1);
|
|
446
|
+
return tailLines.some(isQuestionPromptLine) || (lastLine !== undefined && isResponseCueLine(lastLine));
|
|
447
|
+
}
|
|
390
448
|
/** `customType` for the hidden mid-run todo nudge; `display: false`, so it reaches
|
|
391
449
|
* the model but never renders in the TUI or transcript. */
|
|
392
450
|
const MID_RUN_TODO_NUDGE_MESSAGE_TYPE = "mid-run-todo-nudge";
|
|
@@ -522,6 +580,17 @@ const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
|
|
|
522
580
|
const REFUSAL_BACKOFF_BASE_MS = 2_000;
|
|
523
581
|
const REFUSAL_BACKOFF_MAX_MS = 30_000;
|
|
524
582
|
const REFUSAL_BACKOFF_BUDGET_MS = 30 * 60_000;
|
|
583
|
+
/**
|
|
584
|
+
* Consecutive-use cap on the `reasoning_extraction` fast-degrade path (strip
|
|
585
|
+
* unsigned thinking, resend same-model, zero delay — see
|
|
586
|
+
* `#reasoningExtractionDegradeStreak` below). Bounds the worst case where a
|
|
587
|
+
* pathological classifier state (documented: 1,164 refusals over ~4 hours in
|
|
588
|
+
* one incident) refuses every turn's fresh thinking in a row — after this
|
|
589
|
+
* many back-to-back fast-degrades with no intervening success, fall through
|
|
590
|
+
* to the Rung-4 ladder above, which already wall-clock-bounds via
|
|
591
|
+
* {@link REFUSAL_BACKOFF_BUDGET_MS}.
|
|
592
|
+
*/
|
|
593
|
+
const REASONING_EXTRACTION_DEGRADE_STREAK_CAP = 3;
|
|
525
594
|
|
|
526
595
|
function positiveEnvMs(name: string): number | undefined {
|
|
527
596
|
const raw = process.env[name];
|
|
@@ -1611,13 +1680,31 @@ export class AgentSession {
|
|
|
1611
1680
|
/** Rung-4 classifier-refusal resend streak (post fallback-ladder). */
|
|
1612
1681
|
#refusalBackoff: { startedAt: number; attempt: number } | undefined = undefined;
|
|
1613
1682
|
/**
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1616
|
-
* predictably re-refuses
|
|
1617
|
-
*
|
|
1618
|
-
*
|
|
1619
|
-
|
|
1620
|
-
|
|
1683
|
+
* `reasoning_extraction` classifier refusals are payload-shape-bound (see
|
|
1684
|
+
* advisor/runtime.ts `#includeThinking` degrade): resending the IDENTICAL
|
|
1685
|
+
* batch predictably re-refuses, so a bare retry can't help. But this is a
|
|
1686
|
+
* point-in-time strip of the CURRENT active context, not a persistent
|
|
1687
|
+
* setting — unlike the advisor's permanent-for-the-session
|
|
1688
|
+
* `includeThinking` flag, it does not prevent NEW unsigned thinking from a
|
|
1689
|
+
* LATER turn (a fresh advisor exchange, another interrupt, plain
|
|
1690
|
+
* continued conversation) from independently triggering its own,
|
|
1691
|
+
* genuinely-different refusal. A one-shot latch here previously left every
|
|
1692
|
+
* occurrence after the first to fall through to the generic Rung-4
|
|
1693
|
+
* backoff ladder unstripped — resending the same unstripped payload until
|
|
1694
|
+
* the wall-clock budget expired and the turn failed visibly.
|
|
1695
|
+
*
|
|
1696
|
+
* Fixed by gating on whether {@link #stripUnsignedThinkingForRefusalDegrade}
|
|
1697
|
+
* actually found something to strip THIS time (it already returns
|
|
1698
|
+
* `undefined` when there is nothing new to remove, which is itself proof
|
|
1699
|
+
* the resend would be payload-identical and degrading again cannot help)
|
|
1700
|
+
* rather than a permanent flag — each distinct occurrence gets its own
|
|
1701
|
+
* degrade attempt. Bounded by {@link REASONING_EXTRACTION_DEGRADE_STREAK_CAP}
|
|
1702
|
+
* consecutive uses (reset on any turn that actually succeeds) so a
|
|
1703
|
+
* pathological classifier state that refuses every turn's fresh thinking
|
|
1704
|
+
* in a row still falls through to the wall-clock-bounded ladder instead
|
|
1705
|
+
* of resending at zero delay indefinitely.
|
|
1706
|
+
*/
|
|
1707
|
+
#reasoningExtractionDegradeStreak = 0;
|
|
1621
1708
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
1622
1709
|
#retryResolve: (() => void) | undefined = undefined;
|
|
1623
1710
|
#activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
|
|
@@ -2386,7 +2473,7 @@ export class AgentSession {
|
|
|
2386
2473
|
if (this.#advisorSharedInstructions) systemPrompt.push(this.#advisorSharedInstructions);
|
|
2387
2474
|
if (config.instructions?.trim()) systemPrompt.push(config.instructions.trim());
|
|
2388
2475
|
|
|
2389
|
-
const names = config.tools
|
|
2476
|
+
const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
|
|
2390
2477
|
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
2391
2478
|
|
|
2392
2479
|
const advisorSessionId = this.#advisorSessionId(slug);
|
|
@@ -3674,6 +3761,7 @@ export class AgentSession {
|
|
|
3674
3761
|
});
|
|
3675
3762
|
this.#retryAttempt = 0;
|
|
3676
3763
|
this.#refusalBackoff = undefined;
|
|
3764
|
+
this.#reasoningExtractionDegradeStreak = 0;
|
|
3677
3765
|
}
|
|
3678
3766
|
if (assistantMsg.provider === "opencode-go") {
|
|
3679
3767
|
this.#modelRegistry.authStorage.recordUsageCost(assistantMsg.provider, assistantMsg.usage.cost.total, {
|
|
@@ -3916,6 +4004,17 @@ export class AgentSession {
|
|
|
3916
4004
|
await emitAgentEndNotification();
|
|
3917
4005
|
return;
|
|
3918
4006
|
}
|
|
4007
|
+
} else if (this.#isHardErrorFallbackEligible(msg)) {
|
|
4008
|
+
// A non-retryable hard error on a model covered by a configured
|
|
4009
|
+
// fallback chain: retrying the SAME model is pointless, but a
|
|
4010
|
+
// DIFFERENT model is a fresh chance — consult the chain before
|
|
4011
|
+
// surfacing the failure. #handleRetryableError bails out (no
|
|
4012
|
+
// backoff-retry of the failing model) when no switch happens.
|
|
4013
|
+
const didRetry = await this.#handleRetryableError(msg, { hardErrorFallback: true });
|
|
4014
|
+
if (didRetry) {
|
|
4015
|
+
await emitAgentEndNotification();
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
3919
4018
|
}
|
|
3920
4019
|
// Classifier refusals are persisted-skipped above; also prune the trailing
|
|
3921
4020
|
// stub from active context so the next turn's prompt does not replay it.
|
|
@@ -3960,7 +4059,7 @@ export class AgentSession {
|
|
|
3960
4059
|
await emitAgentEndNotification();
|
|
3961
4060
|
return;
|
|
3962
4061
|
}
|
|
3963
|
-
const todoContinuationScheduled = await this.#checkTodoCompletion();
|
|
4062
|
+
const todoContinuationScheduled = await this.#checkTodoCompletion(msg);
|
|
3964
4063
|
if (todoContinuationScheduled) {
|
|
3965
4064
|
await emitAgentEndNotification();
|
|
3966
4065
|
return;
|
|
@@ -6488,6 +6587,8 @@ export class AgentSession {
|
|
|
6488
6587
|
abort: () => {
|
|
6489
6588
|
this.agent.abort();
|
|
6490
6589
|
},
|
|
6590
|
+
settings: this.settings,
|
|
6591
|
+
localProtocolOptions: this.#localProtocolOptions(),
|
|
6491
6592
|
});
|
|
6492
6593
|
|
|
6493
6594
|
for (const customTool of mcpTools) {
|
|
@@ -6973,6 +7074,30 @@ export class AgentSession {
|
|
|
6973
7074
|
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model);
|
|
6974
7075
|
}
|
|
6975
7076
|
|
|
7077
|
+
/**
|
|
7078
|
+
* Resolve the explicit thinking suffix that should apply when a temporary
|
|
7079
|
+
* picker selects a model already assigned to a configured role.
|
|
7080
|
+
*/
|
|
7081
|
+
resolveTemporaryModelThinkingLevel(model: Model): ConfiguredThinkingLevel | undefined {
|
|
7082
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
7083
|
+
if (availableModels.length === 0) return undefined;
|
|
7084
|
+
|
|
7085
|
+
const matchPreferences = getModelMatchPreferences(this.settings);
|
|
7086
|
+
for (const role of getKnownRoleIds(this.settings)) {
|
|
7087
|
+
const roleValue = this.settings.getModelRole(role);
|
|
7088
|
+
if (!roleValue) continue;
|
|
7089
|
+
|
|
7090
|
+
const resolved = resolveModelRoleValue(roleValue, availableModels, {
|
|
7091
|
+
settings: this.settings,
|
|
7092
|
+
matchPreferences,
|
|
7093
|
+
});
|
|
7094
|
+
if (!resolved.explicitThinkingLevel || resolved.thinkingLevel === undefined || !resolved.model) continue;
|
|
7095
|
+
if (modelsAreEqual(resolved.model, model)) return resolved.thinkingLevel;
|
|
7096
|
+
}
|
|
7097
|
+
|
|
7098
|
+
return undefined;
|
|
7099
|
+
}
|
|
7100
|
+
|
|
6976
7101
|
get promptTemplates(): ReadonlyArray<PromptTemplate> {
|
|
6977
7102
|
return this.#promptTemplates;
|
|
6978
7103
|
}
|
|
@@ -9233,6 +9358,11 @@ export class AgentSession {
|
|
|
9233
9358
|
* candidate is small or the session has been idle long enough that the
|
|
9234
9359
|
* provider prompt cache is cold), so it is cheap to run every turn. Gated
|
|
9235
9360
|
* on the `compaction.supersedeReads` and `compaction.dropUseless` settings.
|
|
9361
|
+
*
|
|
9362
|
+
* Persists via `rewriteEntries` like every other history rewrite — the
|
|
9363
|
+
* session file must match the live (pruned) context or file-based forks
|
|
9364
|
+
* (`/fork`, `/tan`) and resume rebuild a divergent prefix and cold-miss the
|
|
9365
|
+
* provider prompt cache.
|
|
9236
9366
|
*/
|
|
9237
9367
|
async #pruneStaleToolResults(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
9238
9368
|
const { supersedeReads, dropUseless } = this.settings.getGroup("compaction");
|
|
@@ -9255,6 +9385,7 @@ export class AgentSession {
|
|
|
9255
9385
|
return undefined;
|
|
9256
9386
|
}
|
|
9257
9387
|
|
|
9388
|
+
await this.sessionManager.rewriteEntries();
|
|
9258
9389
|
const sessionContext = this.buildDisplaySessionContext();
|
|
9259
9390
|
this.agent.replaceMessages(sessionContext.messages);
|
|
9260
9391
|
this.#resetAllAdvisorRuntimes();
|
|
@@ -10353,21 +10484,21 @@ export class AgentSession {
|
|
|
10353
10484
|
|
|
10354
10485
|
this.#emptyStopRetryCount++;
|
|
10355
10486
|
if (this.#emptyStopRetryCount > EMPTY_STOP_MAX_RETRIES) {
|
|
10356
|
-
|
|
10357
|
-
|
|
10487
|
+
const attempts = this.#emptyStopRetryCount - 1;
|
|
10488
|
+
const finalError = "Assistant returned empty stop after retry cap";
|
|
10489
|
+
logger.warn(finalError, {
|
|
10490
|
+
attempts,
|
|
10358
10491
|
model: assistantMessage.model,
|
|
10359
10492
|
provider: assistantMessage.provider,
|
|
10360
10493
|
});
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
this.#refusalBackoff = undefined;
|
|
10370
|
-
}
|
|
10494
|
+
await this.#emitSessionEvent({
|
|
10495
|
+
type: "auto_retry_end",
|
|
10496
|
+
success: false,
|
|
10497
|
+
attempt: this.#retryAttempt > 0 ? this.#retryAttempt : attempts,
|
|
10498
|
+
finalError,
|
|
10499
|
+
});
|
|
10500
|
+
this.#retryAttempt = 0;
|
|
10501
|
+
this.#refusalBackoff = undefined;
|
|
10371
10502
|
this.#resolveRetry();
|
|
10372
10503
|
// Tool-use orphans corrupt Anthropic message history (tool_result without
|
|
10373
10504
|
// matching tool_use). Always remove them even when the retry cap is hit.
|
|
@@ -10861,7 +10992,7 @@ export class AgentSession {
|
|
|
10861
10992
|
/**
|
|
10862
10993
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
10863
10994
|
*/
|
|
10864
|
-
async #checkTodoCompletion(): Promise<boolean> {
|
|
10995
|
+
async #checkTodoCompletion(message: AssistantMessage): Promise<boolean> {
|
|
10865
10996
|
// Skip todo reminders when the most recent turn was driven by an explicit user force —
|
|
10866
10997
|
// the user wanted exactly that tool, not a follow-up nag about incomplete todos.
|
|
10867
10998
|
const lastServedLabel = this.#toolChoiceQueue.consumeLastServedLabel();
|
|
@@ -10919,6 +11050,13 @@ export class AgentSession {
|
|
|
10919
11050
|
return false;
|
|
10920
11051
|
}
|
|
10921
11052
|
|
|
11053
|
+
if (isAwaitingUserAnswer(message)) {
|
|
11054
|
+
logger.debug("Todo completion: assistant is waiting for user input; skipping reminder", {
|
|
11055
|
+
incomplete: incomplete.length,
|
|
11056
|
+
});
|
|
11057
|
+
return false;
|
|
11058
|
+
}
|
|
11059
|
+
|
|
10922
11060
|
// Build reminder message
|
|
10923
11061
|
this.#todoReminderCount++;
|
|
10924
11062
|
const todoList = incompleteByPhase
|
|
@@ -13048,6 +13186,33 @@ export class AgentSession {
|
|
|
13048
13186
|
if (AIError.is(id, AIError.Flag.AuthFailed)) return false;
|
|
13049
13187
|
return this.#modelRegistry.find("fireworks", toFireworksBaseModelId(model.id)) !== undefined;
|
|
13050
13188
|
}
|
|
13189
|
+
/**
|
|
13190
|
+
* True when a turn failed with a hard (non-retryable) provider error but a
|
|
13191
|
+
* configured `retry.fallbackChains` entry covers the active model: the same
|
|
13192
|
+
* model is not worth retrying, yet a DIFFERENT model is a fresh chance, so
|
|
13193
|
+
* the chain is consulted before the error becomes final. Skips failures a
|
|
13194
|
+
* model switch cannot fix or must not replay: cancellations (abort-flavored
|
|
13195
|
+
* errors are not model faults), context overflow (compaction's job),
|
|
13196
|
+
* classifier refusals (chain consult is handled on the retryable path with
|
|
13197
|
+
* `pinFallback`), and turns that already emitted a tool call (replaying
|
|
13198
|
+
* could duplicate work).
|
|
13199
|
+
*/
|
|
13200
|
+
#isHardErrorFallbackEligible(message: AssistantMessage): boolean {
|
|
13201
|
+
if (message.stopReason !== "error") return false;
|
|
13202
|
+
const model = this.model;
|
|
13203
|
+
if (!model) return false;
|
|
13204
|
+
const retrySettings = this.settings.getGroup("retry");
|
|
13205
|
+
if (!retrySettings.enabled || !retrySettings.modelFallback) return false;
|
|
13206
|
+
if (this.#isClassifierRefusal(message)) return false;
|
|
13207
|
+
const id = this.#classifyRetryMessage(message);
|
|
13208
|
+
if (AIError.is(id, AIError.Flag.Abort) || AIError.is(id, AIError.Flag.UserInterrupt)) return false;
|
|
13209
|
+
if (AIError.isContextOverflow(message, model.contextWindow ?? 0)) return false;
|
|
13210
|
+
if (this.#hasReplayUnsafeToolOutput(message)) return false;
|
|
13211
|
+
const currentSelector = formatRetryFallbackSelector(model, this.thinkingLevel);
|
|
13212
|
+
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
13213
|
+
if (!role) return false;
|
|
13214
|
+
return this.#findRetryFallbackCandidates(role, currentSelector).length > 0;
|
|
13215
|
+
}
|
|
13051
13216
|
|
|
13052
13217
|
/**
|
|
13053
13218
|
* Switch the active model from a Fireworks Fast (`-fast`) variant to its base
|
|
@@ -13178,12 +13343,16 @@ export class AgentSession {
|
|
|
13178
13343
|
}
|
|
13179
13344
|
|
|
13180
13345
|
/**
|
|
13181
|
-
* Handle retryable errors with exponential backoff
|
|
13346
|
+
* Handle retryable errors with exponential backoff, credential rotation, and
|
|
13347
|
+
* model-fallback chains. Also entered for NON-retryable errors when a switch
|
|
13348
|
+
* is the recovery (`fireworksFastFallback`, `hardErrorFallback`): then a
|
|
13349
|
+
* successful model switch retries immediately, and a failed switch surfaces
|
|
13350
|
+
* the error without a same-model backoff retry.
|
|
13182
13351
|
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
13183
13352
|
*/
|
|
13184
13353
|
async #handleRetryableError(
|
|
13185
13354
|
message: AssistantMessage,
|
|
13186
|
-
options?: { allowModelFallback?: boolean; fireworksFastFallback?: boolean },
|
|
13355
|
+
options?: { allowModelFallback?: boolean; fireworksFastFallback?: boolean; hardErrorFallback?: boolean },
|
|
13187
13356
|
): Promise<boolean> {
|
|
13188
13357
|
const retrySettings = this.settings.getGroup("retry");
|
|
13189
13358
|
// The Fireworks Fast→base degrade is an intrinsic model-selection safety net,
|
|
@@ -13224,29 +13393,39 @@ export class AgentSession {
|
|
|
13224
13393
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
13225
13394
|
// `reasoning_extraction` is payload-shape-bound (see advisor/runtime.ts): resending
|
|
13226
13395
|
// the identical batch predictably re-refuses, unlike probabilistic classifier flags
|
|
13227
|
-
// where backoff/fallback genuinely help.
|
|
13228
|
-
//
|
|
13229
|
-
//
|
|
13230
|
-
//
|
|
13396
|
+
// where backoff/fallback genuinely help. Strip unsigned thinking/redactedThinking
|
|
13397
|
+
// from active context and resend immediately on the same model — before falling
|
|
13398
|
+
// through to the generic fallback-pin / backoff ladder below, which would otherwise
|
|
13399
|
+
// retry the unchanged, re-refusing payload. Gated on the strip itself finding
|
|
13400
|
+
// something to remove THIS time (not a one-shot flag): a later turn can
|
|
13401
|
+
// independently accumulate fresh unsigned thinking and trigger its own genuinely
|
|
13402
|
+
// different refusal, and `#stripUnsignedThinkingForRefusalDegrade` already reports
|
|
13403
|
+
// "nothing to strip" via `undefined` when the payload truly is unchanged — which is
|
|
13404
|
+
// itself the correct signal to fall through instead of degrading again. Bounded by
|
|
13405
|
+
// `REASONING_EXTRACTION_DEGRADE_STREAK_CAP` consecutive uses so a pathological
|
|
13406
|
+
// classifier state refusing every turn's fresh thinking in a row still reaches the
|
|
13407
|
+
// wall-clock-bounded ladder instead of resending at zero delay indefinitely.
|
|
13231
13408
|
if (
|
|
13232
13409
|
classifierRefusal &&
|
|
13233
13410
|
message.stopDetails?.category === "reasoning_extraction" &&
|
|
13234
|
-
|
|
13411
|
+
this.#reasoningExtractionDegradeStreak < REASONING_EXTRACTION_DEGRADE_STREAK_CAP
|
|
13235
13412
|
) {
|
|
13236
|
-
this.#reasoningExtractionDegraded = true;
|
|
13237
13413
|
const stripped = this.#stripUnsignedThinkingForRefusalDegrade(this.agent.state.messages);
|
|
13238
|
-
if (stripped)
|
|
13239
|
-
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
|
|
13243
|
-
|
|
13244
|
-
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
13414
|
+
if (stripped) {
|
|
13415
|
+
this.#reasoningExtractionDegradeStreak++;
|
|
13416
|
+
this.agent.replaceMessages(stripped);
|
|
13417
|
+
this.#removeAssistantMessageFromActiveContext(message, "reasoning-extraction-degrade");
|
|
13418
|
+
await this.#emitSessionEvent({
|
|
13419
|
+
type: "auto_retry_start",
|
|
13420
|
+
attempt: this.#retryAttempt,
|
|
13421
|
+
maxAttempts: retrySettings.maxRetries,
|
|
13422
|
+
delayMs: 0,
|
|
13423
|
+
errorMessage,
|
|
13424
|
+
errorId: message.errorId,
|
|
13425
|
+
});
|
|
13426
|
+
this.#scheduleAgentContinue({ delayMs: 1, generation });
|
|
13427
|
+
return true;
|
|
13428
|
+
}
|
|
13250
13429
|
}
|
|
13251
13430
|
|
|
13252
13431
|
const id = this.#classifyRetryMessage(message);
|
|
@@ -13359,11 +13538,16 @@ export class AgentSession {
|
|
|
13359
13538
|
this.#retryAttempt = Math.min(this.#retryAttempt, 1);
|
|
13360
13539
|
delayMs = refusalBackoffDelayMs(backoff.attempt);
|
|
13361
13540
|
}
|
|
13362
|
-
//
|
|
13363
|
-
//
|
|
13364
|
-
//
|
|
13365
|
-
// classifier wouldn't retry —
|
|
13366
|
-
|
|
13541
|
+
// A fallback switch was the whole reason we entered (Fast→base degrade or
|
|
13542
|
+
// a hard-error chain consult) but it could not happen (e.g. no candidate
|
|
13543
|
+
// has a credential). Don't fall through to backing-off and retrying the
|
|
13544
|
+
// failing model for an error the generic classifier wouldn't retry —
|
|
13545
|
+
// surface it instead.
|
|
13546
|
+
if (
|
|
13547
|
+
(options?.fireworksFastFallback || options?.hardErrorFallback) &&
|
|
13548
|
+
!switchedModel &&
|
|
13549
|
+
!this.#isRetryableError(message)
|
|
13550
|
+
) {
|
|
13367
13551
|
this.#retryAttempt = 0;
|
|
13368
13552
|
this.#resolveRetry();
|
|
13369
13553
|
return false;
|
|
@@ -14062,10 +14246,13 @@ export class AgentSession {
|
|
|
14062
14246
|
// Side-channel turns must not share OpenAI/Codex append-only
|
|
14063
14247
|
// conversation state with the main agent turn: IRC and /btw can run
|
|
14064
14248
|
// while the main turn is mid-tool-call. Keep the prompt-cache key
|
|
14065
|
-
// stable, but give provider routing a unique request lineage.
|
|
14249
|
+
// stable, but give provider routing a unique request lineage. The
|
|
14250
|
+
// shared provider state map is still required so Codex can allocate
|
|
14251
|
+
// websocket state under that side-channel session id.
|
|
14066
14252
|
sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
|
|
14067
14253
|
promptCacheKey: cacheSessionId,
|
|
14068
|
-
preferWebsockets:
|
|
14254
|
+
preferWebsockets: this.#preferWebsockets,
|
|
14255
|
+
providerSessionState: this.#providerSessionState,
|
|
14069
14256
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
14070
14257
|
disableReasoning: shouldDisableReasoning(this.thinkingLevel),
|
|
14071
14258
|
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
* Precedence (highest first):
|
|
10
10
|
* 1. `JEOPI_AUTH_BROKER_URL` / `JEOPI_AUTH_BROKER_TOKEN` env vars.
|
|
11
11
|
* 2. `auth.broker.url` / `auth.broker.token` in `~/.jeopi/agent/config.yml`
|
|
12
|
-
* (hidden from the settings UI; `!command` resolution supported).
|
|
12
|
+
* or `~/.jeopi/agent/config.yaml` (hidden from the settings UI; `!command` resolution supported).
|
|
13
13
|
* 3. Token file `~/.jeopi/auth-broker.token` (paired with URL from env or config).
|
|
14
14
|
*
|
|
15
15
|
* Returns null when no broker URL is configured — caller falls back to the
|
|
16
16
|
* local SQLite store.
|
|
17
17
|
*
|
|
18
|
-
* Reads config
|
|
18
|
+
* Reads config YAML directly (instead of going through `Settings.init`) because
|
|
19
19
|
* `discoverAuthStorage` runs before the settings singleton is initialized in
|
|
20
20
|
* `runRootCommand`, and we want hand-edited config entries to be honoured at
|
|
21
21
|
* boot without forcing a startup reorder.
|
|
@@ -36,10 +36,10 @@ export { type AuthBrokerClientConfig, getAuthBrokerTokenFilePath };
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Process-lifetime memo for {@link resolveAuthBrokerConfig}. Keyed on the env
|
|
39
|
-
* inputs (plus agent dir, which decides which config
|
|
39
|
+
* inputs (plus agent dir, which decides which config YAML is read) so tests
|
|
40
40
|
* that flip `JEOPI_AUTH_BROKER_*` between cases still observe the change, while
|
|
41
41
|
* repeated resolution within one CLI invocation (startup, subagent sessions)
|
|
42
|
-
* skips the config
|
|
42
|
+
* skips the config YAML read and any `!command` token resolution.
|
|
43
43
|
*/
|
|
44
44
|
let cachedConfigKey: string | null = null;
|
|
45
45
|
let cachedConfigPromise: Promise<AuthBrokerClientConfig | null> | null = null;
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
import { countTokens } from "jeopi-agent-core";
|
|
18
18
|
import type { Context, ImageContent, Model, TextContent, ToolResultMessage, UserMessage } from "jeopi-ai";
|
|
19
|
-
import { isPersonalGitHubCopilotBaseUrl } from "jeopi-catalog/wire/github-copilot";
|
|
20
19
|
import * as snapcompact from "jeopi-snapcompact";
|
|
21
20
|
import contextFramesNote from "../prompts/system/snapcompact-context-frames-note.md" with { type: "text" };
|
|
22
21
|
import contextStub from "../prompts/system/snapcompact-context-stub.md" with { type: "text" };
|
|
@@ -78,19 +77,6 @@ function passesSavingsGate(frames: number, shape: snapcompact.Shape, textTokens:
|
|
|
78
77
|
return frames * shape.frameTokenEstimate <= textTokens * SAVINGS_MARGIN;
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
/**
|
|
82
|
-
* The model is vision-capable for the endpoint we're actually about to hit.
|
|
83
|
-
* GitHub Copilot business and enterprise hosts respond `400 vision is not
|
|
84
|
-
* supported` on image inputs (issue #3387), so even if a stale cached spec
|
|
85
|
-
* still advertises `["text","image"]` we MUST not rasterize transcripts when
|
|
86
|
-
* the resolved `baseUrl` is non-personal.
|
|
87
|
-
*/
|
|
88
|
-
function canSendImages(model: Model): boolean {
|
|
89
|
-
if (!model.input.includes("image")) return false;
|
|
90
|
-
if (model.provider === "github-copilot" && !isPersonalGitHubCopilotBaseUrl(model.baseUrl)) return false;
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
80
|
interface SystemPromptImageTarget {
|
|
95
81
|
scope: Exclude<SnapcompactSystemPromptMode, "none">;
|
|
96
82
|
text: string;
|
|
@@ -291,7 +277,7 @@ export function estimateInlineSavings(input: {
|
|
|
291
277
|
messages: readonly InlineMessageView[];
|
|
292
278
|
}): SnapcompactSavingsEstimate {
|
|
293
279
|
const { options, model } = input;
|
|
294
|
-
if (!model
|
|
280
|
+
if (!model?.input.includes("image")) {
|
|
295
281
|
return { visionCapable: false, savedTokens: 0 };
|
|
296
282
|
}
|
|
297
283
|
|
|
@@ -430,10 +416,8 @@ export class SnapcompactInlineTransformer {
|
|
|
430
416
|
|
|
431
417
|
async transform(context: Context, model: Model): Promise<Context> {
|
|
432
418
|
// Vision gate: providers silently DROP images on text-only models —
|
|
433
|
-
// rendering would lose the content entirely.
|
|
434
|
-
|
|
435
|
-
// (issue #3387: Copilot business endpoint).
|
|
436
|
-
if (!canSendImages(model)) return context;
|
|
419
|
+
// rendering would lose the content entirely.
|
|
420
|
+
if (!model.input.includes("image")) return context;
|
|
437
421
|
|
|
438
422
|
const shape = snapcompact.resolveShape(model, this.options.shape);
|
|
439
423
|
const budget = snapcompact.providerImageBudget(model.provider) - countContextImages(context);
|