@visual-z/xeno 0.8.4 → 0.9.0
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 +8 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3108
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** Ajv owns JSON Schema semantics; validation never repairs model output (D11/D13). */
|
|
2
|
-
import { Ajv } from "ajv";
|
|
3
|
-
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
4
|
-
import { Ajv2019 } from "ajv/dist/2019.js";
|
|
5
|
-
import { addFormats } from "@modelcontextprotocol/client/validators/ajv";
|
|
6
|
-
|
|
7
|
-
export type OutputValidation = { ok: true; value: unknown } | { ok: false; error: string };
|
|
8
|
-
export function outputValidator(schema: unknown): (text: string) => OutputValidation {
|
|
9
|
-
if (typeof schema !== "boolean" && (schema === null || typeof schema !== "object" || Array.isArray(schema))) throw Error("output schema must be a JSON Schema object or boolean");
|
|
10
|
-
const dialect = typeof schema === "object" && schema !== null && "$schema" in schema ? String(schema.$schema) : "";
|
|
11
|
-
const options = { allErrors: true, strictSchema: true, strictTypes: false, strictTuples: false, strictRequired: false, validateFormats: true,
|
|
12
|
-
coerceTypes: false, useDefaults: false, removeAdditional: false, logger: false as const };
|
|
13
|
-
const ajv = dialect === "https://json-schema.org/draft/2020-12/schema" ? new Ajv2020(options)
|
|
14
|
-
: dialect === "https://json-schema.org/draft/2019-09/schema" ? new Ajv2019(options)
|
|
15
|
-
: dialect === "" || dialect === "http://json-schema.org/draft-07/schema#" || dialect === "http://json-schema.org/draft-07/schema" ? new Ajv(options) : undefined;
|
|
16
|
-
if (!ajv) throw Error("unsupported output schema dialect");
|
|
17
|
-
// The approved MCP client exposes Ajv's standard formats; share them rather than silently ignoring formats.
|
|
18
|
-
addFormats(ajv);
|
|
19
|
-
const validate = ajv.compile(schema);
|
|
20
|
-
return text => {
|
|
21
|
-
let value: unknown;
|
|
22
|
-
try { value = JSON.parse(text); } catch { return { ok: false, error: "final output is not JSON" }; }
|
|
23
|
-
if (!validate(value)) return { ok: false, error: ajv.errorsText(validate.errors, { separator: "; " }).slice(0, 2048) };
|
|
24
|
-
return { ok: true, value };
|
|
25
|
-
};
|
|
26
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/** Explicit package operations, isolated from agent sessions and provider credentials. */
|
|
2
|
-
import { mkdir, mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
4
|
-
import { packageConfig, type PackageDeclaration } from "../core/packages.ts";
|
|
5
|
-
import { configDir, readJsonFile } from "../store/config.ts";
|
|
6
|
-
import { serialized } from "../store/serial.ts";
|
|
7
|
-
import { projectTrust } from "../store/trust.ts";
|
|
8
|
-
import { packageSource, packageHome, packageKey, packageRecords, installedPackage, configuredPackages, savePackageDeclaration, savePackageRecord, removePackageRecords, packageManifest, type PackageRecord } from "../store/packages.ts";
|
|
9
|
-
|
|
10
|
-
export type PackageOperation = "install" | "remove" | "update" | "list";
|
|
11
|
-
export interface PackageCommand { operation: PackageOperation; sources: string[]; local?: boolean }
|
|
12
|
-
export type PackageExecutor = (argv: string[], options: { cwd: string; signal?: AbortSignal }) => Promise<{ code: number; stdout: string; stderr: string }>;
|
|
13
|
-
export interface PackageOptions { workspace: string; userDir?: string; run?: PackageExecutor; signal?: AbortSignal; say?: (text: string) => void; trusted?: boolean }
|
|
14
|
-
export interface ListedPackage { source: string; identity: string; scope: "user" | "project"; path?: string; pinned: boolean; filtered: boolean }
|
|
15
|
-
|
|
16
|
-
export const executePackageCommand: PackageExecutor = async (argv, options) => {
|
|
17
|
-
if (options.signal?.aborted) throw Error("Package operation cancelled");
|
|
18
|
-
const child=Bun.spawn(argv,{cwd:options.cwd,stdin:"ignore",stdout:"pipe",stderr:"pipe"});
|
|
19
|
-
const abort=()=>{child.kill();}; options.signal?.addEventListener("abort",abort,{once:true});
|
|
20
|
-
try {
|
|
21
|
-
const [stdout,stderr,code]=await Promise.all([new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited]);
|
|
22
|
-
if(options.signal?.aborted)throw Error("Package operation cancelled"); return {stdout,stderr,code};
|
|
23
|
-
} finally { options.signal?.removeEventListener("abort",abort); }
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export class PackageManager {
|
|
27
|
-
readonly options: PackageOptions;
|
|
28
|
-
constructor(options: PackageOptions) { this.options={...options,workspace:resolve(options.workspace)}; }
|
|
29
|
-
private base(local=false):string { return local?join(this.options.workspace,".xeno"):this.options.userDir??configDir(); }
|
|
30
|
-
private async command(argv:string[],cwd:string):Promise<string> {
|
|
31
|
-
const result=await (this.options.run??executePackageCommand)(argv,{cwd,...(this.options.signal?{signal:this.options.signal}:{})});
|
|
32
|
-
if(result.code!==0)throw Error(`${argv[0]} exited ${result.code}: ${result.stderr.trim()||result.stdout.trim()}`);
|
|
33
|
-
return result.stdout.trim();
|
|
34
|
-
}
|
|
35
|
-
async list():Promise<ListedPackage[]> {
|
|
36
|
-
const entries:ListedPackage[]=[];
|
|
37
|
-
for(const scope of ["user","project"] as const){const base=this.base(scope==="project");for(const declaration of await configuredPackages(base)){
|
|
38
|
-
const config=packageConfig(declaration),source=packageSource(config.source,base),path=await installedPackage(source,base);
|
|
39
|
-
entries.push({source:config.source,identity:source.id,scope,...(path?{path}:{}),pinned:source.pinned,filtered:typeof declaration!=="string"});
|
|
40
|
-
}}
|
|
41
|
-
return entries;
|
|
42
|
-
}
|
|
43
|
-
async install(raw:string,options:{local?:boolean;persist?:boolean;base?:string;declaration?:PackageDeclaration}={}):Promise<string> {
|
|
44
|
-
if(this.options.signal?.aborted)throw Error("Package operation cancelled");
|
|
45
|
-
const base=options.base??this.base(options.local),source=packageSource(raw,this.options.workspace);
|
|
46
|
-
return serialized(join(packageHome(base),"operation"),async()=>{
|
|
47
|
-
// Parse existing metadata before any network/installation side effect.
|
|
48
|
-
await configuredPackages(base); const records=await packageRecords(base),previous=records[packageKey(source)];
|
|
49
|
-
const declaration=options.declaration??(source.kind==="local"?relative(base,source.path)||".":source.source);
|
|
50
|
-
if(source.kind==="local") {
|
|
51
|
-
if(!await stat(source.path).then(()=>true,()=>false))throw Error(`Package path does not exist: ${source.path}`);
|
|
52
|
-
await packageManifest(source.path);
|
|
53
|
-
if(options.persist!==false)await savePackageDeclaration(base,source,declaration);
|
|
54
|
-
return source.path;
|
|
55
|
-
}
|
|
56
|
-
const versions=join(packageHome(base),"versions"); await mkdir(versions,{recursive:true});
|
|
57
|
-
const staging=await mkdtemp(join(versions,packageKey(source)+"-")); let published=false;
|
|
58
|
-
try {
|
|
59
|
-
let root:string,revision:string|undefined;
|
|
60
|
-
if(source.kind==="npm") {
|
|
61
|
-
await Bun.write(join(staging,"package.json"),JSON.stringify({name:"xeno-package-install",private:true}));
|
|
62
|
-
await this.command([process.execPath,"add","--exact","--production","--omit","peer",source.spec],staging);
|
|
63
|
-
root=join(staging,"node_modules",source.name);
|
|
64
|
-
} else {
|
|
65
|
-
root=join(staging,"repo"); await this.command(["git","clone","--",source.repo,root],staging);
|
|
66
|
-
if(source.ref)await this.command(["git","checkout","--detach",source.ref],root);
|
|
67
|
-
revision=await this.command(["git","rev-parse","HEAD"],root);
|
|
68
|
-
if(await Bun.file(join(root,"package.json")).exists()) await this.command([process.execPath,"install","--production","--omit","peer"],root);
|
|
69
|
-
}
|
|
70
|
-
if(!await stat(root).then(s=>s.isDirectory(),()=>false))throw Error("Package installation produced no package directory");
|
|
71
|
-
await packageManifest(root);
|
|
72
|
-
const manifest=await readJsonFile<{version?:string}>(join(root,"package.json"));if(manifest.error)throw Error(manifest.error);
|
|
73
|
-
const record:PackageRecord={source:source.source,identity:source.id,path:relative(packageHome(base),root),...(manifest.value?.version?{version:manifest.value.version}:{}),...(revision?{revision}:{})};
|
|
74
|
-
await savePackageRecord(base,source,record);
|
|
75
|
-
try { if(options.persist!==false)await savePackageDeclaration(base,source,declaration); }
|
|
76
|
-
catch(error){await savePackageRecord(base,source,previous);throw error;}
|
|
77
|
-
// Old versions remain available to already-running extension generations.
|
|
78
|
-
published=true; return root;
|
|
79
|
-
} finally { if(!published)await rm(staging,{recursive:true,force:true}); }
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
async remove(raw:string,local=false):Promise<boolean> {
|
|
83
|
-
const base=this.base(local),requested=packageSource(raw,this.options.workspace);
|
|
84
|
-
return serialized(join(packageHome(base),"operation"),async()=>{
|
|
85
|
-
const declarations=await configuredPackages(base),match=declarations.find(entry=>{const source=packageSource(packageConfig(entry).source,base);return source.id===requested.id||source.source===raw||source.kind==="npm"&&source.name===raw;});
|
|
86
|
-
if(!match)return false;
|
|
87
|
-
const source=packageSource(packageConfig(match).source,base);
|
|
88
|
-
await packageRecords(base);
|
|
89
|
-
await savePackageDeclaration(base,source,undefined);const keys=await removePackageRecords(base,source.id);
|
|
90
|
-
if(source.kind!=="local") {
|
|
91
|
-
const versions=join(packageHome(base),"versions"),names=await readdir(versions).catch(()=>[]);
|
|
92
|
-
for(const name of names)if(keys.some(key=>name.startsWith(key+"-")))await rm(join(versions,name),{recursive:true,force:true});
|
|
93
|
-
}
|
|
94
|
-
return true;
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
async update(raw?:string,local?:boolean):Promise<number> {
|
|
98
|
-
let count=0;
|
|
99
|
-
const trusted=this.options.trusted??(await projectTrust(this.options.workspace)).trusted;
|
|
100
|
-
for(const scope of [false,true]) {
|
|
101
|
-
if(local!==undefined&&scope!==local)continue;
|
|
102
|
-
if(scope&&local!==true&&!trusted)continue;
|
|
103
|
-
const base=this.base(scope);
|
|
104
|
-
for(const declaration of await configuredPackages(base)) {
|
|
105
|
-
const source=packageSource(packageConfig(declaration).source,base);
|
|
106
|
-
if(raw&&source.source!==raw&&source.id!==packageSource(raw,this.options.workspace).id&&!(source.kind==="npm"&&source.name===raw))continue;
|
|
107
|
-
if(source.kind==="local")continue;
|
|
108
|
-
await this.install(source.source,{base,persist:false,declaration});count++;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if(raw&&count===0)throw Error(`No installed remote package matches ${raw}`);return count;
|
|
112
|
-
}
|
|
113
|
-
async temporary(raw:string):Promise<string> { return this.install(raw,{base:join(this.base(),"temporary"),persist:false}); }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export async function runPackageCommand(command:PackageCommand,options:PackageOptions):Promise<void> {
|
|
117
|
-
const manager=new PackageManager(options),say=options.say??console.log;
|
|
118
|
-
if(!await stat(manager.options.workspace).then(s=>s.isDirectory(),()=>false))throw Error(`Workspace does not exist: ${options.workspace}`);
|
|
119
|
-
if(command.operation==="list"&&command.sources.length||command.operation==="update"&&command.sources.length>1)throw Error(`Unexpected sources for ${command.operation}`);
|
|
120
|
-
if(command.operation==="list") { for(const entry of await manager.list())say(`${entry.scope} ${entry.source}${entry.pinned?" (pinned)":""} · ${entry.path??"not installed"}`); return; }
|
|
121
|
-
if(command.operation==="update") { say(`Updated ${await manager.update(command.sources[0],command.local)} package(s)`); return; }
|
|
122
|
-
if(command.sources.length===0)throw Error(`${command.operation} requires a package source`);
|
|
123
|
-
for(const source of command.sources) if(command.operation==="install") say(`Installed ${source} · ${await manager.install(source,{...(command.local===undefined?{}:{local:command.local})})}`);
|
|
124
|
-
else say(await manager.remove(source,command.local)?`Removed ${source}`:`Package not configured: ${source}`);
|
|
125
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/** One durable mutation owner for multi-file Patch, independent of provider syntax. */
|
|
2
|
-
import { changedPaths } from "../core/checkpoints.ts";
|
|
3
|
-
import { NOT_STARTED, READ_ATTEMPTED, UNKNOWN_EFFECT, failureDiagnostic, failureFromError } from "../core/tool-failure.ts";
|
|
4
|
-
import { preparePatch, applyPreparedPatch } from "../store/patch.ts";
|
|
5
|
-
import { presentObservation, type ToolDefinition } from "./tools.ts";
|
|
6
|
-
|
|
7
|
-
export function createPatchTool(): ToolDefinition {
|
|
8
|
-
return {
|
|
9
|
-
name: "apply_patch", access: "write", execution: "exclusive", branch: "write",
|
|
10
|
-
description: "Apply a Codex-style multi-file patch containing Add File, Update File, optional Move to, and Delete File. Existing sources require a prior session read or explicit expected_sha256. All targets and exact unique hunks are preflighted before changes; add/move destinations must be absent. Uses durable file checkpoints and compensates partial application without overwriting external changes. Preserves original BOM, line endings and modes. Return contains resulting file SHA-256 versions.",
|
|
11
|
-
input_schema: { type: "object", required: ["patchText"], properties: {
|
|
12
|
-
patchText: { type: "string", description: "Full *** Begin Patch ... *** End Patch text; update hunks start with @@ and lines use space/+/- prefixes." },
|
|
13
|
-
expected_sha256: { type: "object", additionalProperties: { type: "string", pattern: "^[a-f0-9]{64}$" }, description: "Optional source path to exact SHA-256 premise; otherwise use observed session versions." },
|
|
14
|
-
} },
|
|
15
|
-
run: async (ctx, input) => {
|
|
16
|
-
if (!ctx.workspace) return { ok: false, content: "Patch requires the session's durable workspace checkpoint owner.", diagnostic: failureDiagnostic("environment_missing", "Patch requires the session's durable workspace checkpoint owner.", NOT_STARTED) };
|
|
17
|
-
let admitted = false;
|
|
18
|
-
try {
|
|
19
|
-
const expected = input["expected_sha256"];
|
|
20
|
-
if (expected !== undefined && (expected === null || typeof expected !== "object" || Array.isArray(expected))) throw new Error("expected_sha256 must be an object of source digests");
|
|
21
|
-
const prepared = await preparePatch(ctx.root, ctx.workspace.blobs, String(input["patchText"]), {
|
|
22
|
-
...(ctx.observedFileVersion === undefined ? {} : { observedFileVersion: ctx.observedFileVersion }),
|
|
23
|
-
...(expected === undefined ? {} : { expectedSha256: expected as Record<string, string> }),
|
|
24
|
-
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
|
|
25
|
-
});
|
|
26
|
-
admitted = true;
|
|
27
|
-
await ctx.workspace.patch(prepared.changes, () => applyPreparedPatch(ctx.root, ctx.workspace!.blobs, prepared, ctx.signal), ctx.signal);
|
|
28
|
-
const paths = changedPaths(prepared.changes);
|
|
29
|
-
const summary = paths.map(path => `${path}: ${prepared.changes[path]!.after?.sha256 ?? "deleted"}`).join("\n");
|
|
30
|
-
const data = paths.map(path => ({ path, before: prepared.changes[path]!.before?.sha256 ?? null, after: prepared.changes[path]!.after?.sha256 ?? null }));
|
|
31
|
-
return { ...await presentObservation(ctx, `Applied patch to ${paths.length} file path(s).\n${summary}\n\n${prepared.diffs.map(diff => diff.diff).join("\n")}`), ok: true, data: { files: data } };
|
|
32
|
-
} catch (error) {
|
|
33
|
-
const diagnostic = failureFromError(error, admitted ? UNKNOWN_EFFECT : READ_ATTEMPTED);
|
|
34
|
-
return { ok: false, content: diagnostic.message, diagnostic };
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/** R13: an indirect content reader must retain the same read-resource policy. */
|
|
2
|
-
import { explainDecision, type DecisionExplanation, type PermissionAction, type PermissionRule } from "../core/permissions.ts";
|
|
3
|
-
import { permissionPaths } from "../store/permission-paths.ts";
|
|
4
|
-
|
|
5
|
-
export async function permissionResourceAction(root: string, path: string, configured: readonly PermissionRule[],
|
|
6
|
-
remembered: readonly PermissionRule[] = [], tool = "read"): Promise<PermissionAction> {
|
|
7
|
-
return (await permissionResourceDecision(root, path, configured, remembered, tool)).action;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** The same verdict, with the rule that produced it, for the durable approval record. */
|
|
11
|
-
export async function permissionResourceDecision(root: string, path: string, configured: readonly PermissionRule[],
|
|
12
|
-
remembered: readonly PermissionRule[] = [], tool = "read"): Promise<DecisionExplanation> {
|
|
13
|
-
const paths = await permissionPaths(root, [path]);
|
|
14
|
-
const read = explainDecision(configured, remembered, { tool: "read", paths });
|
|
15
|
-
const specific = explainDecision(configured, remembered, { tool, paths });
|
|
16
|
-
const rank = (action: PermissionAction) => action === "deny" ? 2 : action === "ask" ? 1 : 0;
|
|
17
|
-
return rank(specific.action) > rank(read.action) ? specific : read;
|
|
18
|
-
}
|
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
/** Pi owns wire protocols; xeno owns retries, durable events, projection and credential precedence. */
|
|
2
|
-
import type { AssistantMessage, Context, Message as PiMessage, Model, Tool, ProviderStreams, SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
3
|
-
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
4
|
-
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
5
|
-
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
6
|
-
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
7
|
-
import { azureOpenAIResponsesApi } from "@earendil-works/pi-ai/api/azure-openai-responses.lazy";
|
|
8
|
-
import { openAICodexResponsesApi } from "@earendil-works/pi-ai/api/openai-codex-responses.lazy";
|
|
9
|
-
import { googleGenerativeAIApi } from "@earendil-works/pi-ai/api/google-generative-ai.lazy";
|
|
10
|
-
import { googleVertexApi } from "@earendil-works/pi-ai/api/google-vertex.lazy";
|
|
11
|
-
import { bedrockConverseStreamApi } from "@earendil-works/pi-ai/api/bedrock-converse-stream.lazy";
|
|
12
|
-
import { mistralConversationsApi } from "@earendil-works/pi-ai/api/mistral-conversations.lazy";
|
|
13
|
-
import { piMessagesApi } from "@earendil-works/pi-ai/api/pi-messages.lazy";
|
|
14
|
-
import { assertNever } from "../core/exhaustive.ts";
|
|
15
|
-
import { completedToolInput } from "../core/tool-input.ts";
|
|
16
|
-
import type { AssistantBlock } from "../core/events.ts";
|
|
17
|
-
import { toolCallIdentityViolations, type FrozenPrefix, type Message } from "../core/project.ts";
|
|
18
|
-
import type { Api, ModelSpec } from "./model.ts";
|
|
19
|
-
import { providerRequest } from "./provider-request.ts";
|
|
20
|
-
import { grokVersionHint } from "./oauth.ts";
|
|
21
|
-
import type { ResolveEffects } from "./auth.ts";
|
|
22
|
-
import { imageData } from "./images.ts";
|
|
23
|
-
import { coherentTurn, FailedResponseError, type OnDelta, type Provider, type ProviderConfig, type ToolSchema, type Turn } from "./provider.ts";
|
|
24
|
-
import { DEFAULT_RETRY, retryLabel, withRetry, type RetryPolicy } from "./retry.ts";
|
|
25
|
-
import { ConnectionReuse } from "./connection-reuse.ts";
|
|
26
|
-
import { watchIdle, type IdleWatch } from "./stream-idle.ts";
|
|
27
|
-
|
|
28
|
-
import { HttpError, StreamError } from "./openai.ts";
|
|
29
|
-
import type { UsageSample } from "./usage.ts";
|
|
30
|
-
import { disablePayloadTools, type ProviderSendOptions } from "./provider-tool-policy.ts";
|
|
31
|
-
|
|
32
|
-
const APIS: Record<Api, ProviderStreams> = {
|
|
33
|
-
"anthropic-messages": anthropicMessagesApi(), "openai-completions": openAICompletionsApi(),
|
|
34
|
-
"openai-responses": openAIResponsesApi(), "azure-openai-responses": azureOpenAIResponsesApi(),
|
|
35
|
-
"openai-codex-responses": openAICodexResponsesApi(), "google-generative-ai": googleGenerativeAIApi(),
|
|
36
|
-
"google-vertex": googleVertexApi(), "bedrock-converse-stream": bedrockConverseStreamApi(),
|
|
37
|
-
"mistral-conversations": mistralConversationsApi(), "pi-messages": piMessagesApi(),
|
|
38
|
-
};
|
|
39
|
-
function record(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
40
|
-
const EMPTY_USAGE: AssistantMessage["usage"] = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
|
|
41
|
-
|
|
42
|
-
/** D11: SDK defaults cannot certify a receipt; native input/output counters can, including measured zeros. */
|
|
43
|
-
function nativeUsageReported(api: Api, event: unknown): boolean {
|
|
44
|
-
if (!record(event)) return false;
|
|
45
|
-
let usage: unknown;
|
|
46
|
-
if (api.includes("responses")) {
|
|
47
|
-
if (event["type"] !== "response.completed" && event["type"] !== "response.incomplete") return false;
|
|
48
|
-
usage = record(event["response"]) ? event["response"]["usage"] : undefined;
|
|
49
|
-
} else if (api === "anthropic-messages") {
|
|
50
|
-
if (event["type"] === "message_start") usage = record(event["message"]) ? event["message"]["usage"] : undefined;
|
|
51
|
-
else if (event["type"] === "message_delta") usage = event["usage"];
|
|
52
|
-
else return false;
|
|
53
|
-
} else if (api === "openai-completions" || api === "mistral-conversations") usage = event["usage"];
|
|
54
|
-
else return false;
|
|
55
|
-
if (!record(usage)) return false;
|
|
56
|
-
const fields = api === "openai-completions" || api === "mistral-conversations" ? ["prompt_tokens", "completion_tokens"] : ["input_tokens", "output_tokens"];
|
|
57
|
-
return fields.some(field => typeof usage[field] === "number" && Number.isSafeInteger(usage[field]) && usage[field] >= 0);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** O.3: Pi's streaming repair is presentation, never evidence that final arguments were valid JSON. */
|
|
61
|
-
class OriginalToolArguments {
|
|
62
|
-
readonly values = new Map<string, string>();
|
|
63
|
-
private readonly slots = new Map<string, { id: unknown; argumentId: string | null; raw: string; position: number }>();
|
|
64
|
-
private readonly faults = new Set<string>();
|
|
65
|
-
private readonly api: Api;
|
|
66
|
-
constructor(api: Api) { this.api = api; }
|
|
67
|
-
|
|
68
|
-
private slotKey(index: unknown, id?: unknown): string | null {
|
|
69
|
-
if (typeof index === "number" && Number.isSafeInteger(index) && index >= 0) return `index:${index}`;
|
|
70
|
-
// Mistral accepts a complete call keyed by ID without an index. A fragment
|
|
71
|
-
// with neither cannot be assigned to a call without inventing its identity.
|
|
72
|
-
if (this.api === "mistral-conversations" && typeof id === "string" && id.trim() && id !== "null") return `id:${id}`;
|
|
73
|
-
this.faults.add("native tool call has no stable stream slot");
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private slot(key: string, id: unknown, argumentId: string | null, supplied: boolean) {
|
|
78
|
-
const existing = this.slots.get(key);
|
|
79
|
-
if (existing) {
|
|
80
|
-
if (supplied && id !== existing.id) this.faults.add(`native call ID changed in stream slot ${existing.position}`);
|
|
81
|
-
if (argumentId !== null && argumentId !== existing.argumentId) this.faults.add(`native argument identity changed in stream slot ${existing.position}`);
|
|
82
|
-
return existing;
|
|
83
|
-
}
|
|
84
|
-
const slot = { id, argumentId, raw: "", position: this.slots.size };
|
|
85
|
-
this.slots.set(key, slot);
|
|
86
|
-
return slot;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private publish(slot: { argumentId: string | null; raw: string }): void {
|
|
90
|
-
if (slot.argumentId !== null) this.values.set(slot.argumentId, slot.raw);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
observe(value: unknown): void {
|
|
94
|
-
if (!record(value)) return;
|
|
95
|
-
if (this.api.includes("responses")) {
|
|
96
|
-
const item = value["item"];
|
|
97
|
-
if (record(item) && (item["type"] === "function_call" || item["type"] === "custom_tool_call")) {
|
|
98
|
-
const key = this.slotKey(value["output_index"]);
|
|
99
|
-
if (key === null) return;
|
|
100
|
-
const id = item["call_id"], argumentId = typeof id === "string" ? `${id}|${String(item["id"])}` : null;
|
|
101
|
-
const slot = this.slot(key, id, argumentId, true);
|
|
102
|
-
if (item["type"] === "function_call") {
|
|
103
|
-
if (typeof item["arguments"] === "string") slot.raw = item["arguments"];
|
|
104
|
-
this.publish(slot);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const slot = typeof value["output_index"] === "number" ? this.slots.get(`index:${value["output_index"]}`) : undefined;
|
|
108
|
-
if (slot && value["type"] === "response.function_call_arguments.delta" && typeof value["delta"] === "string") slot.raw += value["delta"];
|
|
109
|
-
if (slot && value["type"] === "response.function_call_arguments.done" && typeof value["arguments"] === "string") slot.raw = value["arguments"];
|
|
110
|
-
if (slot && ["response.function_call_arguments.delta", "response.function_call_arguments.done"].includes(String(value["type"]))) this.publish(slot);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (this.api === "anthropic-messages") {
|
|
114
|
-
const block = value["content_block"];
|
|
115
|
-
const index = value["index"];
|
|
116
|
-
if (record(block) && block["type"] === "tool_use") {
|
|
117
|
-
const key = this.slotKey(index);
|
|
118
|
-
if (key === null) return;
|
|
119
|
-
const id = block["id"], slot = this.slot(key, id, typeof id === "string" ? id : null, true);
|
|
120
|
-
if (slot.argumentId !== null) this.values.set(slot.argumentId, JSON.stringify(block["input"] ?? {}));
|
|
121
|
-
}
|
|
122
|
-
const slot = typeof index === "number" ? this.slots.get(`index:${index}`) : undefined, delta = value["delta"];
|
|
123
|
-
if (slot && record(delta) && delta["type"] === "input_json_delta" && typeof delta["partial_json"] === "string") {
|
|
124
|
-
slot.raw += delta["partial_json"];
|
|
125
|
-
this.publish(slot);
|
|
126
|
-
}
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
// Mistral's native adapter uses the same completions stream vocabulary.
|
|
130
|
-
if (this.api !== "openai-completions" && this.api !== "mistral-conversations") return;
|
|
131
|
-
const choice = Array.isArray(value["choices"]) ? value["choices"][0] : undefined;
|
|
132
|
-
const delta = record(choice) && record(choice["delta"]) ? choice["delta"] : undefined;
|
|
133
|
-
if (!delta || !Array.isArray(delta["tool_calls"])) return;
|
|
134
|
-
// One native delta contains one entry per slot. Continuations may reuse a
|
|
135
|
-
// slot in later chunks, but merging two entries here could execute neither
|
|
136
|
-
// original declaration's arguments under a silently collapsed identity.
|
|
137
|
-
const chunkSlots = new Set<string>();
|
|
138
|
-
for (const call of delta["tool_calls"]) {
|
|
139
|
-
if (!record(call)) continue;
|
|
140
|
-
const id = call["id"], key = this.slotKey(call["index"], id);
|
|
141
|
-
if (key === null) continue;
|
|
142
|
-
if (chunkSlots.has(key)) { this.faults.add("native stream chunk repeats a tool call slot"); continue; }
|
|
143
|
-
chunkSlots.add(key);
|
|
144
|
-
// Native continuation chunks may spell omitted IDs as null. An indexed
|
|
145
|
-
// slot retains its original identity; a first slot still needs a real ID.
|
|
146
|
-
const supplied = id !== null && id !== undefined;
|
|
147
|
-
const slot = this.slot(key, id, typeof id === "string" ? id : null, supplied);
|
|
148
|
-
if (!record(call["function"])) continue;
|
|
149
|
-
const fragment = call["function"]["arguments"];
|
|
150
|
-
if (typeof fragment === "string") slot.raw += fragment;
|
|
151
|
-
else if (this.api === "mistral-conversations" && fragment !== undefined) slot.raw += JSON.stringify(fragment);
|
|
152
|
-
this.publish(slot);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
identityErrors(): string[] {
|
|
157
|
-
// D5: SDK block IDs may merge Completions calls or replace missing Mistral
|
|
158
|
-
// IDs. Only the protocol's original slots can prove one receipt per call.
|
|
159
|
-
const native = [...this.slots.values()].map(slot => slot.id);
|
|
160
|
-
const malformed = native.flatMap((id, index) => typeof id === "string" && !(this.api === "mistral-conversations" && id === "null")
|
|
161
|
-
? [] : [`native call ID at slot ${index} must be a real string identity`]);
|
|
162
|
-
const calls: AssistantBlock[] = native.filter((id): id is string => typeof id === "string")
|
|
163
|
-
.map(call_id => ({ type: "tool_use", call_id, name: "", input: null }));
|
|
164
|
-
return [...this.faults, ...malformed, ...toolCallIdentityViolations(calls).map(violation => violation.detail)];
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Complete usage survives a rejected response without making malformed calls durable conversation history. */
|
|
169
|
-
export class ProviderIntegrityError extends FailedResponseError {
|
|
170
|
-
readonly code = "invalid_tool_call_identity";
|
|
171
|
-
constructor(details: readonly string[], usage?: UsageSample) {
|
|
172
|
-
super(`Invalid provider tool call identity: ${details.join("; ")}`, usage);
|
|
173
|
-
this.name = "ProviderIntegrityError";
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Native envelopes keep their SDK-owned tools/system/signal when adding nested generation options. */
|
|
178
|
-
function mergePayload(payload: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
|
|
179
|
-
const next = { ...payload };
|
|
180
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
181
|
-
const current = next[key];
|
|
182
|
-
next[key] = record(current) && record(value) ? mergePayload(current, value) : value;
|
|
183
|
-
}
|
|
184
|
-
return next;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Google exposes structured HTTP failures as JSON in its public stream error, without a fetch hook. */
|
|
188
|
-
function nativeStatus(message: string): number | undefined {
|
|
189
|
-
try {
|
|
190
|
-
const body: unknown = JSON.parse(message);
|
|
191
|
-
const code = record(body) && record(body["error"]) ? body["error"]["code"] : undefined;
|
|
192
|
-
return typeof code === "number" && Number.isInteger(code) && code >= 400 && code <= 599 ? code : undefined;
|
|
193
|
-
} catch { return undefined; }
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function piModel(spec: ModelSpec, baseUrl: string): Model<Api> {
|
|
197
|
-
return { id: spec.wireId ?? spec.id, provider: spec.provider, name: spec.name, api: spec.api, baseUrl,
|
|
198
|
-
reasoning: spec.reasoning, input: [...(spec.input ?? ["text"])],
|
|
199
|
-
// Required SDK metadata defaults are operational only; xeno still reports absent prices as unknown.
|
|
200
|
-
contextWindow: spec.contextWindow ?? 131_072, maxTokens: spec.maxTokens ?? 16_000,
|
|
201
|
-
cost: { input: spec.price?.input ?? 0, output: spec.price?.output ?? 0, cacheRead: spec.price?.cacheRead ?? 0, cacheWrite: spec.price?.cacheWrite ?? 0 },
|
|
202
|
-
...(spec.thinkingLevelMap ? { thinkingLevelMap: spec.thinkingLevelMap } : {}),
|
|
203
|
-
...(spec.nativeCompat || spec.compat || spec.api === "anthropic-messages"
|
|
204
|
-
? { compat: { ...spec.compat, ...spec.nativeCompat, ...(spec.api === "anthropic-messages" ? { allowEmptySignature: true } : {}) } } : {}),
|
|
205
|
-
...(spec.samplingParams ? { samplingParams: spec.samplingParams } : {}) } as Model<Api>;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
type PiBlock = AssistantMessage["content"][number];
|
|
209
|
-
function thinkingOf(raw: unknown): Extract<PiBlock, { type: "thinking" }> | null {
|
|
210
|
-
if (!record(raw)) return null;
|
|
211
|
-
if (record(raw["pi"]) && raw["pi"]["type"] === "thinking" && typeof raw["pi"]["thinking"] === "string") {
|
|
212
|
-
return { ...structuredClone(raw["pi"]), type: "thinking", thinking: raw["pi"]["thinking"] };
|
|
213
|
-
}
|
|
214
|
-
if (raw["type"] === "thinking") return { type: "thinking", thinking: typeof raw["thinking"] === "string" ? raw["thinking"] : "", ...(typeof raw["signature"] === "string" ? { thinkingSignature: raw["signature"] } : {}) };
|
|
215
|
-
if (raw["type"] === "redacted_thinking" && typeof raw["data"] === "string") return { type: "thinking", thinking: "", redacted: true, thinkingSignature: raw["data"] };
|
|
216
|
-
if (record(raw["responses_item"])) return { type: "thinking", thinking: "", thinkingSignature: JSON.stringify(raw["responses_item"]) };
|
|
217
|
-
if (typeof raw["reasoning_content"] === "string") return { type: "thinking", thinking: raw["reasoning_content"], ...(raw["reasoning_details"] !== undefined ? { thinkingSignature: JSON.stringify(raw["reasoning_details"]) } : {}) };
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Reasoning never enters the visible channel. Pi turns thinking it cannot replay natively into a
|
|
223
|
-
* text part, and a model shown its own reasoning as earlier visible output starts writing its
|
|
224
|
-
* reasoning as visible output (an unsigned Gemini block via an Anthropic gateway snowballed into
|
|
225
|
-
* monologue in every later reply). An unsigned block here was sent that way by the server --
|
|
226
|
-
* interrupted streams throw and are never logged -- so anthropic-messages replays it verbatim
|
|
227
|
-
* (`allowEmptySignature` in piModel, as the native adapter does). Bedrock has no unsigned
|
|
228
|
-
* reasoning form, so there the block is dropped. Cross-model text conversion cannot occur: a
|
|
229
|
-
* model switch cuts the node (Session.switchModel) and every replayed message names this model.
|
|
230
|
-
*/
|
|
231
|
-
function replaysAsReasoning(api: Api, block: Extract<PiBlock, { type: "thinking" }>): boolean {
|
|
232
|
-
return api !== "bedrock-converse-stream" || block.redacted === true || !!block.thinkingSignature?.trim();
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** Pure conversion of an already hydrated projection; no second transcript is retained. */
|
|
236
|
-
export function piContext(frozen: FrozenPrefix, messages: readonly Message[], spec: ModelSpec): Context {
|
|
237
|
-
const out: PiMessage[] = [];
|
|
238
|
-
const names = new Map<string, string>();
|
|
239
|
-
const ids = new Map<string, string>();
|
|
240
|
-
for (const message of messages) {
|
|
241
|
-
if (message.role === "assistant") {
|
|
242
|
-
const content: PiBlock[] = [];
|
|
243
|
-
for (const block of message.content) {
|
|
244
|
-
switch (block.type) {
|
|
245
|
-
case "text": {
|
|
246
|
-
const wire = record(block.wire) ? block.wire : {};
|
|
247
|
-
content.push({ type: "text", text: block.text, ...(typeof wire["textSignature"] === "string" ? { textSignature: wire["textSignature"] } : {}) }); break;
|
|
248
|
-
}
|
|
249
|
-
case "thinking": { const thinking = thinkingOf(block.raw); if (thinking && replaysAsReasoning(spec.api, thinking)) content.push(thinking); break; }
|
|
250
|
-
case "tool_use": {
|
|
251
|
-
names.set(block.id, block.name);
|
|
252
|
-
const wire = record(block.wire) ? block.wire : {};
|
|
253
|
-
const native = record(wire["pi"]) ? wire["pi"] : {};
|
|
254
|
-
const extra = record(wire["extra_content"]) && record(wire["extra_content"]["google"]) ? wire["extra_content"]["google"] : {};
|
|
255
|
-
const signature = native["thoughtSignature"] ?? extra["thought_signature"];
|
|
256
|
-
const id = typeof wire["item_id"] === "string" && spec.api.includes("responses") ? `${block.id}|${wire["item_id"]}` : block.id;
|
|
257
|
-
ids.set(block.id, id);
|
|
258
|
-
content.push({ type: "toolCall", id, name: block.name, arguments: structuredClone(block.input) as Extract<PiBlock, { type: "toolCall" }>["arguments"],
|
|
259
|
-
...(typeof signature === "string" ? { thoughtSignature: signature } : {}), ...(typeof native["namespace"] === "string" ? { namespace: native["namespace"] } : {}) }); break;
|
|
260
|
-
}
|
|
261
|
-
case "tool_result": case "image": break;
|
|
262
|
-
default: assertNever(block, "pi assistant block");
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
out.push({ role: "assistant", content, api: spec.api, provider: spec.provider, model: spec.wireId ?? spec.id,
|
|
266
|
-
timestamp: 0, usage: structuredClone(EMPTY_USAGE), stopReason: content.some(block => block.type === "toolCall") ? "toolUse" : "stop" });
|
|
267
|
-
} else {
|
|
268
|
-
let content: Extract<PiMessage, { role: "user" }>["content"] = [];
|
|
269
|
-
let result: Extract<PiMessage, { role: "toolResult" }> | undefined;
|
|
270
|
-
const flush = () => { if (content.length) out.push({ role: "user", content, timestamp: 0 }); content = []; };
|
|
271
|
-
for (const block of message.content) {
|
|
272
|
-
if (block.type === "tool_result") {
|
|
273
|
-
flush(); result = { role: "toolResult", toolCallId: ids.get(block.tool_use_id) ?? block.tool_use_id, toolName: names.get(block.tool_use_id) ?? "tool",
|
|
274
|
-
content: [{ type: "text", text: block.content }], isError: block.is_error ?? false, timestamp: 0 };
|
|
275
|
-
out.push(result);
|
|
276
|
-
} else if (block.type === "text") { content.push({ type: "text", text: block.text }); result = undefined; }
|
|
277
|
-
else if (block.type === "image") {
|
|
278
|
-
// D5: the projection puts each image after its paired text receipt. Keep that ownership;
|
|
279
|
-
// Pi emits complete result batches before fallback user images when a protocol requires it.
|
|
280
|
-
const image = { type: "image" as const, data: imageData(block), mimeType: block.image.mimeType };
|
|
281
|
-
if (result) result.content.push(image); else content.push(image);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
flush();
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return { systemPrompt: frozen.system, messages: out, tools: frozen.tools.map(value => {
|
|
288
|
-
const schema = value as ToolSchema;
|
|
289
|
-
return { name: schema.name, description: schema.description, parameters: schema.input_schema as Tool["parameters"] };
|
|
290
|
-
}) };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export function piBlocks(message: AssistantMessage, originalArguments: ReadonlyMap<string, string> = new Map()): AssistantBlock[] {
|
|
294
|
-
return message.content.map((block): AssistantBlock => {
|
|
295
|
-
switch (block.type) {
|
|
296
|
-
case "text": return { type: "text", text: block.text, ...(block.textSignature ? { wire: { textSignature: block.textSignature } } : {}) };
|
|
297
|
-
case "thinking": {
|
|
298
|
-
let raw: unknown = { pi: structuredClone(block) };
|
|
299
|
-
if (message.api === "anthropic-messages") raw = block.redacted ? { type: "redacted_thinking", data: block.thinkingSignature }
|
|
300
|
-
: { type: "thinking", thinking: block.thinking, signature: block.thinkingSignature ?? "" };
|
|
301
|
-
else if (message.api.includes("responses") && block.thinkingSignature) { try { raw = { responses_item: JSON.parse(block.thinkingSignature) }; } catch { /* An opaque signature is still preserved by the envelope. */ } }
|
|
302
|
-
else if (message.api === "openai-completions") {
|
|
303
|
-
let details: unknown;
|
|
304
|
-
try { if (block.thinkingSignature) details = JSON.parse(block.thinkingSignature); } catch { /* The Pi envelope preserves non-JSON signatures. */ }
|
|
305
|
-
raw = { pi: structuredClone(block), reasoning_content: block.thinking, ...(details !== undefined ? { reasoning_details: details } : {}) };
|
|
306
|
-
}
|
|
307
|
-
return { type: "thinking", text: block.thinking, raw };
|
|
308
|
-
}
|
|
309
|
-
case "toolCall": return { type: "tool_use", call_id: block.id, name: block.name,
|
|
310
|
-
input: originalArguments.has(block.id) ? completedToolInput(originalArguments.get(block.id)!) : structuredClone(block.arguments),
|
|
311
|
-
...(block.thoughtSignature || block.namespace ? { wire: { pi: { thoughtSignature: block.thoughtSignature, namespace: block.namespace } } } : {}) };
|
|
312
|
-
default: return assertNever(block, "pi response block");
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export class PiProvider implements Provider {
|
|
318
|
-
readonly config: ProviderConfig;
|
|
319
|
-
private readonly apiKey: string;
|
|
320
|
-
private readonly connections = new ConnectionReuse();
|
|
321
|
-
private readonly options: {
|
|
322
|
-
fetch?: typeof fetch; effects?: ResolveEffects; oauth?: boolean; sessionId?: string;
|
|
323
|
-
retry?: RetryPolicy; onRetry?: (attempt: number, delayMs: number, error: unknown) => void;
|
|
324
|
-
/** Silence after which one attempt is abandoned; tests shorten it (stream-idle.ts). */
|
|
325
|
-
streamIdleMs?: number;
|
|
326
|
-
};
|
|
327
|
-
constructor(config: ProviderConfig, apiKey: string, options: PiProvider["options"] = {}) {
|
|
328
|
-
this.config = config; this.apiKey = apiKey; this.options = options;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
send(frozen: FrozenPrefix, messages: readonly Message[], node: number, signal?: AbortSignal, onDelta?: OnDelta, policy?: ProviderSendOptions): Promise<Turn> {
|
|
332
|
-
// Bedrock's Pi adapter leaves retry ownership with the AWS SDK; never multiply that loop.
|
|
333
|
-
const retry = this.options.retry ?? (this.config.spec.api === "bedrock-converse-stream" ? { ...DEFAULT_RETRY, maxRetries: 0 } : DEFAULT_RETRY);
|
|
334
|
-
return withRetry(() => watchIdle(signal, this.options.streamIdleMs, idle => this.attempt(frozen, messages, node, idle, signal, onDelta, policy)).catch((error: unknown) => { this.connections.note(error); throw error; }), retry, undefined,
|
|
335
|
-
(attempt, delayMs, error) => { onDelta?.({ kind: "discard" }); onDelta?.({ kind: "retry", attempt, delayMs, max: retry.maxRetries, reason: retryLabel(error) }); this.options.onRetry?.(attempt, delayMs, error); }, signal);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private async attempt(frozen: FrozenPrefix, messages: readonly Message[], node: number, idle: IdleWatch, signal?: AbortSignal, onDelta?: OnDelta, policy?: ProviderSendOptions): Promise<Turn> {
|
|
339
|
-
signal?.throwIfAborted();
|
|
340
|
-
const started = Date.now();
|
|
341
|
-
// Wrapped innermost: providerRequest rebuilds the call as a Request and
|
|
342
|
-
// passes no init, so a keepalive set any further out would be dropped.
|
|
343
|
-
const request = providerRequest(this.config.spec, this.apiKey, this.options.effects,
|
|
344
|
-
this.connections.wrap(this.options.fetch ?? globalThis.fetch), this.options.oauth);
|
|
345
|
-
const model = piModel(this.config.spec, request.baseUrl);
|
|
346
|
-
let status: number | undefined;
|
|
347
|
-
let responseHeaders = new Headers();
|
|
348
|
-
const observedFetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
349
|
-
const response = await request.fetch(input, init);
|
|
350
|
-
status = response.status; responseHeaders = new Headers(response.headers);
|
|
351
|
-
return response;
|
|
352
|
-
}) as typeof fetch;
|
|
353
|
-
const nativeHttp = ["google-generative-ai", "google-vertex", "bedrock-converse-stream"].includes(model.api);
|
|
354
|
-
const originalArguments = new OriginalToolArguments(model.api);
|
|
355
|
-
let hasKnownUsage = false;
|
|
356
|
-
const options: SimpleStreamOptions = { ...request.options, ...(request.key ? { apiKey: request.key } : {}),
|
|
357
|
-
// Cache routing follows a root session and its forks without introducing server transcript state (D8).
|
|
358
|
-
...(this.options.sessionId === undefined ? {} : { sessionId: this.options.sessionId }),
|
|
359
|
-
env: request.env, headers: request.headers, ...(nativeHttp ? {} : { fetch: observedFetch }), maxRetries: 0, transport: "sse",
|
|
360
|
-
maxTokens: this.config.maxTokens, ...(this.config.effort !== "off" ? { reasoning: this.config.effort } : {}),
|
|
361
|
-
signal: idle.signal, onResponse: response => { idle.touch(); status = response.status; responseHeaders = new Headers(response.headers); },
|
|
362
|
-
onProviderStreamEvent: event => {
|
|
363
|
-
idle.touch();
|
|
364
|
-
originalArguments.observe(event);
|
|
365
|
-
hasKnownUsage ||= nativeUsageReported(model.api, event);
|
|
366
|
-
},
|
|
367
|
-
onPayload: payload => {
|
|
368
|
-
if (!record(payload)) return payload;
|
|
369
|
-
const next = mergePayload(payload, request.body);
|
|
370
|
-
// Stateless Responses is required by D8: never replace the recomputable projection with server state.
|
|
371
|
-
if (model.api.includes("responses")) { next["store"] = false; delete next["previous_response_id"]; }
|
|
372
|
-
return policy?.toolUse === "disabled" ? disablePayloadTools(model.api, next) : next;
|
|
373
|
-
} };
|
|
374
|
-
let completed: AssistantMessage | undefined;
|
|
375
|
-
// Pi emits fragments; xeno's progress contract replaces the displayed argument buffer.
|
|
376
|
-
// Keep buffers per content block so interleaved calls and retries cannot share partial JSON.
|
|
377
|
-
const toolArguments = new Map<number, string>();
|
|
378
|
-
for await (const event of idle.events(APIS[model.api].streamSimple(model, normalizeContext(piContext(frozen, messages, this.config.spec)), options))) {
|
|
379
|
-
switch (event.type) {
|
|
380
|
-
case "text_delta": onDelta?.({ kind: "text", text: event.delta }); break;
|
|
381
|
-
case "thinking_delta": onDelta?.({ kind: "thinking", text: event.delta }); break;
|
|
382
|
-
case "toolcall_start": {
|
|
383
|
-
const block = event.partial.content[event.contentIndex];
|
|
384
|
-
toolArguments.set(event.contentIndex, "");
|
|
385
|
-
if (block?.type === "toolCall") onDelta?.({ kind: "tool", id: block.id, name: block.name, arguments: "" }); break;
|
|
386
|
-
}
|
|
387
|
-
case "toolcall_delta": {
|
|
388
|
-
const block = event.partial.content[event.contentIndex];
|
|
389
|
-
const argumentsText = (toolArguments.get(event.contentIndex) ?? "") + event.delta;
|
|
390
|
-
toolArguments.set(event.contentIndex, argumentsText);
|
|
391
|
-
if (block?.type === "toolCall") onDelta?.({ kind: "tool", id: block.id, name: block.name, arguments: argumentsText }); break;
|
|
392
|
-
}
|
|
393
|
-
case "toolcall_end": {
|
|
394
|
-
const block = event.toolCall;
|
|
395
|
-
onDelta?.({ kind: "tool", id: block.id, name: block.name, arguments: JSON.stringify(block.arguments) });
|
|
396
|
-
toolArguments.delete(event.contentIndex); break;
|
|
397
|
-
}
|
|
398
|
-
case "done": completed = event.message; break;
|
|
399
|
-
case "error":
|
|
400
|
-
if (event.reason === "aborted" || signal?.aborted) throw new DOMException("Request aborted", "AbortError");
|
|
401
|
-
status ??= nativeStatus(event.error.errorMessage ?? "");
|
|
402
|
-
if (status && status >= 400) {
|
|
403
|
-
// The proxy's own text says "run `grok update`", which a xeno user cannot act on.
|
|
404
|
-
const hint = grokVersionHint(status, this.config.spec.provider);
|
|
405
|
-
throw new HttpError(status, responseHeaders, (event.error.errorMessage ?? "Provider request failed") + (hint ? `\n ${hint}` : ""));
|
|
406
|
-
}
|
|
407
|
-
throw new StreamError(event.error.errorMessage ?? "Provider stream failed");
|
|
408
|
-
case "start": case "text_start": case "text_end": case "thinking_start": case "thinking_end": break;
|
|
409
|
-
default: assertNever(event, "pi stream event");
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
signal?.throwIfAborted();
|
|
413
|
-
if (!completed || completed.stopReason === "deferred") throw new StreamError("Provider did not complete a synchronous agent turn");
|
|
414
|
-
const usage: UsageSample = { input_tokens: completed.usage.input, output_tokens: completed.usage.output,
|
|
415
|
-
cache_read_input_tokens: completed.usage.cacheRead, cache_creation_input_tokens: completed.usage.cacheWrite, node, ms: Date.now() - started };
|
|
416
|
-
const identityErrors = originalArguments.identityErrors();
|
|
417
|
-
if (identityErrors.length) throw new ProviderIntegrityError(identityErrors, hasKnownUsage ? usage : undefined);
|
|
418
|
-
// The SDK has already classified native STOP/end_turn/toolUse/length values. The loop consumes
|
|
419
|
-
// xeno's closed completion vocabulary, never a raw native spelling that could skip tool execution.
|
|
420
|
-
return coherentTurn({ blocks: piBlocks(completed, originalArguments.values), stopReason: completed.stopReason === "length" ? "max_tokens" : completed.stopReason === "toolUse" ? "tool_use" : "end_turn",
|
|
421
|
-
usage }, hasKnownUsage);
|
|
422
|
-
}
|
|
423
|
-
}
|