zidane 6.2.36 → 6.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/dist/{acp-DZNCQaTQ.js → acp-Cs86McDR.js} +6 -6
- package/dist/{acp-DZNCQaTQ.js.map → acp-Cs86McDR.js.map} +1 -1
- package/dist/acp-cli.js +7 -7
- package/dist/acp.d.ts +2 -2
- package/dist/acp.js +1 -1
- package/dist/{agent-CrPwqpLI.js → agent-AZ_crMfB.js} +168 -23
- package/dist/agent-AZ_crMfB.js.map +1 -0
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +4 -4
- package/dist/{anthropic-asnZVFS_.js → anthropic-CWwySM3p.js} +59 -24
- package/dist/anthropic-CWwySM3p.js.map +1 -0
- package/dist/{auth-C5YTniaq.js → auth-BPSnotuN.js} +5 -5
- package/dist/auth-BPSnotuN.js.map +1 -0
- package/dist/{cache-telemetry-BhTryxFT.js → cache-telemetry-DzyfqSds.js} +87 -5
- package/dist/cache-telemetry-DzyfqSds.js.map +1 -0
- package/dist/chat/pure.d.ts +3 -3
- package/dist/chat.d.ts +43 -5
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +31 -3
- package/dist/chat.js.map +1 -1
- package/dist/context-transport-64EeXTAQ.js +84 -0
- package/dist/context-transport-64EeXTAQ.js.map +1 -0
- package/dist/contexts/daytona.d.ts +33 -4
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +136 -0
- package/dist/contexts/daytona.js.map +1 -1
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +85 -2
- package/dist/contexts/docker.js.map +1 -1
- package/dist/contexts/e2b.d.ts +34 -4
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +92 -0
- package/dist/contexts/e2b.js.map +1 -1
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +4 -2
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-o8y5B3Tm.js → contexts-DbMuUPjS.js} +92 -5
- package/dist/contexts-DbMuUPjS.js.map +1 -0
- package/dist/contexts.d.ts +4 -4
- package/dist/contexts.js +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +3 -3
- package/dist/{exec-file-size-PXkr-MGA.js → exec-file-size-BRpoB6Io.js} +3 -32
- package/dist/exec-file-size-BRpoB6Io.js.map +1 -0
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +4 -4
- package/dist/{index-DsPQbyUn.d.ts → index-B9jSo84a.d.ts} +2 -2
- package/dist/index-B9jSo84a.d.ts.map +1 -0
- package/dist/{index-CohO0oaX.d.ts → index-D-PvuMCb.d.ts} +5607 -5188
- package/dist/index-D-PvuMCb.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -13
- package/dist/{login-CWwu9Sex.js → login-fgr9zD6z.js} +2 -2
- package/dist/{login-CWwu9Sex.js.map → login-fgr9zD6z.js.map} +1 -1
- package/dist/{mcp-Ct5tF4oj.js → mcp-DolMi7WV.js} +77 -34
- package/dist/mcp-DolMi7WV.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/{messages-Cpb4g_KV.js → messages-CLkUdPeG.js} +2 -2
- package/dist/{messages-Cpb4g_KV.js.map → messages-CLkUdPeG.js.map} +1 -1
- package/dist/{oauth-CBi9eymX.js → oauth-CuFPhsHY.js} +117 -11
- package/dist/oauth-CuFPhsHY.js.map +1 -0
- package/dist/{openai-compat-C77gOaHh.js → openai-compat-DHlBIjj9.js} +186 -42
- package/dist/openai-compat-DHlBIjj9.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/prompt.d.ts +1 -1
- package/dist/prompt.js +14 -0
- package/dist/prompt.js.map +1 -1
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +2 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +5 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/{providers-DnHPW-dL.js → providers-Nu5ysnXN.js} +4 -4
- package/dist/providers-Nu5ysnXN.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +5 -5
- package/dist/{read-state-rDmfeLmz.js → read-state-CNyeMG7W.js} +7 -4
- package/dist/read-state-CNyeMG7W.js.map +1 -0
- package/dist/restate.d.ts +2 -2
- package/dist/restate.js +2 -2
- package/dist/restate.js.map +1 -1
- package/dist/{sandbox-CgjG2VvQ.d.ts → sandbox-DqwpKX18.d.ts} +14 -2
- package/dist/{sandbox-CgjG2VvQ.d.ts.map → sandbox-DqwpKX18.d.ts.map} +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/{session-DpDBnktP.js → session-vmy9jTVM.js} +2 -2
- package/dist/{session-DpDBnktP.js.map → session-vmy9jTVM.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/shell-quote-BmnhZmdM.js +33 -0
- package/dist/shell-quote-BmnhZmdM.js.map +1 -0
- package/dist/skills.d.ts +1 -1
- package/dist/{stdio-loader-Ce68wUmM.js → stdio-D0v-W5w7.js} +8 -215
- package/dist/stdio-D0v-W5w7.js.map +1 -0
- package/dist/stdio-loader-BPUedHxT.js +217 -0
- package/dist/stdio-loader-BPUedHxT.js.map +1 -0
- package/dist/stream-queue-Cd9OJxl7.js +71 -0
- package/dist/stream-queue-Cd9OJxl7.js.map +1 -0
- package/dist/{system-prompt-BlsdjcUi.d.ts → system-prompt-MNVHatSD.d.ts} +15 -1
- package/dist/system-prompt-MNVHatSD.d.ts.map +1 -0
- package/dist/{tool-formatters-bZYz74qU.d.ts → tool-formatters-BPKA2if5.d.ts} +2 -2
- package/dist/{tool-formatters-bZYz74qU.d.ts.map → tool-formatters-BPKA2if5.d.ts.map} +1 -1
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +2 -2
- package/dist/{transcript-anchors-ZZYwCTjo.js → transcript-anchors-6-uB2Xvl.js} +19 -8
- package/dist/transcript-anchors-6-uB2Xvl.js.map +1 -0
- package/dist/{transcript-anchors-EBchHUMo.d.ts → transcript-anchors-DzuIcJpm.d.ts} +2 -2
- package/dist/{transcript-anchors-EBchHUMo.d.ts.map → transcript-anchors-DzuIcJpm.d.ts.map} +1 -1
- package/dist/tui.d.ts +3 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +124 -45
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-DygcPreU.d.ts → turn-operations-Dzr3QhDD.d.ts} +3 -3
- package/dist/{turn-operations-DygcPreU.d.ts.map → turn-operations-Dzr3QhDD.d.ts.map} +1 -1
- package/dist/types-BIarq1qE.js.map +1 -1
- package/dist/{types-BDccavwj.d.ts → types-DdZMQxfT.d.ts} +117 -2
- package/dist/types-DdZMQxfT.d.ts.map +1 -0
- package/dist/types.d.ts +3 -3
- package/dist/{xai-B2hIs5We.js → xai-Cn0LsHRt.js} +3 -3
- package/dist/xai-Cn0LsHRt.js.map +1 -0
- package/docs/ARCHITECTURE.md +62 -4
- package/docs/CHAT.md +12 -5
- package/docs/EXTENSIONS.md +1 -1
- package/docs/RESTATE.md +1 -1
- package/docs/SKILL.md +21 -1
- package/docs/TUI.md +2 -0
- package/package.json +2 -2
- package/dist/agent-CrPwqpLI.js.map +0 -1
- package/dist/anthropic-asnZVFS_.js.map +0 -1
- package/dist/auth-C5YTniaq.js.map +0 -1
- package/dist/cache-telemetry-BhTryxFT.js.map +0 -1
- package/dist/contexts-o8y5B3Tm.js.map +0 -1
- package/dist/exec-file-size-PXkr-MGA.js.map +0 -1
- package/dist/index-CohO0oaX.d.ts.map +0 -1
- package/dist/index-DsPQbyUn.d.ts.map +0 -1
- package/dist/mcp-Ct5tF4oj.js.map +0 -1
- package/dist/oauth-CBi9eymX.js.map +0 -1
- package/dist/openai-compat-C77gOaHh.js.map +0 -1
- package/dist/providers-DnHPW-dL.js.map +0 -1
- package/dist/read-state-rDmfeLmz.js.map +0 -1
- package/dist/stdio-loader-Ce68wUmM.js.map +0 -1
- package/dist/system-prompt-BlsdjcUi.d.ts.map +0 -1
- package/dist/transcript-anchors-ZZYwCTjo.js.map +0 -1
- package/dist/types-BDccavwj.d.ts.map +0 -1
- package/dist/xai-B2hIs5We.js.map +0 -1
|
@@ -34,6 +34,17 @@ interface ContextCapabilities {
|
|
|
34
34
|
network: boolean;
|
|
35
35
|
/** Has GPU access */
|
|
36
36
|
gpu: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Can open a bidirectional byte stream to a long-lived process through
|
|
39
|
+
* {@link ExecutionContext.execStream}. Absent / `false` = the context
|
|
40
|
+
* only supports the buffered {@link ExecutionContext.exec} and (maybe)
|
|
41
|
+
* the write-to-a-log-file {@link ExecutionContext.execBackground}.
|
|
42
|
+
*
|
|
43
|
+
* Consumers that NEED live stdin (stdio MCP servers routed into the
|
|
44
|
+
* context) must check this before calling; the seam is optional so
|
|
45
|
+
* contexts backed by a request/response-only API stay valid.
|
|
46
|
+
*/
|
|
47
|
+
stream?: boolean;
|
|
37
48
|
/**
|
|
38
49
|
* Background-task lifetime guarantee. Optional for backward
|
|
39
50
|
* compatibility — when absent, callers infer `'process-lifetime'`
|
|
@@ -329,6 +340,56 @@ interface ExecutionContext {
|
|
|
329
340
|
stallTimeoutMs?: number; /** One-shot stall callback — see `stallTimeoutMs`. */
|
|
330
341
|
onStall?: (info: TaskStallInfo) => void;
|
|
331
342
|
}) => Promise<TaskHandle>;
|
|
343
|
+
/**
|
|
344
|
+
* Open a LIVE bidirectional byte stream to a long-lived process in the
|
|
345
|
+
* context. The third execution mode, alongside the buffered `exec`
|
|
346
|
+
* (request/response) and `execBackground` (fire-and-forget into a log
|
|
347
|
+
* file): neither of those can carry a protocol that interleaves reads
|
|
348
|
+
* and writes over the child's lifetime.
|
|
349
|
+
*
|
|
350
|
+
* Motivating consumer: stdio MCP servers. An MCP server is an ordinary
|
|
351
|
+
* process doing `fs.readFileSync` / `fs.writeFile` against whatever
|
|
352
|
+
* filesystem it was spawned on. Spawned on the host (the default — see
|
|
353
|
+
* `McpServerConfig.scope`) it sees the HOST filesystem while the agent's
|
|
354
|
+
* own tools see the context's, so the two disagree about what a path
|
|
355
|
+
* means. Routing the server through this seam puts both on the same
|
|
356
|
+
* filesystem.
|
|
357
|
+
*
|
|
358
|
+
* Optional — a context backed by a request/response-only API can't
|
|
359
|
+
* implement it. Advertise support via `capabilities.stream`; callers
|
|
360
|
+
* check that (or `execStream !== undefined`) and degrade with a clear
|
|
361
|
+
* error rather than silently falling back to a host spawn, which would
|
|
362
|
+
* reintroduce exactly the split this seam exists to close.
|
|
363
|
+
*
|
|
364
|
+
* IMPLEMENTOR CONTRACT — the shape is deliberately the SUPERSET of what
|
|
365
|
+
* the backing APIs can do, because they do not agree:
|
|
366
|
+
*
|
|
367
|
+
* - `stdout` / `stderr` are byte streams, never pre-decoded strings.
|
|
368
|
+
* A provider SDK that hands back `string` has already destroyed any
|
|
369
|
+
* multibyte codepoint split across a chunk boundary; the caller
|
|
370
|
+
* needs the raw bytes to run its own streaming decoder.
|
|
371
|
+
* - `stderr` is OPTIONAL because a PTY-backed context physically
|
|
372
|
+
* cannot separate it — one terminal, one stream. Such contexts
|
|
373
|
+
* redirect the child's stderr to a file; they may then surface it
|
|
374
|
+
* here out-of-band (the Daytona context polls that file back), but
|
|
375
|
+
* they are not obliged to, so consumers must tolerate its absence.
|
|
376
|
+
* - A PTY-backed context MUST also neutralise terminal semantics
|
|
377
|
+
* before the child's first byte (echo of everything written to
|
|
378
|
+
* stdin, `\n`→`\r\n` rewriting, and the ~4096-byte canonical-mode
|
|
379
|
+
* line cap that arbitrary protocol frames exceed). In practice:
|
|
380
|
+
* launch through `stty raw -echo; exec <cmd> 2><file>`.
|
|
381
|
+
* - `exited` resolves with the exit code. On a PTY the child is an
|
|
382
|
+
* interactive shell that never exits on its own, so `kill()` is
|
|
383
|
+
* the normal termination path, not an abnormal one.
|
|
384
|
+
* - Providers that impose a default command timeout (E2B and Daytona
|
|
385
|
+
* both default to 60s) MUST disable or maximise it here. A stream
|
|
386
|
+
* opened by this method is expected to live for the whole session.
|
|
387
|
+
*/
|
|
388
|
+
execStream?: (handle: ExecutionHandle, command: string, options?: {
|
|
389
|
+
cwd?: string;
|
|
390
|
+
env?: Record<string, string>; /** Aborts startup and kills the child once running. */
|
|
391
|
+
signal?: AbortSignal;
|
|
392
|
+
}) => Promise<StreamHandle>;
|
|
332
393
|
/**
|
|
333
394
|
* SIGTERM the whole process group of a running background task.
|
|
334
395
|
* Idempotent — second call returns `null` (or the cached exit info).
|
|
@@ -498,6 +559,60 @@ interface TaskHandle {
|
|
|
498
559
|
/** Absolute path to the log file the context is streaming output into. */
|
|
499
560
|
outputPath: string;
|
|
500
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* A live bidirectional stream to a process in the context. Returned by
|
|
564
|
+
* {@link ExecutionContext.execStream}; see that method for the full
|
|
565
|
+
* implementor contract (why the streams are bytes, why `stderr` is
|
|
566
|
+
* optional, what PTY-backed contexts must do before the first byte).
|
|
567
|
+
*
|
|
568
|
+
* Ownership: the caller owns the lifetime. Nothing reaps this handle
|
|
569
|
+
* automatically, so `kill()` belongs in the consumer's teardown path —
|
|
570
|
+
* for MCP that is the transport's `close()`, reached via the agent's
|
|
571
|
+
* `mcpConnection.close()` during `destroy()`.
|
|
572
|
+
*/
|
|
573
|
+
interface StreamHandle {
|
|
574
|
+
/** Process id inside the context — NOT meaningful on the host. */
|
|
575
|
+
pid: number;
|
|
576
|
+
/**
|
|
577
|
+
* Child's stdout as raw byte chunks, in emit order.
|
|
578
|
+
*
|
|
579
|
+
* CONSUMER OBLIGATION: drain this (and {@link stderr}) for the stream's
|
|
580
|
+
* lifetime. Nothing here is bounded, and the consequence of ignoring it
|
|
581
|
+
* varies by backend and by host runtime, none of it good:
|
|
582
|
+
*
|
|
583
|
+
* - `process` / `docker` hand back real OS pipes. Under Node an unread
|
|
584
|
+
* stream stops draining the fd and the child BLOCKS once the pipe
|
|
585
|
+
* fills (~64 KiB) — a deadlock, not a slowdown. Under Bun the runtime
|
|
586
|
+
* buffers eagerly instead, so it presents as unbounded host memory
|
|
587
|
+
* (measured: 32 MiB of unread stderr never blocked the child).
|
|
588
|
+
* - `e2b` / `daytona` buffer in a `StreamQueue`, which grows without
|
|
589
|
+
* limit. It cannot do otherwise: both SDKs push output through a
|
|
590
|
+
* synchronous `(data) => void` callback, so there is no channel to
|
|
591
|
+
* apply backpressure, and dropping bytes would desynchronise a framed
|
|
592
|
+
* protocol permanently. The queue logs a one-shot warning once the
|
|
593
|
+
* unread backlog passes 8 MiB so a stalled consumer is diagnosable.
|
|
594
|
+
*
|
|
595
|
+
* `createContextStdioTransport` satisfies this for both streams.
|
|
596
|
+
*/
|
|
597
|
+
stdout: AsyncIterable<Uint8Array>;
|
|
598
|
+
/**
|
|
599
|
+
* Child's stderr, when the implementation can provide it separately.
|
|
600
|
+
* Present on pipe-backed contexts. A PTY-backed context cannot split it
|
|
601
|
+
* from the terminal stream, so it redirects stderr to a file and either
|
|
602
|
+
* surfaces it here out-of-band or leaves this unset — expect a lower
|
|
603
|
+
* latency guarantee there than on stdout.
|
|
604
|
+
*/
|
|
605
|
+
stderr?: AsyncIterable<Uint8Array>;
|
|
606
|
+
/** Write to the child's stdin. Resolves once the bytes are handed off. */
|
|
607
|
+
write: (data: Uint8Array) => Promise<void>;
|
|
608
|
+
/** Terminate the child. Idempotent — killing a dead child is a no-op. */
|
|
609
|
+
kill: () => Promise<void>;
|
|
610
|
+
/**
|
|
611
|
+
* Resolves with the exit code when the child terminates, including
|
|
612
|
+
* termination via {@link kill}. Never rejects.
|
|
613
|
+
*/
|
|
614
|
+
exited: Promise<number>;
|
|
615
|
+
}
|
|
501
616
|
/**
|
|
502
617
|
* Fired exactly once per task when the child process terminates, via
|
|
503
618
|
* `execBackground`'s `onExit` callback. The agent layer translates this
|
|
@@ -559,5 +674,5 @@ interface TaskStallInfo {
|
|
|
559
674
|
bytesWritten: number;
|
|
560
675
|
}
|
|
561
676
|
//#endregion
|
|
562
|
-
export {
|
|
563
|
-
//# sourceMappingURL=types-
|
|
677
|
+
export { TaskHandle as _, ContextType as a, ExecutionContext as c, ListFilesEntry as d, ListFilesOptions as f, TaskExitInfo as g, TaskEntry as h, ContextMount as i, ExecutionHandle as l, StreamHandle as m, ContextCapabilities as n, DetachedTasksCapability as o, SpawnConfig as p, ContextHardening as r, ExecResult as s, BackgroundTaskStatus as t, ListFileMetadata as u, TaskStallInfo as v, resolveDetachedTasksCapability as y };
|
|
678
|
+
//# sourceMappingURL=types-DdZMQxfT.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-DdZMQxfT.d.ts","names":[],"sources":["../src/contexts/types.ts"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as AgentProviderError, c as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, f as matchesContextExceeded, i as AgentContextExceededError, l as ClassifiedError, n as AgentAbortedError, o as AgentToolNotAllowedError, r as AgentBudgetExceededError, u as ClassifiedErrorKind } from "./timeout-8vSIRjzl.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { t as
|
|
2
|
+
import { _ as TaskHandle, a as ContextType, c as ExecutionContext, g as TaskExitInfo, h as TaskEntry, l as ExecutionHandle, n as ContextCapabilities, o as DetachedTasksCapability, p as SpawnConfig, r as ContextHardening, s as ExecResult, t as BackgroundTaskStatus, v as TaskStallInfo } from "./types-DdZMQxfT.js";
|
|
3
|
+
import { t as SandboxProvider } from "./sandbox-DqwpKX18.js";
|
|
4
|
+
import { $d as StreamOptions, $f as AnthropicParams, $p as PromptVideoPart, Am as TurnUsage, Ap as ChildRunStats, Bi as AgentHooks, Bp as McpServerConfig, C as SuccessfulMutationOutcome, Cm as ToolResultDocumentRefContent, Cp as AgentBehavior, Dm as ToolResultTextContent, Em as ToolResultMetadata, Ep as AgentStats, Fm as ToolDef, Fp as DEFAULT_AGENT_CLOCK, Gf as CerebrasParams, Gp as OAuthRefreshHookContext, If as OpenAIParams, Im as ToolMap, Jp as PromptAudioPart, Kp as ProgressNudgeKind, Mm as toolOutputByteLength, Mr as InteractionToolOptions, Nm as toolResultToText, Om as ToolResultVideoContent, Pm as ToolContext, Qd as StreamCallbacks, Qm as CreateSessionOptions, Qp as PromptTextPart, Ri as Agent, Rm as ReadStateEntry, S as MutationValidationResult, Sm as ToolResultDocumentContent, Tm as ToolResultImageRefContent, Tp as AgentRunOptions, Ts as SpawnToolState, Vi as AgentOptions, Vp as McpToolHookContext, Xd as Provider, Xp as PromptImagePart, Ym as SkillResource, Yp as PromptDocumentPart, Zd as ProviderCapabilities, Zm as SkillsConfig, Zp as PromptPart, _ as statsByModel, am as RunHookMap, b as MutationValidationHooksOptions, bm as ToolResultAudioContent, cm as SessionHookContext, dm as SpawnHookContext, eh as Session, em as RepeatGuardConfig, ff as OpenRouterParams, fr as ValidationResult, gm as ThinkingLevel, h as flattenTurns, if as TurnResult, ih as SessionStore, im as RetryConfig, jm as toolOutputBudgetByteLength, km as TurnFinishReason, lm as SessionMessage, mh as RemoteStoreOptions, mm as StreamHookContext, nf as ToolResult, nm as ResolveContentRef, om as SessionContentBlock, p as ModelUsage, qh as McpConnection, qm as SkillConfig, qp as ProgressNudgeMessage, rf as ToolSpec, rh as SessionRun, rm as ResolveContentRefBlock, sm as SessionEndStatus, t as Preset, tf as ToolCall, th as SessionData, tm as RepeatGuardToolMatcher, um as SessionTurn, v as MutationValidationBatch, vm as ToolHookContext, wm as ToolResultImageContent, wp as AgentClock, ws as SpawnToolOptions, x as MutationValidationMutation, xm as ToolResultContent, y as MutationValidationFailure, ym as ToolOutcome, ys as ChildAgent, zi as AgentHookMap, zm as ReadStateMap } from "./index-D-PvuMCb.js";
|
|
5
5
|
export { type Agent, AgentAbortedError, type AgentBehavior, AgentBudgetExceededError, type AgentClock, AgentContextExceededError, type AgentHookMap, type AgentHooks, type AgentOptions, AgentProviderError, type AgentRunOptions, type AgentStats, AgentToolNotAllowedError, type AnthropicParams, type BackgroundTaskStatus, CONTEXT_EXCEEDED_MESSAGE_PATTERNS, type CerebrasParams, type ChildAgent, type ChildRunStats, type ClassifiedError, type ClassifiedErrorKind, type ContextCapabilities, type ContextHardening, type ContextType, type CreateSessionOptions, DEFAULT_AGENT_CLOCK, type DetachedTasksCapability, type ExecResult, type ExecutionContext, type ExecutionHandle, type InteractionToolOptions, type McpConnection, type McpServerConfig, type McpToolHookContext, type ModelUsage, type MutationValidationBatch, type MutationValidationFailure, type MutationValidationHooksOptions, type MutationValidationMutation, type MutationValidationResult, type OAuthRefreshHookContext, type OpenAIParams, type OpenRouterParams, type Preset, type ProgressNudgeKind, type ProgressNudgeMessage, type PromptAudioPart, type PromptDocumentPart, type PromptImagePart, type PromptPart, type PromptTextPart, type PromptVideoPart, type Provider, type ProviderCapabilities, type ReadStateEntry, type ReadStateMap, type RemoteStoreOptions, type RepeatGuardConfig, type RepeatGuardToolMatcher, type ResolveContentRef, type ResolveContentRefBlock, type RetryConfig, type RunHookMap, type SandboxProvider, type Session, type SessionContentBlock, type SessionData, type SessionEndStatus, type SessionHookContext, type SessionMessage, type SessionRun, type SessionStore, type SessionTurn, type SkillConfig, type SkillResource, type SkillsConfig, type SpawnConfig, type SpawnHookContext, type SpawnToolOptions, type SpawnToolState, type StreamCallbacks, type StreamHookContext, type StreamOptions, type SuccessfulMutationOutcome, type TaskEntry, type TaskExitInfo, type TaskHandle, type TaskStallInfo, type ThinkingLevel, type ToolCall, type ToolContext, type ToolDef, type ToolHookContext, type ToolMap, type ToolOutcome, type ToolResult, type ToolResultAudioContent, type ToolResultContent, type ToolResultDocumentContent, type ToolResultDocumentRefContent, type ToolResultImageContent, type ToolResultImageRefContent, type ToolResultMetadata, type ToolResultTextContent, type ToolResultVideoContent, type ToolSpec, type TurnFinishReason, type TurnResult, type TurnUsage, type ValidationResult, flattenTurns, matchesContextExceeded, statsByModel, toolOutputBudgetByteLength, toolOutputByteLength, toolResultToText };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { m as openaiCompat } from "./openai-compat-
|
|
2
|
-
import { i as isOAuthAccessToken, r as extractRuntimeCredentials, s as resolveOAuthApiKey } from "./oauth-
|
|
1
|
+
import { m as openaiCompat } from "./openai-compat-DHlBIjj9.js";
|
|
2
|
+
import { i as isOAuthAccessToken, r as extractRuntimeCredentials, s as resolveOAuthApiKey } from "./oauth-CuFPhsHY.js";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
//#region src/chat/oauth-page/pkce.ts
|
|
5
5
|
/**
|
|
@@ -376,4 +376,4 @@ function xai(params) {
|
|
|
376
376
|
//#endregion
|
|
377
377
|
export { createXaiOAuthProvider as n, generatePkce as r, xai as t };
|
|
378
378
|
|
|
379
|
-
//# sourceMappingURL=xai-
|
|
379
|
+
//# sourceMappingURL=xai-Cn0LsHRt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xai-Cn0LsHRt.js","names":[],"sources":["../src/chat/oauth-page/pkce.ts","../src/chat/oauth-page/xai.ts","../src/providers/xai.ts"],"sourcesContent":["/**\n * PKCE helper. Inlined here (rather than imported from pi-ai) because\n * `@earendil-works/pi-ai/oauth` does not re-export it, and we don't want\n * to reach into `node_modules/.../utils/oauth/pkce.js` — that's an\n * implementation-detail path that breaks on minor upstream rearrangements.\n *\n * Web Crypto API only. Works under Bun + Node 22+ without any node:crypto\n * import (matches pi-ai's own behavior). Output is base64url per RFC 7636.\n */\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes)\n binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '')\n}\n\nexport interface PkcePair {\n /** Random verifier used in the token-exchange step. */\n verifier: string\n /** SHA-256(verifier) sent on the authorize URL. */\n challenge: string\n}\n\nexport async function generatePkce(): Promise<PkcePair> {\n const verifierBytes = new Uint8Array(32)\n crypto.getRandomValues(verifierBytes)\n const verifier = base64UrlEncode(verifierBytes)\n\n const data = new TextEncoder().encode(verifier)\n const hashBuffer = await crypto.subtle.digest('SHA-256', data)\n const challenge = base64UrlEncode(new Uint8Array(hashBuffer))\n\n return { verifier, challenge }\n}\n","/**\n * xAI Grok OAuth flow (SuperGrok / X Premium+).\n *\n * xAI is **not** built into pi-ai, so — like Cursor — we implement the flow\n * here and register it with pi-ai's OAuth registry (`registerXaiOAuthProvider`)\n * so `getOAuthApiKey('xai-oauth', …)` resolves to it during lazy token refresh.\n *\n * Auth shape: OAuth 2.0 + PKCE with a loopback callback server, against xAI's\n * OIDC issuer (`https://auth.x.ai`). Differs from the Anthropic / Codex flows\n * (which use the shared `startCallbackServer`) on three points, so it gets its\n * own login impl rather than reusing that primitive:\n *\n * 1. The `redirect_uri` must be `http://127.0.0.1:56121/callback` verbatim\n * (xAI matches the registered loopback IP, not `localhost`).\n * 2. Endpoints come from OIDC discovery, not hard-coded constants.\n * 3. Token exchange + refresh are `application/x-www-form-urlencoded`\n * (the Anthropic flow posts JSON).\n *\n * The callback page still routes through {@link OAuthCallbackPageRenderer} so\n * the post-redirect HTML matches the rest of zidane's themed flows.\n *\n * Flow + constants adapted from the public reference implementations\n * (`stnly/pi-grok`, `BlockedPath/pi-xai-oauth`) and the Hermes Agent docs.\n * xAI's OAuth surface is unofficial/undocumented — re-verify the client id,\n * scopes, and callback port if login starts failing.\n */\n\nimport type {\n OAuthCredentials,\n OAuthLoginCallbacks,\n} from '@earendil-works/pi-ai/oauth'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { OAuthProviderInterface } from '../../providers/oauth-registry'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { createServer } from 'node:http'\nimport { generatePkce } from './pkce'\n\n/** pi-ai / zidane OAuth provider id. Also the credentials-file key. */\nexport const XAI_OAUTH_PROVIDER_ID = 'xai-oauth'\n\nconst DEFAULT_BASE_URL = 'https://api.x.ai/v1'\nconst ISSUER = 'https://auth.x.ai'\nconst DISCOVERY_URL = `${ISSUER}/.well-known/openid-configuration`\n/** Public client id used by the Grok CLI OAuth surface. */\nconst DEFAULT_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828'\nconst DEFAULT_SCOPE = 'openid profile email offline_access grok-cli:access api:access'\n/** xAI matches the registered loopback IP exactly — `localhost` is rejected. */\nconst DEFAULT_CALLBACK_HOST = '127.0.0.1'\nconst DEFAULT_CALLBACK_PORT = 56121\nconst CALLBACK_PATH = '/callback'\nconst PROVIDER_NAME = 'xAI Grok'\n/** Refresh slightly early so requests don't race expiry. */\nconst REFRESH_SKEW_MS = 120_000\nconst CALLBACK_TIMEOUT_MS = 180_000\n\nfunction xaiOAuthClientId(): string {\n return process.env.PI_XAI_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID\n}\n\nfunction xaiOAuthScope(): string {\n return process.env.PI_XAI_OAUTH_SCOPE || DEFAULT_SCOPE\n}\n\nfunction xaiOAuthCallbackHost(): string {\n return process.env.PI_XAI_OAUTH_CALLBACK_HOST || DEFAULT_CALLBACK_HOST\n}\n\nfunction xaiOAuthCallbackPort(): number {\n const parsed = Number.parseInt(process.env.PI_XAI_OAUTH_CALLBACK_PORT || String(DEFAULT_CALLBACK_PORT), 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CALLBACK_PORT\n}\n\n/** Resolve the xAI API base URL (env override → default). */\nexport function xaiBaseUrl(): string {\n return (process.env.PI_XAI_BASE_URL || process.env.XAI_BASE_URL || DEFAULT_BASE_URL)\n .replace(/\\/+$/, '')\n}\n\ninterface XaiDiscovery {\n authorization_endpoint: string\n token_endpoint: string\n}\n\n/**\n * xAI OAuth credentials. Extends the base `{ access, refresh, expires }` with\n * the resolved `token_endpoint` so refresh doesn't need a second discovery\n * round-trip, plus the cached discovery doc for validation.\n */\nexport interface XaiOAuthCredentials extends OAuthCredentials {\n tokenEndpoint?: string\n discovery?: XaiDiscovery\n}\n\nfunction base64Url(bytes: Uint8Array): string {\n let binary = ''\n for (const b of bytes)\n binary += String.fromCharCode(b)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction randomToken(byteLength = 16): string {\n return base64Url(crypto.getRandomValues(new Uint8Array(byteLength)))\n}\n\n/**\n * Refuse any OIDC endpoint that isn't HTTPS on an xAI origin.\n *\n * The discovery doc is cached long-term in `credentials.json`. A single MITM\n * during initial login could otherwise pin a malicious `token_endpoint` that\n * receives every subsequent refresh token. Validating scheme + host keeps the\n * endpoint on `x.ai` / `*.x.ai`.\n */\nfunction validateEndpoint(value: string, field: string): string {\n let url: URL\n try {\n url = new URL(value)\n }\n catch {\n throw new Error(`xAI OAuth discovery returned an invalid ${field}: ${value}`)\n }\n if (url.protocol !== 'https:')\n throw new Error(`xAI OAuth ${field} must use HTTPS: ${value}`)\n const host = url.hostname.toLowerCase()\n if (host !== 'x.ai' && !host.endsWith('.x.ai'))\n throw new Error(`Refusing non-xAI OAuth ${field}: ${value}`)\n return url.toString()\n}\n\nasync function discover(): Promise<XaiDiscovery> {\n let response: Response\n try {\n response = await fetch(DISCOVERY_URL, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n })\n }\n catch (cause) {\n throw new Error(`xAI OIDC discovery failed: ${cause instanceof Error ? cause.message : String(cause)}`)\n }\n if (!response.ok)\n throw new Error(`xAI OIDC discovery returned ${response.status}`)\n\n const payload = await response.json() as Record<string, unknown>\n return {\n authorization_endpoint: validateEndpoint(String(payload.authorization_endpoint ?? ''), 'authorization_endpoint'),\n token_endpoint: validateEndpoint(String(payload.token_endpoint ?? ''), 'token_endpoint'),\n }\n}\n\ninterface CallbackResult {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\ninterface XaiCallbackServer {\n redirectUri: string\n waitForCallback: (timeoutMs: number) => Promise<CallbackResult>\n close: () => void\n}\n\n/**\n * Start the loopback callback server on `127.0.0.1:56121`. Falls back to an\n * OS-assigned port if 56121 is taken — xAI accepts any loopback port as long\n * as the `redirect_uri` we send on the authorize URL matches the listener.\n */\nasync function startXaiCallbackServer(renderPage: OAuthCallbackPageRenderer): Promise<XaiCallbackServer> {\n const callbackHost = xaiOAuthCallbackHost()\n const callbackPort = xaiOAuthCallbackPort()\n let settle: ((value: CallbackResult) => void) | undefined\n let settled = false\n const callbackPromise = new Promise<CallbackResult>((resolve) => {\n settle = (value) => {\n if (settled)\n return\n settled = true\n resolve(value)\n }\n })\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n try {\n // accounts.x.ai may issue a CORS preflight against the loopback listener.\n const origin = req.headers.origin\n if (origin === 'https://accounts.x.ai' || origin === 'https://auth.x.ai') {\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type')\n res.setHeader('Access-Control-Allow-Private-Network', 'true')\n res.setHeader('Vary', 'Origin')\n }\n if (req.method === 'OPTIONS') {\n res.statusCode = 204\n res.end()\n return\n }\n\n const url = new URL(req.url ?? '/', `http://${callbackHost}`)\n if (url.pathname !== CALLBACK_PATH) {\n res.statusCode = 404\n res.end('Not found')\n return\n }\n\n const result: CallbackResult = {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n error: url.searchParams.get('error') ?? undefined,\n errorDescription: url.searchParams.get('error_description') ?? undefined,\n }\n\n res.statusCode = result.error ? 400 : 200\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(renderPage(result.error\n ? {\n kind: 'error',\n provider: PROVIDER_NAME,\n message: `${PROVIDER_NAME} authentication did not complete.`,\n details: result.errorDescription ?? result.error,\n }\n : {\n kind: 'success',\n provider: PROVIDER_NAME,\n message: 'xAI authentication completed. You can close this window.',\n }))\n settle?.(result)\n }\n catch {\n res.statusCode = 500\n res.end('Internal error')\n }\n })\n\n const listen = (port: number) => new Promise<number>((resolve, reject) => {\n server.once('error', reject)\n server.listen(port, callbackHost, () => {\n server.removeListener('error', reject)\n const addr = server.address()\n resolve(typeof addr === 'object' && addr ? addr.port : port)\n })\n })\n\n let actualPort: number\n try {\n actualPort = await listen(callbackPort)\n }\n catch {\n actualPort = await listen(0)\n }\n\n return {\n redirectUri: `http://${callbackHost}:${actualPort}${CALLBACK_PATH}`,\n waitForCallback: timeoutMs => Promise.race([\n callbackPromise,\n new Promise<CallbackResult>(resolve =>\n setTimeout(\n resolve,\n timeoutMs,\n { error: 'timeout', errorDescription: 'Timed out waiting for the xAI OAuth callback.' },\n )),\n ]),\n close: () => {\n try { server.close() }\n catch { /* already closed */ }\n },\n }\n}\n\nfunction expiryFromPayload(payload: Record<string, unknown>): number {\n const expiresIn = typeof payload.expires_in === 'number'\n ? payload.expires_in\n : Number(payload.expires_in ?? 3600)\n return Date.now() + expiresIn * 1000 - REFRESH_SKEW_MS\n}\n\nasync function exchangeCode(\n tokenEndpoint: string,\n code: string,\n redirectUri: string,\n verifier: string,\n discovery: XaiDiscovery,\n): Promise<XaiOAuthCredentials> {\n const clientId = xaiOAuthClientId()\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: clientId,\n code,\n redirect_uri: redirectUri,\n code_verifier: verifier,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token exchange failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n const refresh = String(payload.refresh_token ?? '')\n if (!access)\n throw new Error('xAI token exchange did not return an access_token.')\n if (!refresh)\n throw new Error('xAI token exchange did not return a refresh_token.')\n\n return { access, refresh, expires: expiryFromPayload(payload), tokenEndpoint, discovery }\n}\n\nexport async function loginXai(\n renderPage: OAuthCallbackPageRenderer,\n callbacks: OAuthLoginCallbacks,\n): Promise<XaiOAuthCredentials> {\n const discovery = await discover()\n const { verifier, challenge } = await generatePkce()\n const state = randomToken()\n const nonce = randomToken()\n const server = await startXaiCallbackServer(renderPage)\n\n try {\n const authUrl = new URL(discovery.authorization_endpoint)\n authUrl.searchParams.set('response_type', 'code')\n authUrl.searchParams.set('client_id', xaiOAuthClientId())\n authUrl.searchParams.set('redirect_uri', server.redirectUri)\n authUrl.searchParams.set('scope', xaiOAuthScope())\n authUrl.searchParams.set('code_challenge', challenge)\n authUrl.searchParams.set('code_challenge_method', 'S256')\n authUrl.searchParams.set('state', state)\n authUrl.searchParams.set('nonce', nonce)\n // Opt into xAI's generic OAuth plan tier (SuperGrok / X Premium+).\n authUrl.searchParams.set('plan', 'generic')\n authUrl.searchParams.set('referrer', 'zidane')\n\n callbacks.onAuth({\n url: authUrl.toString(),\n instructions: `Sign in to xAI and approve access. If the browser is on another machine, the callback listener is ${server.redirectUri}.`,\n })\n\n const result = await server.waitForCallback(CALLBACK_TIMEOUT_MS)\n if (result.error)\n throw new Error(result.errorDescription ?? result.error)\n if (result.state !== state)\n throw new Error('xAI OAuth state mismatch — possible CSRF. Please retry.')\n if (!result.code)\n throw new Error('xAI OAuth callback did not include an authorization code.')\n\n callbacks.onProgress?.('Exchanging authorization code for tokens...')\n return await exchangeCode(discovery.token_endpoint, result.code, server.redirectUri, verifier, discovery)\n }\n finally {\n server.close()\n }\n}\n\nexport async function refreshXaiToken(credentials: OAuthCredentials): Promise<XaiOAuthCredentials> {\n const xai = credentials as XaiOAuthCredentials\n const clientId = xaiOAuthClientId()\n if (!credentials.refresh)\n throw new Error('xAI credentials are missing a refresh token — re-login required.')\n\n const tokenEndpoint = xai.tokenEndpoint\n ?? xai.discovery?.token_endpoint\n ?? (await discover()).token_endpoint\n validateEndpoint(tokenEndpoint, 'token_endpoint')\n\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: clientId,\n refresh_token: credentials.refresh,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token refresh failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n if (!access)\n throw new Error('xAI token refresh did not return an access_token.')\n\n return {\n ...xai,\n access,\n // xAI may omit a rotated refresh token — keep the existing one.\n refresh: String(payload.refresh_token ?? credentials.refresh),\n expires: expiryFromPayload(payload),\n tokenEndpoint,\n }\n}\n\n/**\n * Build an xAI `OAuthProviderInterface`. Shape matches Anthropic / Codex /\n * Cursor so it drops into `BUILTIN_PROVIDERS` and `src/auth.ts` unchanged.\n *\n * `usesCallbackServer: true` — the loopback flow has a real callback server,\n * so the TUI doesn't need to prompt for a manual code paste.\n */\nexport function createXaiOAuthProvider(renderPage: OAuthCallbackPageRenderer): OAuthProviderInterface {\n return {\n id: XAI_OAUTH_PROVIDER_ID,\n name: 'xAI Grok (SuperGrok / X Premium+)',\n usesCallbackServer: true,\n login: callbacks => loginXai(renderPage, callbacks),\n refreshToken: refreshXaiToken,\n getApiKey: credentials => credentials.access,\n }\n}\n","import type { Provider, ProviderCapabilities, StreamCallbacks, StreamOptions, TurnResult } from '.'\nimport type { OAuthParams } from './oauth'\nimport { XAI_OAUTH_PROVIDER_ID, xaiBaseUrl } from '../chat/oauth-page/xai'\nimport { extractRuntimeCredentials, isOAuthAccessToken, resolveOAuthApiKey } from './oauth'\nimport { openaiCompat } from './openai-compat'\n\nconst DEFAULT_MODEL = 'grok-4.3'\n\n/**\n * xAI Grok provider — supports both a static API key (`XAI_API_KEY`) and the\n * SuperGrok / X Premium+ OAuth subscription.\n *\n * Both auth modes hit the same OpenAI-compatible endpoint (`https://api.x.ai/v1`),\n * so the wire handling is entirely {@link openaiCompat}. The only difference is\n * the bearer:\n *\n * - **API key**: `XAI_API_KEY` (or `params.apiKey`) → used verbatim.\n * - **OAuth**: no static key → {@link resolveOAuthApiKey} resolves + lazily\n * refreshes the stored `xai-oauth` token on every turn.\n *\n * `resolveOAuthApiKey` unifies the two: a real API key in `XAI_API_KEY` is\n * returned as-is, while an OAuth access token (or no env value at all) falls\n * through to the refreshable stored credentials. So this single factory serves\n * both paths without branching at construction time.\n */\nexport interface XaiParams extends OAuthParams {\n defaultModel?: string\n /**\n * Provider capability flags. Grok models are vision-capable; default\n * `{ vision: true, imageInToolResult: false }`. Override for text-only\n * deployments.\n */\n capabilities?: ProviderCapabilities\n /** Extra HTTP headers sent on every request. */\n extraHeaders?: Record<string, string>\n}\n\nexport function xai(params?: XaiParams): Provider {\n const defaultModel = params?.defaultModel || DEFAULT_MODEL\n const baseURL = xaiBaseUrl()\n const capabilities: ProviderCapabilities = params?.capabilities ?? {\n vision: true,\n imageInToolResult: false,\n }\n const baseCredentials = extractRuntimeCredentials(params)\n let runtimeCredentials = baseCredentials\n\n // Factory-time snapshot for the static `meta.isOAuth` display flag only (the\n // terminal header's `oauth`/`key` badge). The real bearer resolves per turn\n // in `stream`, so this is best-effort, mirroring anthropic's pattern. It's\n // OAuth unless a genuine static API key is present: a `xai-…` key → key mode;\n // runtime OAuth creds, a JWT access token, or no static key (stored creds\n // used) → oauth mode.\n const staticKey = params?.apiKey ?? process.env.XAI_API_KEY\n const isOAuth = baseCredentials !== undefined || !staticKey || isOAuthAccessToken(staticKey)\n\n /**\n * Build the openai-compat delegate for a resolved bearer. Cheap — the factory\n * only wires closures — so we re-create it per turn with the freshly resolved\n * (possibly refreshed) OAuth token rather than baking a stale key in once.\n */\n function delegateFor(apiKey: string): Provider {\n return openaiCompat({\n name: 'xai',\n apiKey,\n baseURL,\n defaultModel,\n capabilities,\n extraHeaders: params?.extraHeaders,\n })\n }\n\n // A delegate built with a placeholder key supplies the non-stream methods\n // (formatTools, userMessage, message builders, classifyError). Only `stream`\n // resolves the real bearer, so the placeholder never reaches the wire.\n const skeleton = delegateFor('placeholder-resolved-at-stream')\n\n return {\n ...skeleton,\n name: 'xai',\n // Reuse the delegate's normalized capability flags (openaiCompat fills the\n // audio/video/documents defaults) so the advertised shape matches the wire.\n meta: { ...skeleton.meta, defaultModel, isOAuth },\n\n async stream(options: StreamOptions, callbacks: StreamCallbacks): Promise<TurnResult> {\n const apiKey = await resolveOAuthApiKey(\n {\n provider: 'xai',\n providerId: XAI_OAUTH_PROVIDER_ID,\n params: runtimeCredentials ? { ...params, ...runtimeCredentials } : params,\n envKey: 'XAI_API_KEY',\n missingError: 'No xAI credentials found. Set XAI_API_KEY or run `bun run auth --xai` first.',\n refreshError: reason => `xAI OAuth token refresh failed. Run \\`bun run auth --xai\\` again. ${reason}`,\n },\n {\n ...callbacks,\n async onOAuthRefresh(ctx) {\n // Keep param-sourced credentials current so a long-lived provider\n // instance reuses the rotated token instead of re-refreshing.\n if (ctx.source === 'params') {\n runtimeCredentials = {\n access: ctx.credentials.access,\n refresh: ctx.credentials.refresh,\n expires: ctx.credentials.expires,\n }\n }\n await callbacks.onOAuthRefresh?.(ctx)\n },\n },\n )\n\n return delegateFor(apiKey).stream(options, callbacks)\n },\n }\n}\n"],"mappings":"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"}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -352,12 +352,41 @@ The built-in profiles (`buildBuildSystem` / `buildPlanSystem` in `zidane/chat`)
|
|
|
352
352
|
**Provider integration:**
|
|
353
353
|
|
|
354
354
|
- `anthropic` — `applyAnthropicCacheBreakpoints(params, originalSystem)` splits string system prompts on the marker. The OAuth path skips the split (the canonical CC prompt occupies `params.system`; the user doctrine lives in an injected user message).
|
|
355
|
-
- `openaiCompat` — `toOAIMessages` strips the marker; `applyOAICacheBreakpoints(messages, originalSystem)` splits the leading system message into static + dynamic content parts when caching is on.
|
|
355
|
+
- `openaiCompat` — `toOAIMessages` strips the marker; `applyOAICacheBreakpoints(messages, originalSystem, ttl)` splits the leading system message into static + dynamic content parts when caching is on, stamping `cacheTtl` on each breakpoint (forwarded as-is; OpenRouter's Anthropic-family routes honor it without a beta header — measured).
|
|
356
356
|
- `openai` (Codex), `cerebras`, `arcee`, `local`, and any cache-disabled route — marker is collapsed via `renderSystemForWire` so it never reaches the model.
|
|
357
357
|
|
|
358
|
+
### Cache-stable configuration
|
|
359
|
+
|
|
360
|
+
A prefix cache breaks at the first differing byte: the provider hashes tools → system → messages cumulatively, so a change at position N invalidates every breakpoint at or after N. Everything in this section follows from one principle — **the wire must be append-only** — and from provider facts that were measured live rather than assumed (the measurements are pinned in `test-inference/cache-stability.inference.test.ts`, so a provider-side change surfaces as a test failure, not a bill).
|
|
361
|
+
|
|
362
|
+
Measured facts (Anthropic, Sonnet, ~25k-token seeded transcript):
|
|
363
|
+
|
|
364
|
+
| Change between turns | Cache effect |
|
|
365
|
+
|---|---|
|
|
366
|
+
| Nothing (append-only wire) | ~99.9% hit rate; `cacheRead` grows with the transcript |
|
|
367
|
+
| One interpolated counter in the system prompt | Partial break every turn: `cacheRead` pinned to the scaffold constant, conversation re-bills — ~12% hit rate |
|
|
368
|
+
| A `read_file` result rewritten in place (`elideStaleReads: 'stub'`) | Partial break at the read's position; bounded to one per read→edit transition (stubbing is idempotent) |
|
|
369
|
+
| `thinking.budget_tokens` changed | **Total** loss: `cacheRead: 0`, scaffold included — the budget is part of the cache key |
|
|
370
|
+
| >5 min idle | Entry expired; full re-write on the next turn regardless of wire stability |
|
|
371
|
+
|
|
372
|
+
The knobs, and the cache-aware setting for each:
|
|
373
|
+
|
|
374
|
+
| Knob | Cache-aware setting | Why |
|
|
375
|
+
|---|---|---|
|
|
376
|
+
| `elideStaleReads` | `'notice'` | Append-only: a persisted `<system-reminder>` naming the stale path rides the mutating tool's own result. Zero prefix rewrites. `'stub'` (= legacy `true`) reclaims the stale bytes from context instead, at one full-prefix re-bill per read→edit transition — only worth it when context-window pressure outweighs cache cost (break-even ≈ `12.5 × prefix / staleBytes` turns). |
|
|
377
|
+
| `thinkingDecay` | Step form `{ afterTurn, floor }` | Every distinct budget a schedule emits is one total cache loss. Step emits one change per run; the geometric form (`factor`) emits one per halving; a per-turn function disables caching entirely for the rest of the run. |
|
|
378
|
+
| `cacheTtl` | `'1h'` for interactive hosts | The 5-minute TTL is the dominant cache cost for a human-paced session — one long pause forfeits the whole prefix. `'1h'` (2× write cost, ~0.1× reads) converts idle-loss into a cheap read. Honored by the native `anthropic` provider (ttl + beta header) and forwarded on `openrouter` routes (no beta needed there). Headless back-to-back runs should stay on `'5m'`. Caveat: the wire shape is byte-verified, but our 2026-07 live measurement saw a 1h entry evicted within 6 minutes — run the gated idle test (`INFERENCE_SLOW=1`) against your own credentials before counting the savings. |
|
|
379
|
+
| `compactStrategy` / `mediaKeepTurns` | Keep windows equal, chunk-aligned | The two frontier-quantized passes (tail compaction, media age-out) snap to `COMPACT_CHUNK_TURNS` so a window costs one invalidation, not two. Stale-read elision is deliberately NOT quantized — measured worse (it is already idempotent per read). |
|
|
380
|
+
| `thinking` level | Constant for the whole run | Toggling thinking on/off mid-run forfeits the prefix even with an unchanged transcript — the thinking config, like the budget, is part of the cache key. Pick a level per run and keep it. |
|
|
381
|
+
| `system:transform` handlers | Byte-stable output only | Anything per-turn (counters, timestamps, tallies) belongs in the transcript as immutable history, not in the system prompt — see the hook's docstring for the incident this rule comes from. |
|
|
382
|
+
|
|
383
|
+
The built-in chat profiles (`BUILD_AGENT` / `PLAN_AGENT`, consumed by the TUI, GUI, and ACP server) ship the cache-aware settings; `test/chat-cache-behavior.test.ts` pins them. What remains outside configuration's reach: TTL expiry beyond `'1h'`, context-window-forced compaction (one intentional break when it happens), provider fallback switching models mid-run, and provider-side eviction. The realistic target is **zero self-inflicted breaks** — one cold write on turn 1, plus whatever the wall clock and the context window force.
|
|
384
|
+
|
|
385
|
+
The end-to-end proof lives in the live suite's *golden configuration* case: a real model driving the real file tools through the loop under the settings above, asserted on **perfect prefix carry-forward** — every turn's `cacheRead` equals the previous turn's `cacheRead + cacheCreation`, i.e. writes only ever cover new transcript. Carry-forward is the metric to reach for over hit *rate*: a short run with large tool outputs can carry its prefix perfectly while scoring a mediocre ratio, because the ratio measures transcript proportions, not cache health.
|
|
386
|
+
|
|
358
387
|
### Cache-break diagnosis
|
|
359
388
|
|
|
360
|
-
`installCacheBreakLogger(hooks, options?)` hashes per-request dimensions (`system`, `tools`, `model`, `thinking`, `thinkingBudget`) at `turn:before` and compares against the previous turn on `turn:after`. When
|
|
389
|
+
`installCacheBreakLogger(hooks, options?)` hashes per-request dimensions (`system`, `tools`, `model`, `thinking`, `thinkingBudget`, `messagePrefix`) at `turn:before` and compares against the previous turn on `turn:after`. When a break is detected on turn 2+ and dimensions differ, it logs the responsible dimension(s) — turning "cache broke" into "tools array changed at turn 7 because MCP server X loaded".
|
|
361
390
|
|
|
362
391
|
```ts
|
|
363
392
|
import { installCacheBreakLogger } from 'zidane'
|
|
@@ -368,7 +397,16 @@ const uninstall = installCacheBreakLogger(agent.hooks, {
|
|
|
368
397
|
})
|
|
369
398
|
```
|
|
370
399
|
|
|
371
|
-
|
|
400
|
+
Two thresholds decide what counts as a break:
|
|
401
|
+
|
|
402
|
+
- `expectedReadAtLeast` (default `1`) — absolute floor. On its own this only catches *total* cache loss.
|
|
403
|
+
- `expectedReadRatio` (default `0.8`) — relative floor, compared against the prefix the previous turn established (`cacheRead + cacheCreation`). This is what catches a **partial-prefix break**, where the scaffold still matches so `cacheRead` stays large and non-zero (pinned to a constant) while the whole conversation re-bills at the cache-write rate. Set `0` to restore absolute-only behavior. Skipped until the previous prefix exceeds 1024 tokens, below which the ratio is noise.
|
|
404
|
+
|
|
405
|
+
The `messagePrefix` dimension keeps one hash *per message* (`messageHashes`) and diffs them positionally over the range both turns share — so appending messages, which every healthy turn does, is silent, and any in-place rewrite of a shared position (including the previous turn's last message) is reported with its index. It is what catches compaction, stale-read elision, media age-out, and host-side history trimming rewriting the transcript mid-run, none of which touch `system`, `tools`, or the model id.
|
|
406
|
+
|
|
407
|
+
Besides the text `log` line, the installer emits the typed **`'cache:break'`** agent hook on every detected break, carrying a structured `CacheBreakInfo` (`turn`, `model`, `cacheRead`, `cacheCreation`, the floor that tripped, `rebilledTokens` — the approximate re-billed prefix — and the `changed` dimensions + human-readable `details`). Pair `rebilledTokens` with `estimateRebilledCostUsd(provider, model, tokens)` from `providers/cost` to price the break in dollars; it returns `undefined` on a rate miss rather than fabricating $0. Two deliberate asymmetries with the text line: the hook fires even when NO dimension changed — an empty `changed` on a byte-stable request is the TTL-expiry / provider-eviction signature, which grep-oriented logs have always skipped — and it only exists while the installer is attached (like `compact:*`, the loop itself never emits it). The TUI consumes it for its footer chip; hosts can drive metrics or alerts from the same payload.
|
|
408
|
+
|
|
409
|
+
Heuristic only: skips providers that don't report `cacheRead` (Codex / Cerebras). Pure helpers `snapshotCacheDimensions`, `diffCacheDimensions`, `hashMessages`, and `fnv1a32` are exported for custom telemetry that needs to stamp the same hashes onto spans.
|
|
372
410
|
|
|
373
411
|
## Tool aliasing (wire vs. canonical)
|
|
374
412
|
|
|
@@ -582,6 +620,26 @@ execution:ready ← once per agent when the execution handle binds
|
|
|
582
620
|
|
|
583
621
|
**Orphan reaper.** `ProcessContext` lazy-registers a `process.on('exit')` handler on the first `execBackground` call that synchronously SIGTERMs every still-running task's process group. Without it, the `detached: true` spawn flag (required for the kill-tree semantics) would leak children when the host process exits without calling `destroy()` (Ctrl+C, uncaught exception, OOM kill). The handler deregisters in `destroy()` so reconstructed contexts don't accumulate listeners.
|
|
584
622
|
|
|
623
|
+
### Where MCP servers run (`scope: 'host' | 'context'`)
|
|
624
|
+
|
|
625
|
+
An MCP server is an ordinary program: its `fs.readFileSync` / `fs.writeFile` hit whatever filesystem it was spawned on. By default (`scope: 'host'`) stdio servers are spawned as children of the agent process via `StdioClientTransport`, so under a sandboxed execution context the agent's own tools and its MCP servers see **different filesystems** — a path the model found with `list_files` doesn't exist for the server, and an MCP write lands where `read_file` can't see it.
|
|
626
|
+
|
|
627
|
+
`McpServerConfig.scope: 'context'` relocates a stdio server into the execution context through the optional `ExecutionContext.execStream` seam (`capabilities.stream`). `createTransport` then builds `createContextStdioTransport` (`src/mcp/context-transport.ts`) instead, reusing the SDK's own `ReadBuffer` / `serializeMessage` for newline framing — `ReadBuffer` decodes only at newline boundaries, which is why the seam carries bytes: decoding per chunk would corrupt any multibyte codepoint split across a boundary.
|
|
628
|
+
|
|
629
|
+
Opt-in rather than automatic, because it's wrong for most servers: a GitHub / Linear / Sentry MCP wants the host's credentials and only talks to the network. Only workspace-touching servers (filesystem, git, build tooling) want `'context'`. Two consequences: the context's image must contain the server's runtime (`node`, `uvx`, …), and this buys filesystem **coherence, not isolation** — remote-transport MCP servers can never be relocated, so the agent's reach is still not bounded by the sandbox. A `scope: 'context'` server on a context without `execStream` is a bootstrap error, never a silent fallback to a host spawn.
|
|
630
|
+
|
|
631
|
+
**Ordering.** The agent always hands the connector an `McpExecutionBinding`, but the binding is **lazy**: it carries `getHandle()` rather than a resolved handle, and `createTransport` calls it only when a `scope: 'context'` stdio server actually builds its transport. So `warmup()` keeps fanning MCP connect and skills out in parallel, host-only setups never spawn, and there is no ordering special case — the handle necessarily exists by the time `execStream` runs because the same call site awaits it. This also matters for sub-agents: `spawn` hands the child the parent's inherited `mcpServers` alongside a `nonOwningMcpConnector` that reuses the parent's live connection, so the child builds no transport and must not cold-boot a sandbox per spawn.
|
|
632
|
+
|
|
633
|
+
**Lifecycle.** Reconnect after a mid-session server death goes through `makeLazyConnector`, which captures the binding in its closure and rebuilds the transport through `execStream` — a reconnect can never silently fall back to a host spawn. `connection.close()` reaches `StreamHandle.kill()`, so no server survives in the sandbox to accumulate across reconnects. The context transport is branded `__contextScoped`, and `stdioPidOf` refuses to read a pid from it: `killStdioChild` signals **host** pids, and a sandbox pid (often a small number like `77`) names an unrelated host process.
|
|
634
|
+
|
|
635
|
+
**Per-context backing.** `process` uses a piped `spawn`. `docker` uses the `docker exec -i` **CLI**, not dockerode: dockerode's hijacked exec (the only HTTP-API route to a writable stdin) rides on docker-modem's HTTP 101 upgrade handling, which hangs under Bun — verified working under Node, so it's a runtime incompatibility rather than misuse. `e2b` uses `commands.run({ background: true, stdin: true, timeoutMs: 2^31-1 })` + `sendStdin` — all three options non-default and each load-bearing (no `stdin` → immediate EOF; the 60s default `timeoutMs` → the server is killed mid-session; and NOT `timeoutMs: 0`, which `@connectrpc/connect`'s deadline signal treats as already expired). `daytona` must use a **PTY**, since its process/session API has no stdin at all; that forces `stty raw -echo` (terminal echo would return our own writes as responses; canonical mode rewrites `\n`→`\r\n` and caps lines near 4096 bytes) and a stderr redirect to a file (one terminal stream means stderr would otherwise interleave into the protocol). The real command lives in an uploaded script — the typed launch line is itself subject to canonical-mode echo and the line cap, so it stays short and constant — and the stderr file is polled back through the exec API onto `StreamHandle.stderr` (the poll carries a `ZEOF-<token>:<wc -c>` suffix so the next offset comes from the file's real size rather than from possibly-transformed content), then removed on teardown.
|
|
636
|
+
|
|
637
|
+
**Two non-obvious behaviours, both found only by running against live infrastructure.**
|
|
638
|
+
|
|
639
|
+
*Daytona's PTY is `lazyStart`: the shell does not exist when you type into it.* The launch line is buffered, then the login shell boots, prints a prompt, and replays the input through its line editor — so the raw stream opens with the echoed launch line, the echoed first request, bracketed-paste escapes and a `%` prompt. An MCP client parses that echoed request as a message and the handshake collapses. `execStream` therefore gates the stream on a readiness sentinel the *child* prints (`printf 'Z-%s-READY\n' <token>`) and discards every byte before it. The sentinel is assembled at runtime so the echoed source line — which contains the format string, not the result — cannot false-match it.
|
|
640
|
+
|
|
641
|
+
*Killing a context-scoped server needs a process group, and neither sandbox gives you one for free.* `npx`-style launchers fork the real server as a grandchild that survives a pid-only kill and accumulates across reconnects. Both `docker` and `e2b` therefore launch under `setsid -w` (with a runtime probe falling back to a plain exec on images whose setsid lacks `-w`; bare `setsid` is not an option, as its parent exits immediately and tears the stream down) and record the session leader's pid, which is then the PGID. On E2B this is a safety requirement, not an optimisation: the pid E2B reports runs in **envd's own process group**, so a group kill on it would take down the sandbox agent. The group signal is `kill -TERM -$pid` with no `--` — dash's kill builtin rejects it.
|
|
642
|
+
|
|
585
643
|
### Once (lazy on first `warmup()` / `run()` / `activateSkill()` — or eager when `eager: true`)
|
|
586
644
|
|
|
587
645
|
```
|
|
@@ -731,7 +789,7 @@ Defaults: unlimited turns (set `maxTurns` as a safety net), model-driven output
|
|
|
731
789
|
|
|
732
790
|
**`autoCompact`** — loop-native auto-compaction (`boolean | AutoCompactConfig`). At each turn boundary the loop computes the last turn's effective input (`input + cacheRead + cacheCreation`, so prompt caching can't hide a full window) and, once it crosses `threshold` (default `0.85`) of the effective context window, runs a real summary over the older turns (`compactConversation`, `scope: 'tail'`), appends a `compact-summary` marker plus optional file/skill restoration (`buildPostCompactAttachments`) to the session, and continues — the next turn's `applyCompactSummaryCutoff` elides the summarized turns. Fires mid tool-loop AND at end-of-run (so a resumed session starts compacted). Window source is `autoCompact.contextWindow` → `provider.meta.contextWindow`; with neither it's inert (the SDK core has no model catalog, so chat hosts inject the picked model's window). A hysteresis floor (`minGrowthFraction`, default `0.1`) stops post-compact thrashing and a consecutive-failure circuit breaker (`maxFailures`, default `2`) disables it for the run if the summary keeps failing. Distinct from `compactStrategy` (per-turn wire-only tool-output trim) and the always-on reactive `context_exceeded` recovery; observe via the `compact:start` / `compact:end` hooks. SDK default off; chat profiles enable it (`{ threshold: 0.85 }`) and the TUI/GUI inject the window + honor the user's enable/threshold settings per run.
|
|
733
791
|
|
|
734
|
-
**`dedupReads` + `requireReadBeforeEdit`** share the same per-session `WeakMap<Session, Map<canonicalAbsPath, { contentHash, offset, limit, maxBytes, mtimeMs }>>`. The key is the canonical absolute path (via `node:path.resolve(handle.cwd, modelProvidedPath)`) so all path shapes the model can write — `src/x`, `./src/x`, `/abs/cwd/src/x` — converge on one entry. Map population happens whenever **either** feature is enabled; the dedup *short-circuit* (returning the "unchanged since the previous read" stub) is what `dedupReads` actually gates. This decoupling means `dedupReads: false` no longer breaks the `requireReadBeforeEdit` guard. `edit` / `multi_edit` reject when no entry exists or the on-disk hash drifted; both update the hash on success so chained edits don't need re-reads. Optional `AgentOptions.readState` (also exposed on `ToolContext.readState`) forwards an explicit map — used by `createSpawnTool({ shareReadState: true })` so a sessionless child still feeds the parent's tracking without sharing turn persistence. GC'd with the session when no explicit map is in play. The map is **never serialized**, so a host that reloads a fresh `Session` from a store on resume — or a durable/replay runtime where a journaled `read_file` never re-runs its live `rememberRead` — would start with an empty map and the gate would reject every edit whose read happened in an earlier turn. To prevent that, a `tool:gate` hook (installed when `requireReadBeforeEdit` is on) pre-hydrates the map from `session.turns` right before an `edit`/`multi_edit` runs, when its target path is missing from the map: transcript reconstruction for full reads, an `execution.readFile` re-hash fallback for partial ones, alias-aware for wire-named transcripts. It fires **inside the loop, outside any journaled tool body, on every replay**, so the verdict can never be frozen by a host that journals tool execution; it has an O(1) `Map.has` fast path so it only scans when a path is genuinely absent, and never blocks (the authoritative read/drift verdict stays in the tool body). The `restateTool` wrapper additionally hydrates once per process for `read_file` dedup; it is a complement, not what the gate depends on.
|
|
792
|
+
**`dedupReads` + `requireReadBeforeEdit`** share the same per-session `WeakMap<Session, Map<canonicalAbsPath, { contentHash, offset, limit, maxBytes, mtimeMs }>>`. The key is the canonical absolute path (via `node:path.resolve(handle.cwd, modelProvidedPath)`) so all path shapes the model can write — `src/x`, `./src/x`, `/abs/cwd/src/x` — converge on one entry. Map population happens whenever **either** feature is enabled; the dedup *short-circuit* (returning the "unchanged since the previous read" stub) is what `dedupReads` actually gates. This decoupling means `dedupReads: false` no longer breaks the `requireReadBeforeEdit` guard. `edit` / `multi_edit` reject when no entry exists or the on-disk hash drifted; both update the hash on success so chained edits don't need re-reads — and set the entry's `elided` flag in the same update, so the next re-read misses dedup and serves the fresh body instead of a "still current" stub pointing at the pre-mutation output (`write_file` seeds with the flag for the same reason). Dedup resumes after that one honest read; the flag never affects the edit gate. Optional `AgentOptions.readState` (also exposed on `ToolContext.readState`) forwards an explicit map — used by `createSpawnTool({ shareReadState: true })` so a sessionless child still feeds the parent's tracking without sharing turn persistence. GC'd with the session when no explicit map is in play. The map is **never serialized**, so a host that reloads a fresh `Session` from a store on resume — or a durable/replay runtime where a journaled `read_file` never re-runs its live `rememberRead` — would start with an empty map and the gate would reject every edit whose read happened in an earlier turn. To prevent that, a `tool:gate` hook (installed when `requireReadBeforeEdit` is on) pre-hydrates the map from `session.turns` right before an `edit`/`multi_edit` runs, when its target path is missing from the map: transcript reconstruction for full reads, an `execution.readFile` re-hash fallback for partial ones, alias-aware for wire-named transcripts. It fires **inside the loop, outside any journaled tool body, on every replay**, so the verdict can never be frozen by a host that journals tool execution; it has an O(1) `Map.has` fast path so it only scans when a path is genuinely absent, and never blocks (the authoritative read/drift verdict stays in the tool body). The `restateTool` wrapper additionally hydrates once per process for `read_file` dedup; it is a complement, not what the gate depends on.
|
|
735
793
|
|
|
736
794
|
**`dedupTools`** generalizes the same pattern via a consumer hasher per tool. Parallel `WeakMap<Session, Map<toolName, { hash, result, repeats }>>` keeps only the most recent hash. Two forms:
|
|
737
795
|
|
package/docs/CHAT.md
CHANGED
|
@@ -355,8 +355,13 @@ Both share `SHARED_BEHAVIOR`:
|
|
|
355
355
|
// context / token discipline
|
|
356
356
|
compactStrategy: 'tail',
|
|
357
357
|
compactThreshold: 128 * 1024,
|
|
358
|
-
compactKeepTurns:
|
|
359
|
-
elideStaleReads:
|
|
358
|
+
compactKeepTurns: 8,
|
|
359
|
+
elideStaleReads: 'notice', // append-only staleness reminders — never rewrites
|
|
360
|
+
// cached prefix bytes ('stub' re-bills the
|
|
361
|
+
// conversation per newly-stale read)
|
|
362
|
+
mediaKeepTurns: 8, // matched to compactKeepTurns so both frontiers
|
|
363
|
+
// jump on the same chunk boundary — one prompt-
|
|
364
|
+
// cache invalidation per window, not two
|
|
360
365
|
toolDisclosure: 'lazy',
|
|
361
366
|
toolOutputBudget: 64 * 1024,
|
|
362
367
|
toolOutputBudgetExcludeTools: DEFAULT_BUDGET_EXCLUDE_TOOLS,
|
|
@@ -374,8 +379,10 @@ Both share `SHARED_BEHAVIOR`:
|
|
|
374
379
|
},
|
|
375
380
|
repeatGuard: {}, // chain guard: shell blocks @4 / aborts @8;
|
|
376
381
|
// read_file blocks @6 / aborts @12
|
|
377
|
-
thinkingDecay: { afterTurn: 10,
|
|
378
|
-
//
|
|
382
|
+
thinkingDecay: { afterTurn: 10, floor: 1024 }, // step form: ONE budget change
|
|
383
|
+
// per run = one cache invalidation (the budget
|
|
384
|
+
// is part of Anthropic's cache key); no-op
|
|
385
|
+
// unless a thinking budget is set
|
|
379
386
|
}
|
|
380
387
|
```
|
|
381
388
|
|
|
@@ -1332,7 +1339,7 @@ Two distinct OAuth flows, both ending in stored credentials and a working sessio
|
|
|
1332
1339
|
|
|
1333
1340
|
### AI-provider OAuth
|
|
1334
1341
|
|
|
1335
|
-
Used by Anthropic (Claude Pro/Max) and OpenAI Codex. Wired through `descriptor.oauthProvider` (
|
|
1342
|
+
Used by Anthropic (Claude Pro/Max) and OpenAI Codex. Wired through `descriptor.oauthProvider` (an `OAuthProviderInterface` — zidane's own type, importable from `'zidane'`; pi-ai removed its version of the interface in 0.80.8).
|
|
1336
1343
|
|
|
1337
1344
|
```ts
|
|
1338
1345
|
import { oauthUsesManualCodePaste, runOAuthLogin, setProviderCredential, supportsOAuth } from 'zidane/chat'
|
package/docs/EXTENSIONS.md
CHANGED
|
@@ -157,7 +157,7 @@ ctx.registerConfigItem(categoryId?, item)
|
|
|
157
157
|
| `registerCommand(Command)` | A `/<name>` workflow — ordered handler steps (or a bare `run`) that `emit` local output (full markdown, or collapsed diff-colorable `detail` blocks), call `ctx.runAgent` for interactive sub-runs, `ctx.generateData` for silent schema-pinned JSON (no turns, no transcript; takes a zod v4 schema — result type inferred — or a plain JSON Schema object; optional `model` pins a catalog model), or `ctx.spawnAgents?` for parallel ISOLATED child agents (own context windows, full tool set, per-task `model` pinning — rendered like `spawn`-tool children); `ctx.appendContext?` persists a compact model-visible context note; `ctx.session?` exposes a read-only live view of the surrounding session (`id`/`turns`/`runs`/`metadata`) and `ctx.interaction?` offers user interaction (`confirm` yes/no, `askUser` forms — selects support `multi: true` — `presentPlan` approval) — the optional surfaces are host-dependent, so guard for bare hosts | `emit` output is display-only (persisted, never sent to the model); `appendContext` is the sanctioned model-visible escape hatch. Names dedupe first-registration-wins. Extensions extend `/` here rather than via a raw completion provider. |
|
|
158
158
|
| `registerTheme(Theme)` | New `Settings → Theme` option | The picker's option list folds extension themes in at render time; built-in theme ids (`default`, `catppuccin-mocha`, …) shadow extension ids on collision. |
|
|
159
159
|
| `registerAgent(AgentProfile)` | A complete agent (preset = tools + system + behavior + hooks + mcpServers + skills) added to the registry. Appears in the `shift+tab` cycle alongside Build / Plan. | Built-in profiles (`build`, `plan`) win on id collision; host-supplied profiles win over extensions too. |
|
|
160
|
-
| `registerProvider(ProviderDescriptor)` | New LLM provider in the auth + model pickers | Host-supplied providers (incl. `BUILTIN_PROVIDERS`) win on `key` collision. A `models` list on the descriptor REPLACES the built-in registry, so the picker shows only your provider's models. For an OAuth provider, `await registerCustomOAuthProvider(flow)` in `setup` — pi-ai resolves the flow by id on EVERY token resolve, not just on refresh. A gateway speaking a built-in wire format reuses its factory: `anthropic({ baseURL, authMode: 'bearer', credentialSource: { providerId: '<key>' }, capabilities })` gets bearer auth, credentials under your own key (with the shared lazy refresh + cross-process lock), and no Claude Code persona. `extensions/bolt` is a complete example: device-flow login + Anthropic-compatible proxy. |
|
|
160
|
+
| `registerProvider(ProviderDescriptor)` | New LLM provider in the auth + model pickers | Host-supplied providers (incl. `BUILTIN_PROVIDERS`) win on `key` collision. A `models` list on the descriptor REPLACES the built-in registry, so the picker shows only your provider's models. For an OAuth provider, `await registerCustomOAuthProvider(flow)` in `setup` — zidane's own OAuth registry (`src/providers/oauth-registry.ts`; pi-ai removed its global one in 0.80.8) resolves the flow by id on EVERY token resolve, not just on refresh. A gateway speaking a built-in wire format reuses its factory: `anthropic({ baseURL, authMode: 'bearer', credentialSource: { providerId: '<key>' }, capabilities })` gets bearer auth, credentials under your own key (with the shared lazy refresh + cross-process lock), and no Claude Code persona. `extensions/bolt` is a complete example: device-flow login + Anthropic-compatible proxy. |
|
|
161
161
|
| `registerSystemPromptFragment(text)` | Append a paragraph to every system prompt | Joined with `\n\n` separators, appended to `ctx.system` via a `system:transform` hook re-attached on every agent rebuild (works for every profile — Build / Plan / host-supplied). |
|
|
162
162
|
| `registerContextBreakdownSection(fn)` | Existing low-level context panel contributor | May return a section or array of sections; async contributors are supported. |
|
|
163
163
|
| `registerContextPanelSection(section)` | Richer context panel section with `id`, `title`, static rows, and/or async `load` | Normalized into the same context panel model. `registerContextBreakdownSection` remains supported. |
|
package/docs/RESTATE.md
CHANGED
|
@@ -177,7 +177,7 @@ On platforms without bidirectional streaming (AWS Lambda, sandbox-reattach runne
|
|
|
177
177
|
3. **Share that clock with the store**: `restateSessionStore(ctx, { clock })` routes the store's finalize-time `save` / `updateStatus` timestamps through the same coalesced anchor instead of spending their own `ctx.date.now()`.
|
|
178
178
|
4. **MCP `exclude`**: list deterministic, side-effect-free MCP read tools in `restateMcpToolWrap(ctx, { exclude: [...] })` so they skip journaling entirely.
|
|
179
179
|
5. **Stay on eager state.** Lazy state (`enableLazyState`) makes each `ctx.get` a suspend+replay on no-streaming platforms and gets worse as the journal grows — the opposite of what you want here. The session store reads each slot at most once and (under default eager state) those reads are local/free anyway.
|
|
180
|
-
6. **Context discipline** (already on in `restateBehavior`): `compactStrategy: 'tail'`, `elideStaleReads`, `toolOutputBudget`, `autoCompact` shrink the per-turn provider payload — fewer tokens and lower LLM latency every turn.
|
|
180
|
+
6. **Context discipline** (already on in `restateBehavior`): `compactStrategy: 'tail'`, `elideStaleReads: 'stub'`, `toolOutputBudget`, `autoCompact` shrink the per-turn provider payload — fewer tokens and lower LLM latency every turn. Note the deliberate divergence from the chat profiles' `'notice'`: restate optimizes for payload size (journal weight + latency), so it accepts the prompt-cache re-bill that wire-level stubbing costs; see `AgentBehavior.elideStaleReads` for the trade.
|
|
181
181
|
|
|
182
182
|
`ctx.run` for LLM and tool calls is the irreducible floor — it's the real work. Levers 1–4 remove the overhead *around* it.
|
|
183
183
|
|
package/docs/SKILL.md
CHANGED
|
@@ -29,7 +29,7 @@ const agent = createAgent({
|
|
|
29
29
|
maxTurns: 50,
|
|
30
30
|
maxTokens: 16384,
|
|
31
31
|
thinkingBudget: 10240,
|
|
32
|
-
thinkingDecay: { afterTurn: 5,
|
|
32
|
+
thinkingDecay: { afterTurn: 5, floor: 1024 },
|
|
33
33
|
cache: true, // prompt-cache breakpoints
|
|
34
34
|
toolOutputBudget: 32768, // off by default
|
|
35
35
|
dedupReads: true, // re-read dedup
|
|
@@ -682,6 +682,26 @@ agent.hooks.hook('mcp:bootstrap:end', (ctx) => {
|
|
|
682
682
|
|
|
683
683
|
For `transport: 'stdio'`, `env` **merges** on top of the MCP SDK's inherited-env whitelist (POSIX: `HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, `USER`; Win32: `APPDATA`, `PATH`, …) — `env: { MY_TOKEN: '…' }` no longer strips `PATH` and trips `spawn ENOENT`. Use `strictEnv: true` only for fully-isolated child envs (tight sandboxes).
|
|
684
684
|
|
|
685
|
+
### Where a stdio server runs (`scope`)
|
|
686
|
+
|
|
687
|
+
`scope: 'host'` (default) spawns the server as a child of the agent process. `scope: 'context'` spawns it **inside** the agent's execution context instead, via `ExecutionContext.execStream`:
|
|
688
|
+
|
|
689
|
+
```ts
|
|
690
|
+
mcpServers: [
|
|
691
|
+
// Touches the workspace → must share the agent's filesystem.
|
|
692
|
+
{ name: 'fs', transport: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '.'], scope: 'context' },
|
|
693
|
+
// Network + host credentials only → leave it on the host.
|
|
694
|
+
{ name: 'github', transport: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] },
|
|
695
|
+
]
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
Without this, a sandboxed context (Docker / E2B / Daytona) leaves the agent's tools and its MCP servers on different filesystems — the model writes a file through an MCP tool and `read_file` can't find it. Requires a context advertising `capabilities.stream` (`process`, `docker`, `e2b`, `daytona`; not `createMockContext`). Ignored for `sse` / `streamable-http`. Note this is filesystem coherence, **not** isolation: remote MCP servers are unreachable by this mechanism.
|
|
699
|
+
|
|
700
|
+
Two operational requirements:
|
|
701
|
+
|
|
702
|
+
- **The context image must contain the server's runtime.** A missing `node` / `uvx` / `bunx` surfaces as a bootstrap error for that server (the rest of the agent keeps working). Verified available out of the box: E2B ships `node` + `npx` + `python3`, Daytona ships those plus `bun` and `uvx`.
|
|
703
|
+
- **The `docker` context additionally needs the `docker` CLI on `PATH`** for `scope: 'context'` servers specifically — unlike every other Docker operation, which only needs the daemon socket. The CLI is used because dockerode's hijacked exec (the only HTTP-API route to a writable stdin) hangs under Bun. `DOCKER_HOST` is still honoured, so remote daemons work.
|
|
704
|
+
|
|
685
705
|
### Per-server tool filtering
|
|
686
706
|
|
|
687
707
|
`enabledTools` / `disabledTools` / `toolFilter` apply at config time (before registration); the `mcp:tools:filter` hook is the runtime escape hatch when host-side policy depends on the user / environment. Config filters compose: allow-list first, then deny-list, then predicate, then hook. `cachedTools` skips the `tools/list` round-trip when the host can supply the schemas itself; `lazyConnect: true` (requires `cachedTools`, incompatible with `auth: 'oauth'`) defers `client.connect()` to the first `tools/call`.
|
package/docs/TUI.md
CHANGED
|
@@ -546,6 +546,8 @@ For a non-OpenTUI shell (Electron / Tauri / web), **don't** import `<App>` — s
|
|
|
546
546
|
|
|
547
547
|
## Auto-update
|
|
548
548
|
|
|
549
|
+
The footer also surfaces a transient prompt-cache toast, fed by the `'cache:break'` agent hook (`installCacheBreakLogger` is installed per agent) and visible for ~8 seconds, carrying the approximate re-billed token count and its estimated dollar cost (from the same price registry as the footer cost figure; the `~$` segment is omitted when no rate is published for the model — no fabricated $0). Severity is split so the chip never cries wolf: an attributable break renders warn-colored as **`⚡ cache break · <culprit> · +Nk tok · ~$0.48`**, naming the culprit dimension (`system`, `tools`, `messagePrefix`, …); a byte-stable break — the TTL lapsing over normal think-time between prompts — renders dim as **`⚡ cache expired · +Nk tok · ~$0.48`**. A fresh break restarts the window with the newest culprit; a session switch clears it immediately. Hidden while the cache is healthy, dropped in `minimal` UI mode, sheds first on narrow terminals — a transient cost signal, not an action. Break detail lines land in the Logs tab under `ZIDANE_DEBUG`.
|
|
550
|
+
|
|
549
551
|
The TUI quietly polls the npm registry once a day for a newer release of `zidane-tui` and surfaces a passive `↑ vX.Y.Z` chip in the footer when one is available. The check is cache-driven (one HTTP roundtrip per 24 h, persisted under `userDir/update-check-<pkg>@<channel>.json`), 3-second timeout, never blocks boot, swallows network errors. Opt out via `Settings.checkForUpdates`, or the env vars `ZIDANE_NO_UPDATE` / `NO_UPDATE_NOTIFIER` / `CI` (any non-empty value disables it).
|
|
550
552
|
|
|
551
553
|
The explicit `zidane upgrade` subcommand bypasses both the setting and the env opt-outs — it's a user gesture:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zidane",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.1",
|
|
4
4
|
"description": "an agent that goes straight to the goal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -262,7 +262,7 @@
|
|
|
262
262
|
"typecheck": "tsc --noEmit"
|
|
263
263
|
},
|
|
264
264
|
"dependencies": {
|
|
265
|
-
"@earendil-works/pi-ai": "0.
|
|
265
|
+
"@earendil-works/pi-ai": "0.82.1",
|
|
266
266
|
"chalk": "^5.6.2",
|
|
267
267
|
"fzf": "^0.5.2",
|
|
268
268
|
"hookable": "^6.1.1",
|