@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,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The current request's operations outside the workspace, read from the log
|
|
3
|
-
* when a request ends (feat-external-effects-summary). The derivation is the
|
|
4
|
-
* pure core function; this file only supplies what core may not read: the
|
|
5
|
-
* home directory and the platform's temporary directory.
|
|
6
|
-
* Nothing is recorded -- the list is a projection and is recomputed on demand.
|
|
7
|
-
*/
|
|
8
|
-
import { realpathSync } from "node:fs";
|
|
9
|
-
import { homedir, tmpdir } from "node:os";
|
|
10
|
-
import { externalEffects, type ExternalEffect, type ExternalOptions } from "../core/external-effects.ts";
|
|
11
|
-
import { currentRequestEvents } from "../core/request-state.ts";
|
|
12
|
-
import type { Session } from "./loop.ts";
|
|
13
|
-
|
|
14
|
-
/** macOS reports /var/folders/... while processes see /private/var/folders/...; both are temporary. */
|
|
15
|
-
export function hostExternalOptions(): ExternalOptions {
|
|
16
|
-
const temp = tmpdir();
|
|
17
|
-
let real = temp;
|
|
18
|
-
try { real = realpathSync(temp); } catch { /* an unreadable temp root is still the reported one */ }
|
|
19
|
-
return { home: homedir(), temp: [...new Set([temp, real])] };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Synchronous on purpose: SessionControl computes this between the turn's last
|
|
24
|
-
* event and releasing the turn, and an await there lets a disposal observe a
|
|
25
|
-
* turn that has already ended. The cost is one known gap: a request that
|
|
26
|
-
* crossed a segment boundary lists only what ran after the last cut (D8 keeps
|
|
27
|
-
* the earlier part in the sealed segment, not in memory). Silence, not a guess.
|
|
28
|
-
*/
|
|
29
|
-
export function requestExternalEffects(session: Session, options: ExternalOptions = hostExternalOptions()): ExternalEffect[] {
|
|
30
|
-
return externalEffects(currentRequestEvents(session.state.events), session.config.tools.root, options);
|
|
31
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ask the npm registry for the newest published xeno (docs/tasks/feat-update-check.md).
|
|
3
|
-
*
|
|
4
|
-
* One small GET (the dist-tags document is about 20 bytes) with a 3-second
|
|
5
|
-
* timeout, falling back to the package's `latest` document. It carries
|
|
6
|
-
* Bun's default User-Agent and nothing that identifies the person: the
|
|
7
|
-
* registry sees an IP address, the time and the package name. Failure of
|
|
8
|
-
* any kind is "no answer"; callers decide whether an explicit check needs feedback.
|
|
9
|
-
*/
|
|
10
|
-
import { distTagsUrl } from "../core/update-check.ts";
|
|
11
|
-
import { parseVersion } from "../core/version.ts";
|
|
12
|
-
|
|
13
|
-
/** How long to wait for the registry. Codex and pi wait longer; startup never waits on it anyway. Assumed. */
|
|
14
|
-
export const UPDATE_TIMEOUT_MS = 3000;
|
|
15
|
-
|
|
16
|
-
type Fetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
17
|
-
|
|
18
|
-
export interface LatestVersionOptions {
|
|
19
|
-
/**
|
|
20
|
-
* Ends the request early. The interactive app aborts it on close: an open
|
|
21
|
-
* socket keeps the process alive, so quitting would otherwise wait out the
|
|
22
|
-
* timeout (feat-update-check DoD 3).
|
|
23
|
-
*/
|
|
24
|
-
readonly signal?: AbortSignal;
|
|
25
|
-
readonly fetcher?: Fetch;
|
|
26
|
-
readonly timeoutMs?: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function fetchLatestVersion(registry: string | undefined, options: LatestVersionOptions = {}): Promise<string | null> {
|
|
30
|
-
const fetcher = options.fetcher ?? ((input, init) => fetch(input, init));
|
|
31
|
-
const timeout = AbortSignal.timeout(options.timeoutMs ?? UPDATE_TIMEOUT_MS);
|
|
32
|
-
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
33
|
-
const tags = distTagsUrl(registry);
|
|
34
|
-
const attempts: [string, (body: Record<string, unknown>) => unknown][] = [
|
|
35
|
-
[tags, body => body["latest"]],
|
|
36
|
-
// Mirrors that do not serve dist-tags still serve the package's latest document.
|
|
37
|
-
[tags.replace(/\/-\/package\/([^/]+)\/dist-tags$/, "/$1/latest"), body => body["version"]],
|
|
38
|
-
];
|
|
39
|
-
for (const [url, pick] of attempts) {
|
|
40
|
-
try {
|
|
41
|
-
const response = await fetcher(url, { signal, headers: { accept: "application/json" } });
|
|
42
|
-
if (!response.ok) continue;
|
|
43
|
-
const value = pick(await response.json() as Record<string, unknown>);
|
|
44
|
-
if (typeof value === "string" && value.length <= 64 && parseVersion(value)) return value.trim();
|
|
45
|
-
} catch { if (signal.aborted) return null; }
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
package/src/runtime/usage.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-request usage accounting.
|
|
3
|
-
*
|
|
4
|
-
* This exists to answer the question the whole design rests on: is the prompt
|
|
5
|
-
* prefix actually stable? The core's claim is that appending within a node
|
|
6
|
-
* only appends, so every turn should read the entire accumulated prefix from
|
|
7
|
-
* cache and write only what the last turn added. That is checkable, and the
|
|
8
|
-
* shape of a healthy loop is specific:
|
|
9
|
-
*
|
|
10
|
-
* cache_read grows turn over turn and covers the whole prior prefix
|
|
11
|
-
* cache_creation is small -- only the delta
|
|
12
|
-
* input_tokens (uncached) stays near zero after the first turn
|
|
13
|
-
*
|
|
14
|
-
* A cache_read that sits at zero across turns with the same prefix means a
|
|
15
|
-
* silent invalidator got in. Since that failure costs money without ever
|
|
16
|
-
* raising an error, measuring it is the only way to know.
|
|
17
|
-
*
|
|
18
|
-
* Pure: the aggregator takes samples and returns totals. The caller records
|
|
19
|
-
* the timestamps.
|
|
20
|
-
*
|
|
21
|
-
* The field names below are a NORMALISED internal vocabulary, not one
|
|
22
|
-
* provider's wire format. Every adapter maps onto them: a provider that bills
|
|
23
|
-
* nothing for cache writes reports 0 there rather than leaving the field to
|
|
24
|
-
* mean something else.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import type { CacheKind, CacheRates, Price } from "./model.ts";
|
|
28
|
-
|
|
29
|
-
/** What one API response reports, normalised by the adapter. */
|
|
30
|
-
export interface UsageSample {
|
|
31
|
-
input_tokens: number;
|
|
32
|
-
output_tokens: number;
|
|
33
|
-
cache_creation_input_tokens: number;
|
|
34
|
-
cache_read_input_tokens: number;
|
|
35
|
-
/** Which node the request belonged to, so per-node costs stay separable. */
|
|
36
|
-
node: number;
|
|
37
|
-
/** Wall-clock milliseconds for the request. */
|
|
38
|
-
ms: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface UsageTotals {
|
|
42
|
-
requests: number;
|
|
43
|
-
input_tokens: number;
|
|
44
|
-
output_tokens: number;
|
|
45
|
-
cache_creation_input_tokens: number;
|
|
46
|
-
cache_read_input_tokens: number;
|
|
47
|
-
ms: number;
|
|
48
|
-
/**
|
|
49
|
-
* Total prompt size is the sum of all three input fields -- `input_tokens`
|
|
50
|
-
* alone is only the uncached remainder. An agent that ran for hours can show
|
|
51
|
-
* 4K there while the real prefix was 200K.
|
|
52
|
-
*/
|
|
53
|
-
prompt_tokens: number;
|
|
54
|
-
/**
|
|
55
|
-
* Fraction of prompt tokens served from cache. The number that says whether
|
|
56
|
-
* the append-only property is holding in practice.
|
|
57
|
-
*/
|
|
58
|
-
cache_hit_rate: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function emptyTotals(): UsageTotals {
|
|
62
|
-
return {
|
|
63
|
-
requests: 0,
|
|
64
|
-
input_tokens: 0,
|
|
65
|
-
output_tokens: 0,
|
|
66
|
-
cache_creation_input_tokens: 0,
|
|
67
|
-
cache_read_input_tokens: 0,
|
|
68
|
-
ms: 0,
|
|
69
|
-
prompt_tokens: 0,
|
|
70
|
-
cache_hit_rate: 0,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function totalUsage(samples: readonly UsageSample[]): UsageTotals {
|
|
75
|
-
const t = emptyTotals();
|
|
76
|
-
for (const s of samples) {
|
|
77
|
-
t.requests += 1;
|
|
78
|
-
t.input_tokens += s.input_tokens;
|
|
79
|
-
t.output_tokens += s.output_tokens;
|
|
80
|
-
t.cache_creation_input_tokens += s.cache_creation_input_tokens;
|
|
81
|
-
t.cache_read_input_tokens += s.cache_read_input_tokens;
|
|
82
|
-
t.ms += s.ms;
|
|
83
|
-
}
|
|
84
|
-
t.prompt_tokens =
|
|
85
|
-
t.input_tokens + t.cache_creation_input_tokens + t.cache_read_input_tokens;
|
|
86
|
-
t.cache_hit_rate =
|
|
87
|
-
t.prompt_tokens === 0 ? 0 : t.cache_read_input_tokens / t.prompt_tokens;
|
|
88
|
-
return t;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Below this a request's speed is not shown: a response that returns at once
|
|
93
|
-
* (a test double, a local cache) would divide into thousands of tokens per
|
|
94
|
-
* second. Assumed, not measured.
|
|
95
|
-
*/
|
|
96
|
-
export const SPEED_MIN_MS = 250;
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Output tokens per second over a set of model requests, or undefined when
|
|
100
|
-
* none can be measured (feat-sidebar-goal-and-speed). OpenCode's built-in
|
|
101
|
-
* turnTokensPerSecond uses the same definition: output over the time the
|
|
102
|
-
* requests took, each request counted once. Not output over the time since
|
|
103
|
-
* the first visible output -- output counts include reasoning that providers
|
|
104
|
-
* do not always stream (OpenAI summaries arrive late, Anthropic can omit
|
|
105
|
-
* them), so that would keep reasoning tokens and drop reasoning time, and
|
|
106
|
-
* overstate. Whole request time never overstates; an overloaded upstream
|
|
107
|
-
* reads slow, which is what the user is waiting on.
|
|
108
|
-
*/
|
|
109
|
-
export function generationSpeed(samples: readonly UsageSample[]): number | undefined {
|
|
110
|
-
let tokens = 0, ms = 0;
|
|
111
|
-
for (const sample of samples) {
|
|
112
|
-
if (sample.output_tokens <= 0 || !(sample.ms >= SPEED_MIN_MS)) continue;
|
|
113
|
-
tokens += sample.output_tokens;
|
|
114
|
-
ms += sample.ms;
|
|
115
|
-
}
|
|
116
|
-
return ms > 0 ? tokens / (ms / 1000) : undefined;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Cost in equivalent base-input tokens, the unit appendix A's model uses.
|
|
121
|
-
*
|
|
122
|
-
* Reported as a multiplier rather than dollars so it stays true across models
|
|
123
|
-
* and price changes. The rates come from the model rather than from this
|
|
124
|
-
* module: they are not a property of the unit, they are a property of the
|
|
125
|
-
* provider's contract, and they differ by an order of magnitude. Anthropic
|
|
126
|
-
* bills reads at 0.1x and 5-minute writes at 1.25x; DeepSeek bills reads at
|
|
127
|
-
* 0.033x and writes at nothing. Output is counted separately because its rate
|
|
128
|
-
* is a different multiple of input and varies by model.
|
|
129
|
-
*/
|
|
130
|
-
export interface EquivalentCost {
|
|
131
|
-
input_equivalent: number;
|
|
132
|
-
output_tokens: number;
|
|
133
|
-
/** What the same prompts would have cost with no caching at all. */
|
|
134
|
-
uncached_equivalent: number;
|
|
135
|
-
/** uncached / actual. 1.0 means caching bought nothing. */
|
|
136
|
-
savings_ratio: number;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Dollar cost of a session: complete, partial, or an explicit unknown.
|
|
141
|
-
*
|
|
142
|
-
* Unknown is a distinct member, not the number 0: a missing price times any
|
|
143
|
-
* token count is not "this session was free", and collapsing the two is how
|
|
144
|
-
* `/usage` would silently misreport every unpriced model.
|
|
145
|
-
*
|
|
146
|
-
* Partial is a distinct member, not `{kind:"usd", amount, note}`. A caller
|
|
147
|
-
* that only reads `amount` on `usd` must not be able to print a total that
|
|
148
|
-
* silently omitted cache spend as if the unknown rate were zero. `known`
|
|
149
|
-
* is the part we can prove; `uncounted` is why it is not a complete bill.
|
|
150
|
-
*/
|
|
151
|
-
export type ReportedCost =
|
|
152
|
-
| { kind: "unknown" }
|
|
153
|
-
| { kind: "usd"; amount: number }
|
|
154
|
-
| { kind: "partial"; known: number; uncounted: "cache" };
|
|
155
|
-
|
|
156
|
-
export function reportedCost(t: UsageTotals, price: Price | undefined): ReportedCost {
|
|
157
|
-
if (price === undefined) return { kind: "unknown" };
|
|
158
|
-
const known =
|
|
159
|
-
(t.input_tokens * price.input + t.output_tokens * price.output) / 1_000_000;
|
|
160
|
-
if (price.cacheRead !== undefined && price.cacheWrite !== undefined) {
|
|
161
|
-
const amount =
|
|
162
|
-
known +
|
|
163
|
-
(t.cache_read_input_tokens * price.cacheRead +
|
|
164
|
-
t.cache_creation_input_tokens * price.cacheWrite) /
|
|
165
|
-
1_000_000;
|
|
166
|
-
return { kind: "usd", amount };
|
|
167
|
-
}
|
|
168
|
-
// One or both cache rates are missing. Add the ones we have; never
|
|
169
|
-
// substitute 0 for the ones we do not — that is the low-bill path.
|
|
170
|
-
let extra = 0;
|
|
171
|
-
if (price.cacheRead !== undefined) {
|
|
172
|
-
extra += (t.cache_read_input_tokens * price.cacheRead) / 1_000_000;
|
|
173
|
-
}
|
|
174
|
-
if (price.cacheWrite !== undefined) {
|
|
175
|
-
extra += (t.cache_creation_input_tokens * price.cacheWrite) / 1_000_000;
|
|
176
|
-
}
|
|
177
|
-
return { kind: "partial", known: known + extra, uncounted: "cache" };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function equivalentCost(t: UsageTotals, rates: CacheRates): EquivalentCost {
|
|
181
|
-
const actual =
|
|
182
|
-
t.input_tokens +
|
|
183
|
-
t.cache_creation_input_tokens * rates.write +
|
|
184
|
-
t.cache_read_input_tokens * rates.read;
|
|
185
|
-
const uncached = t.prompt_tokens;
|
|
186
|
-
return {
|
|
187
|
-
input_equivalent: actual,
|
|
188
|
-
output_tokens: t.output_tokens,
|
|
189
|
-
uncached_equivalent: uncached,
|
|
190
|
-
savings_ratio: actual === 0 ? 1 : uncached / actual,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Per-turn diagnosis of whether the prefix held.
|
|
196
|
-
*
|
|
197
|
-
* `cache_creation` above the delta means something ahead of the new content
|
|
198
|
-
* changed, which is the signature of an invalidator rather than of growth.
|
|
199
|
-
*
|
|
200
|
-
* What counts as a symptom depends on who controls the cache. Under `explicit`
|
|
201
|
-
* caching the caller placed the breakpoint, so a miss on a follow-up request
|
|
202
|
-
* means the prefix moved and that is worth shouting about. Under `automatic`
|
|
203
|
-
* caching the provider decides: DeepSeek documents its cache as best-effort
|
|
204
|
-
* with no guaranteed hit rate and writes that "take seconds", so an early miss
|
|
205
|
-
* is the normal case. Reporting it would light a warning at the start of every
|
|
206
|
-
* session, and a warning that is always on stops being read.
|
|
207
|
-
*/
|
|
208
|
-
export type PrefixVerdict =
|
|
209
|
-
| { kind: "cold"; detail: string }
|
|
210
|
-
| { kind: "healthy"; read: number; wrote: number }
|
|
211
|
-
| { kind: "suspicious"; detail: string };
|
|
212
|
-
|
|
213
|
-
export function diagnosePrefix(
|
|
214
|
-
sample: UsageSample,
|
|
215
|
-
previous: UsageSample | undefined,
|
|
216
|
-
cache: CacheKind,
|
|
217
|
-
): PrefixVerdict {
|
|
218
|
-
if (!previous) {
|
|
219
|
-
return { kind: "cold", detail: "first request of the session; nothing to reuse yet" };
|
|
220
|
-
}
|
|
221
|
-
if (sample.cache_read_input_tokens === 0) {
|
|
222
|
-
return cache === "automatic"
|
|
223
|
-
? {
|
|
224
|
-
kind: "cold",
|
|
225
|
-
detail:
|
|
226
|
-
"no cache hit yet -- this provider caches on its own, best-effort, and " +
|
|
227
|
-
"a write takes seconds to land. Only a problem if it persists across turns",
|
|
228
|
-
}
|
|
229
|
-
: {
|
|
230
|
-
kind: "suspicious",
|
|
231
|
-
detail:
|
|
232
|
-
"cache_read is 0 on a follow-up request -- the prefix changed, or it is " +
|
|
233
|
-
"below the model's minimum cacheable size",
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
const previousPrompt =
|
|
237
|
-
previous.input_tokens +
|
|
238
|
-
previous.cache_creation_input_tokens +
|
|
239
|
-
previous.cache_read_input_tokens;
|
|
240
|
-
if (sample.cache_read_input_tokens < previousPrompt * 0.5) {
|
|
241
|
-
return {
|
|
242
|
-
kind: "suspicious",
|
|
243
|
-
detail:
|
|
244
|
-
`cache_read (${sample.cache_read_input_tokens}) covers less than half of the ` +
|
|
245
|
-
`previous prompt (${previousPrompt}) -- part of the prefix was invalidated`,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
return {
|
|
249
|
-
kind: "healthy",
|
|
250
|
-
read: sample.cache_read_input_tokens,
|
|
251
|
-
wrote: sample.cache_creation_input_tokens,
|
|
252
|
-
};
|
|
253
|
-
}
|
package/src/runtime/version.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one place xeno's version is known (S.4: a release edits package.json and
|
|
3
|
-
* nothing else). It is sent as the User-Agent (D18: xeno names itself
|
|
4
|
-
* honestly) and compared against npm for update notices.
|
|
5
|
-
*
|
|
6
|
-
* Read at load rather than imported: `bun run check` requires every import to
|
|
7
|
-
* be a literal relative `.ts` path. package.json ships in every install, so
|
|
8
|
-
* this resolves the same way from a checkout and from a global install.
|
|
9
|
-
*/
|
|
10
|
-
import { readFileSync } from "node:fs";
|
|
11
|
-
|
|
12
|
-
export const XENO_VERSION: string = (JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")) as { version: string }).version;
|
package/src/runtime/web-http.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/** Shared bounded network leaves; request errors never include arbitrary peer text or credentials. */
|
|
2
|
-
import { webUrl } from "../core/web-content.ts";
|
|
3
|
-
import { READ_ATTEMPTED, ToolFault } from "../core/tool-failure.ts";
|
|
4
|
-
|
|
5
|
-
// Assumed resource bounds follow OpenCode's 5 MiB response cap; streamed enforcement prevents oversized allocation.
|
|
6
|
-
export const MAX_WEB_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
7
|
-
export const MAX_WEB_REDIRECTS = 5;
|
|
8
|
-
export type WebFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
9
|
-
export async function boundedResponseBytes(response: Response, limit = MAX_WEB_RESPONSE_BYTES): Promise<Uint8Array> {
|
|
10
|
-
const length = Number(response.headers.get("content-length"));
|
|
11
|
-
if (Number.isFinite(length) && length > limit) {
|
|
12
|
-
await response.body?.cancel(); throw new ToolFault("execution_failed", `Response exceeds ${limit} bytes`, READ_ATTEMPTED);
|
|
13
|
-
}
|
|
14
|
-
if (!response.body) return new Uint8Array();
|
|
15
|
-
const reader = response.body.getReader(), chunks: Uint8Array[] = [];
|
|
16
|
-
let size = 0;
|
|
17
|
-
try {
|
|
18
|
-
for (;;) {
|
|
19
|
-
const next = await reader.read();
|
|
20
|
-
if (next.done) break;
|
|
21
|
-
size += next.value.byteLength;
|
|
22
|
-
if (size > limit) { await reader.cancel(); throw new ToolFault("execution_failed", `Response exceeds ${limit} bytes`, READ_ATTEMPTED); }
|
|
23
|
-
chunks.push(next.value);
|
|
24
|
-
}
|
|
25
|
-
} finally { reader.releaseLock(); }
|
|
26
|
-
const bytes = new Uint8Array(size); let offset = 0;
|
|
27
|
-
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
28
|
-
return bytes;
|
|
29
|
-
}
|
|
30
|
-
export async function boundedWebRequest(url: string, init: RequestInit, timeoutMs: number, signal: AbortSignal | undefined,
|
|
31
|
-
fetcher: WebFetch = fetch, redirects = MAX_WEB_REDIRECTS): Promise<{ response: Response; bytes: Uint8Array; url: string; redirects: string[] }> {
|
|
32
|
-
const deadline = new AbortController(), combined = AbortSignal.any([deadline.signal, ...(signal ? [signal] : [])]);
|
|
33
|
-
const timer = setTimeout(() => deadline.abort(), timeoutMs), history: string[] = [];
|
|
34
|
-
let target = webUrl(url);
|
|
35
|
-
try {
|
|
36
|
-
combined.throwIfAborted();
|
|
37
|
-
for (;;) {
|
|
38
|
-
const response = await fetcher(target, { ...init, redirect: "manual", signal: combined });
|
|
39
|
-
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
40
|
-
await response.body?.cancel();
|
|
41
|
-
const location = response.headers.get("location");
|
|
42
|
-
if (!location || history.length >= redirects) throw new ToolFault("execution_failed", "Redirect limit exceeded or Location missing", READ_ATTEMPTED);
|
|
43
|
-
const next = webUrl(new URL(location, target).href);
|
|
44
|
-
if (target.startsWith("https:") && next.startsWith("http:")) throw new ToolFault("execution_failed", "HTTPS redirects cannot downgrade to HTTP", READ_ATTEMPTED);
|
|
45
|
-
history.push(target); target = next;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
return { response, bytes: await boundedResponseBytes(response), url: target, redirects: history };
|
|
49
|
-
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
if (signal?.aborted) throw new ToolFault("cancelled", "Network request cancelled", READ_ATTEMPTED);
|
|
52
|
-
if (deadline.signal.aborted) throw new ToolFault("timeout", "Network request timed out", READ_ATTEMPTED);
|
|
53
|
-
if (error instanceof ToolFault) throw error;
|
|
54
|
-
throw new ToolFault("execution_failed", "Network request failed; verify the URL and network connection", READ_ATTEMPTED);
|
|
55
|
-
} finally { clearTimeout(timer); }
|
|
56
|
-
}
|
package/src/runtime/web-tools.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/** Network adapters share the normal tool admission and append-only observation path (D11/D13). */
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { webFetchArguments, webSearchArguments, webSearchHits, webSearchProjection, decodeWebEntities,
|
|
4
|
-
type WebFormat, type WebSearchConfig } from "../core/web-content.ts";
|
|
5
|
-
import { ToolFault, NOT_STARTED, READ_ATTEMPTED } from "../core/tool-failure.ts";
|
|
6
|
-
import { archiveObservation } from "../store/observations.ts";
|
|
7
|
-
import { presentObservation, type ToolContext, type ToolDefinition } from "./tools.ts";
|
|
8
|
-
import { boundedWebRequest, type WebFetch } from "./web-http.ts";
|
|
9
|
-
|
|
10
|
-
/** Bun's native HTML parser handles malformed/quoted markup; no homegrown HTML tokeniser or new dependency. */
|
|
11
|
-
export async function extractWebHtml(html: string, url: string, format: Exclude<WebFormat, "html">): Promise<{ title: string; content: string }> {
|
|
12
|
-
let title = "";
|
|
13
|
-
const cleaned = await new HTMLRewriter().on("title", { text(chunk) { title += chunk.text; } })
|
|
14
|
-
.on("head,script,style,noscript,iframe,object,embed,template", { element(element) { element.remove(); } })
|
|
15
|
-
.transform(new Response(html)).text();
|
|
16
|
-
const blocks = new Set(["p", "div", "section", "article", "main", "header", "footer", "blockquote", "ul", "ol", "table", "tr", "pre", "h1", "h2", "h3", "h4", "h5", "h6"]);
|
|
17
|
-
const content = await new HTMLRewriter().on("*", { element(element) {
|
|
18
|
-
const tag = element.tagName;
|
|
19
|
-
if (blocks.has(tag) || tag === "br" || tag === "li") element.before("\n");
|
|
20
|
-
if (format === "markdown" && /^h[1-6]$/.test(tag)) element.before("#".repeat(Number(tag[1])) + " ");
|
|
21
|
-
if (format === "markdown" && tag === "li") element.before("- ");
|
|
22
|
-
if (format === "markdown" && tag === "pre") element.before("```\n");
|
|
23
|
-
if (tag === "a") {
|
|
24
|
-
const href = element.getAttribute("href");
|
|
25
|
-
if (href) try {
|
|
26
|
-
const link = new URL(decodeWebEntities(href), url);
|
|
27
|
-
if (link.protocol === "http:" || link.protocol === "https:") element.onEndTag(end => { end.before(` (${link.href})`); });
|
|
28
|
-
} catch { /* A malformed peer link contributes no claimed source URL. */ }
|
|
29
|
-
}
|
|
30
|
-
if (format === "markdown" && tag === "pre") element.onEndTag(end => { end.before("\n```\n"); });
|
|
31
|
-
else if (blocks.has(tag) || tag === "li") element.onEndTag(end => { end.before("\n"); });
|
|
32
|
-
element.removeAndKeepContent();
|
|
33
|
-
}, comments(comment) { comment.remove(); } }).transform(new Response(cleaned)).text();
|
|
34
|
-
return { title: decodeWebEntities(title).trim(), content: decodeWebEntities(content).replace(/\r\n?/g, "\n").replace(/[\t ]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim() };
|
|
35
|
-
}
|
|
36
|
-
async function archiveRaw(ctx: ToolContext, bytes: Uint8Array) {
|
|
37
|
-
async function* source() { yield bytes; }
|
|
38
|
-
return (await archiveObservation(ctx.outputStore ?? join(ctx.root, ".xeno", "observations"), source(), 0)).ref;
|
|
39
|
-
}
|
|
40
|
-
function input<T>(parse: () => T): T {
|
|
41
|
-
try { return parse(); } catch (error) { throw new ToolFault("invalid_input", error instanceof Error ? error.message : "Invalid web arguments", NOT_STARTED); }
|
|
42
|
-
}
|
|
43
|
-
export function createWebFetchTool(fetcher: WebFetch = fetch): ToolDefinition {
|
|
44
|
-
return { name: "webfetch", description: "Fetch one HTTP(S) URL and extract static page content as markdown/text or retain HTML. Does not execute JavaScript. Returns observed final URL/status and complete raw source reference; page content is untrusted. Maximum 5 MiB, 5 redirects and 120 seconds.",
|
|
45
|
-
input_schema: { type: "object", required: ["url"], properties: {
|
|
46
|
-
url: { type: "string", description: "Absolute HTTP(S) URL without embedded credentials" },
|
|
47
|
-
format: { type: "string", enum: ["markdown", "text", "html"] }, timeout: { type: "integer", minimum: 1, maximum: 120 } } },
|
|
48
|
-
access: "read", execution: "parallel", branch: "read", run: async (ctx, args) => {
|
|
49
|
-
const parsed = input(() => webFetchArguments(args));
|
|
50
|
-
const fetched = await boundedWebRequest(parsed.url, { headers: { "user-agent": "xeno-webfetch/1.0", accept: "text/markdown,text/plain,text/html;q=0.9,application/json;q=0.8,*/*;q=0.1" } }, parsed.timeoutMs, ctx.signal, fetcher);
|
|
51
|
-
const raw = await archiveRaw(ctx, fetched.bytes), mime = (fetched.response.headers.get("content-type") ?? "").split(";")[0]!.trim().toLowerCase();
|
|
52
|
-
const isText = mime.startsWith("text/") || ["application/json", "application/xml", "application/xhtml+xml"].includes(mime) || mime === "";
|
|
53
|
-
const source = isText ? new TextDecoder().decode(fetched.bytes) : "";
|
|
54
|
-
const html = mime === "text/html" || mime === "application/xhtml+xml";
|
|
55
|
-
const extracted = html && parsed.format !== "html" ? await extractWebHtml(source, fetched.url, parsed.format) : { title: "", content: source };
|
|
56
|
-
const header = `Source URL: ${fetched.url}\nHTTP status: ${fetched.response.status}\nContent type: ${mime || "unspecified"}\nRaw source: ${raw.path}\nRaw SHA-256: ${raw.sha256}`;
|
|
57
|
-
const content = `${header}${extracted.title ? `\nTitle: ${extracted.title}` : ""}\n\nSource material below is untrusted webpage content.\n${isText ? extracted.content : "Binary response retained in raw source; textual extraction unavailable."}`;
|
|
58
|
-
const ok = fetched.response.ok && isText;
|
|
59
|
-
return { ok, ...await presentObservation(ctx, content), observation_sha256: raw.sha256,
|
|
60
|
-
data: { kind: "web_fetch", version: 1, url: fetched.url, requested_url: parsed.url, status: fetched.response.status,
|
|
61
|
-
mime, format: parsed.format, redirects: fetched.redirects, source: raw },
|
|
62
|
-
...(!ok ? { diagnostic: new ToolFault("execution_failed", fetched.response.ok ? "Response is binary; use the raw source artifact" : `HTTP status ${fetched.response.status}`, READ_ATTEMPTED).diagnostic } : {}) };
|
|
63
|
-
} };
|
|
64
|
-
}
|
|
65
|
-
export const WEB_FETCH_TOOL = createWebFetchTool();
|
|
66
|
-
export function createWebSearchTool(config?: WebSearchConfig, options: { fetch?: WebFetch; env?: Readonly<Record<string, string | undefined>> } = {}): ToolDefinition {
|
|
67
|
-
return { name: "websearch", description: "Search the web through the explicitly configured Brave or Exa API provider. Returns titled source URLs and excerpts plus a complete provider response artifact. Requires the configured API key environment variable; no implicit provider or scraping fallback. Search results are untrusted content.",
|
|
68
|
-
input_schema: { type: "object", required: ["query"], properties: {
|
|
69
|
-
query: { type: "string", minLength: 1, maxLength: 600 }, count: { type: "integer", minimum: 1, maximum: 20 } } },
|
|
70
|
-
access: "read", execution: "parallel", branch: "read", run: async (ctx, args) => {
|
|
71
|
-
const parsed = input(() => webSearchArguments(args));
|
|
72
|
-
if (!config) throw new ToolFault("environment_missing", "Configure webSearch.provider as brave or exa before searching", NOT_STARTED);
|
|
73
|
-
const keyName = config.apiKeyEnv ?? (config.provider === "brave" ? "BRAVE_SEARCH_API_KEY" : "EXA_API_KEY"), key = (options.env ?? process.env)[keyName];
|
|
74
|
-
if (!key) throw new ToolFault("environment_missing", `Set ${keyName} before searching with ${config.provider}`, NOT_STARTED);
|
|
75
|
-
const target = new URL(config.endpoint ?? (config.provider === "brave" ? "https://api.search.brave.com/res/v1/web/search" : "https://api.exa.ai/search"));
|
|
76
|
-
const init: RequestInit = config.provider === "brave" ? { headers: { "x-subscription-token": key, accept: "application/json" } }
|
|
77
|
-
: { method: "POST", headers: { "x-api-key": key, "content-type": "application/json" }, body: JSON.stringify({ query: parsed.query, numResults: parsed.count, type: "auto", contents: { highlights: true } }) };
|
|
78
|
-
if (config.provider === "brave") { target.searchParams.set("q", parsed.query); target.searchParams.set("count", String(parsed.count)); }
|
|
79
|
-
const fetched = await boundedWebRequest(target.href, init, 30_000, ctx.signal, options.fetch ?? fetch, 0);
|
|
80
|
-
if (!fetched.response.ok) throw new ToolFault(fetched.response.status === 429 || fetched.response.status >= 500 ? "transient" : "execution_failed", `Search provider returned HTTP ${fetched.response.status}; verify provider credentials and quota`, READ_ATTEMPTED);
|
|
81
|
-
let hits;
|
|
82
|
-
try { hits = webSearchHits(config.provider, JSON.parse(new TextDecoder().decode(fetched.bytes)), parsed.count); }
|
|
83
|
-
catch { throw new ToolFault("execution_failed", "Search provider response failed schema validation", READ_ATTEMPTED); }
|
|
84
|
-
const raw = await archiveRaw(ctx, fetched.bytes);
|
|
85
|
-
return { ok: true, ...await presentObservation(ctx, webSearchProjection(config.provider, parsed.query, hits) + `\n\nProvider response: ${raw.path}\nSHA-256: ${raw.sha256}`),
|
|
86
|
-
observation_sha256: raw.sha256, data: { kind: "web_search", version: 1, provider: config.provider, query: parsed.query, results: hits.map(hit => ({ ...hit })), source: raw } };
|
|
87
|
-
} };
|
|
88
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/** O.2/O.6: validate annotations against the full projection before they can be appended. */
|
|
2
|
-
import type { LogEvent } from "../core/events.ts";
|
|
3
|
-
import { deriveHandoff } from "../core/derive.ts";
|
|
4
|
-
import { scopedWorkId, workChangeError } from "../core/work-state.ts";
|
|
5
|
-
import { resolveEvidence, type EvidenceResolution } from "../core/evidence.ts";
|
|
6
|
-
import { unknownWorkClaimDiagnostic, unverifiedNoteDiagnostic } from "../core/note-guidance.ts";
|
|
7
|
-
import { failureDiagnostic, NOT_STARTED } from "../core/tool-failure.ts";
|
|
8
|
-
import type { ToolDiagnostic } from "../core/tool-contract.ts";
|
|
9
|
-
import { replayContext } from "../store/replay.ts";
|
|
10
|
-
import { digestNode, nodePath } from "../store/log.ts";
|
|
11
|
-
import { resolveEvidenceNote } from "./evidence-notes.ts";
|
|
12
|
-
import type { ToolResult } from "./tools.ts";
|
|
13
|
-
|
|
14
|
-
function rejected(message: string, diagnostic = failureDiagnostic("invalid_input", message, NOT_STARTED)): { out: ToolResult } {
|
|
15
|
-
return { out: { ok: false, content: message, data: { recorded: false }, diagnostic } };
|
|
16
|
-
}
|
|
17
|
-
function claimResult(annotation: Extract<LogEvent, { t: "claim" }>, proof: EvidenceResolution, openedAt?: string): ToolResult {
|
|
18
|
-
const diagnostic: ToolDiagnostic | undefined = proof.ok ? undefined : unverifiedNoteDiagnostic(proof, annotation.id, openedAt);
|
|
19
|
-
return { ok: proof.ok, content: JSON.stringify({ recorded: true, claim_event: annotation.id,
|
|
20
|
-
verification: proof.ok ? "verified" : "unverified", ...(proof.ok ? { evidence: proof.evidence } : { reason: proof.reason }) }),
|
|
21
|
-
...(diagnostic ? { diagnostic } : {}) };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function prepareWorkingNote(logDir: string, node: number, namespace: string, annotation: LogEvent | undefined, out: ToolResult): Promise<{ out: ToolResult; annotation?: LogEvent }> {
|
|
25
|
-
if (!annotation) return { out };
|
|
26
|
-
if (annotation.t !== "claim" && annotation.t !== "evidence_link" && !("work_id" in annotation && annotation.work_id !== undefined)) return { out, annotation };
|
|
27
|
-
const replayed = await replayContext(logDir, node);
|
|
28
|
-
const input = { prev: replayed.fullHandoff, events: replayed.events, depth: replayed.depth,
|
|
29
|
-
self: { node, log_ref: nodePath(logDir, node), sha256: await digestNode(logDir, node) } };
|
|
30
|
-
const current = deriveHandoff(input);
|
|
31
|
-
if (annotation.t === "evidence_link") {
|
|
32
|
-
const resolved = await resolveEvidenceNote(logDir, node, namespace, current, annotation);
|
|
33
|
-
return { out: resolved.result, ...(resolved.event ? { annotation: resolved.event } : {}) };
|
|
34
|
-
}
|
|
35
|
-
if (annotation.t === "claim" && annotation.work_id === undefined) {
|
|
36
|
-
return { annotation, out: claimResult(annotation, resolveEvidence(input.events, annotation.evidence)) };
|
|
37
|
-
}
|
|
38
|
-
if (!("work_id" in annotation) || annotation.work_id === undefined) throw new Error("missing identified work annotation");
|
|
39
|
-
const workId = scopedWorkId(namespace, annotation.work_id, current.state.work_items ?? []);
|
|
40
|
-
annotation = { ...annotation, work_id: workId };
|
|
41
|
-
const owned = current.state.work_items?.find(item => item.id === workId);
|
|
42
|
-
if (owned?.owner && owned.owner !== namespace) return rejected(`work ${workId} is owned by ${owned.owner}`);
|
|
43
|
-
if ((annotation.t === "next_action" || annotation.t === "work_dependencies") && annotation.depends_on !== undefined) annotation = {
|
|
44
|
-
...annotation, depends_on: annotation.depends_on.map(id => scopedWorkId(namespace, id, current.state.work_items ?? [])),
|
|
45
|
-
};
|
|
46
|
-
if ((annotation.t === "decision" || annotation.t === "pitfall") && !current.state.work_items?.some(item => item.id === workId)) {
|
|
47
|
-
return rejected(`unknown work_id ${workId}; record a next note first`);
|
|
48
|
-
}
|
|
49
|
-
if (!owned && annotation.t === "claim") {
|
|
50
|
-
const diagnostic = unknownWorkClaimDiagnostic(input.events, annotation);
|
|
51
|
-
return rejected(diagnostic.message, diagnostic);
|
|
52
|
-
}
|
|
53
|
-
const error = workChangeError(current.state.work_items ?? [], annotation);
|
|
54
|
-
if (error) return rejected(error);
|
|
55
|
-
const proposed = deriveHandoff({ ...input, events: [...input.events, annotation] });
|
|
56
|
-
const item = proposed.state.work_items?.find(item => item.id === workId);
|
|
57
|
-
const claim = annotation.t === "claim" ? claimResult(annotation, resolveEvidence(input.events, annotation.evidence, owned?.opened_at), owned?.opened_at) : undefined;
|
|
58
|
-
return { annotation, out: { ...claim, ok: annotation.t !== "claim" || item?.verification === "verified",
|
|
59
|
-
content: JSON.stringify({ recorded: true, work_item: item }) } };
|
|
60
|
-
}
|