chore: update merge branch with latest v2
This commit is contained in:
commit
24d93cf720
505 changed files with 28393 additions and 17650 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -13,6 +13,7 @@ tmp
|
||||||
dist
|
dist
|
||||||
ts-dist
|
ts-dist
|
||||||
.turbo
|
.turbo
|
||||||
|
.typecheck-profiles
|
||||||
**/.serena
|
**/.serena
|
||||||
.serena/
|
.serena/
|
||||||
**/.omo
|
**/.omo
|
||||||
|
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
---
|
|
||||||
name: debug-opencode
|
|
||||||
description: Use when interactively running, debugging, or verifying opencode's own V2 CLI/TUI or server during development in this repo — starting the dev TUI, driving it with termctrl, comparing V2 against the legacy TUI, hitting the V2 server/API directly, reading log files, or attaching Bun's inspector.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Debugging opencode itself
|
|
||||||
|
|
||||||
Workflow for interactively exercising the V2 CLI/TUI and server while developing in this repo. All commands below run from `packages/cli` unless noted otherwise.
|
|
||||||
|
|
||||||
## Migration context
|
|
||||||
|
|
||||||
- The TUI is being ported from legacy APIs to the new V2 APIs. New and migrated TUI behavior should use `sdk.client.v2` and the location-scoped data in `packages/tui/src/context/data.tsx` instead of adding dependencies on legacy sync state.
|
|
||||||
- Preserve established TUI behavior unless the task intentionally changes it. When behavior, copy, keyboard interaction, or layout is unclear, compare the local V2 TUI with the latest released legacy TUI (see "Comparing V2 against the legacy TUI" below) rather than guessing.
|
|
||||||
|
|
||||||
## Server/client model
|
|
||||||
|
|
||||||
opencode V2 is a client/server system, not a single monolithic process:
|
|
||||||
|
|
||||||
- **Server process** runs the Effect HTTP API (`packages/server`) and owns all domain state: sessions, database, plugins, permissions, Location services. It's started by the `serve` command (`packages/cli/src/commands/handlers/serve.ts`).
|
|
||||||
- **TUI process** is a separate process that runs no application logic itself — it's an HTTP/SSE client of the server via the generated SDK (`createOpencodeClient` / `sdk.client.v2`).
|
|
||||||
- **Discovery**: CLI processes find the shared server through a JSON registration file at `~/.local/state/opencode/service.json` (or `service-local.json` for the local/dev channel) containing `{id, version, url, pid}`. A separate password file under `~/.config/opencode/service.json` provides HTTP Basic auth. Before reusing a registration, the client calls `GET /health` to confirm the server is alive, authenticated, and version-compatible.
|
|
||||||
- **Sharing**: because of this registration/health-check dance, many concurrent `opencode`/TUI invocations converge on one shared background daemon rather than each spawning their own. If no compatible healthy daemon is found, a new one is spawned detached (`serve --service`) and registers itself.
|
|
||||||
- **`bun dev service start|status|stop|restart`** manages this shared background daemon's lifecycle directly — useful when you need to force a fresh server, confirm one is running, or kill a stuck one.
|
|
||||||
- **Standalone mode** (`--standalone`) opts a single invocation out of the shared daemon: it spawns a private one-off `serve --stdio --port 0` child tied to that invocation's lifetime, with its own random password. Use this to isolate a debugging session from your other running opencode sessions.
|
|
||||||
- Every log line is tagged `role=server` or `role=cli` and a per-process `run=<id>`, so you can distinguish server-side and client-side activity in one shared log file (see "Logs" below) even when both roles are interleaved from concurrent processes.
|
|
||||||
|
|
||||||
## Starting the dev TUI
|
|
||||||
|
|
||||||
- This package is the V2 CLI adapter. Run its `dev` script when testing the TUI; do not use the repository-root `bun dev`, which launches the legacy `packages/opencode` CLI.
|
|
||||||
- Run commands from `packages/cli`. Use `bun dev` for most debugging so the TUI starts with a private V2 server.
|
|
||||||
|
|
||||||
## Interactive debugging with termctrl
|
|
||||||
|
|
||||||
- Use `termctrl` for interactive checks instead of starting the TUI as a blocking foreground process. It provides a real PTY, handles OpenTUI's host handshake, and can save reviewable screenshots.
|
|
||||||
- Use a dedicated session name and do not reuse or kill an unrelated session.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
|
||||||
termctrl wait opencode-v2-dev "Ask anything" --timeout 20000
|
|
||||||
termctrl show opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
- Wait for visible text before interacting instead of relying on fixed sleeps. Use the text expected from the screen under test, such as `Ask anything` or `Connect a provider`.
|
|
||||||
- Drive the running TUI with `termctrl send`. Prefix typed input with `text:` and send control keys separately so the interaction matches real terminal input.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl send opencode-v2-dev 'text:example prompt' enter
|
|
||||||
termctrl send opencode-v2-dev ctrl-c
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use `termctrl show` after each meaningful interaction and inspect the full visible screen for rendering errors, stale state, error toasts, and unexpected exits.
|
|
||||||
- Save PNG evidence for every user-visible bug and fix. Do not save text captures; inspect the rendered PNG. Write temporary captures outside the repository unless the artifact is intended to be committed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2-tui.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- For resize-sensitive changes, resize the viewport, wait for the expected content, and capture the screen again:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl resize opencode-v2-dev --cols 100 --rows 30
|
|
||||||
termctrl show opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
- Source changes may require restarting the process. Use `termctrl restart opencode-v2-dev` rather than assuming the running TUI reloaded the change.
|
|
||||||
- To exercise background-service behavior, use `bun dev service start`, `bun dev service status`, and `bun dev service stop`.
|
|
||||||
- Always clean up the Terminal Control session when the check is complete:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl stop opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Comparing V2 against the legacy TUI
|
|
||||||
|
|
||||||
Run both versions in separate Terminal Control sessions and save PNG-only captures at equivalent states:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From packages/cli: local V2 TUI
|
|
||||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
|
||||||
|
|
||||||
# Released legacy TUI behavior reference
|
|
||||||
termctrl start opencode-legacy --host opentui --cols 112 --rows 34 -- bunx opencode-ai@latest
|
|
||||||
|
|
||||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2.png
|
|
||||||
termctrl save opencode-legacy --format png --out /tmp/opencode/legacy.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use the same viewport and send equivalent inputs to both sessions before comparing screenshots. The released CLI is a behavioral reference, not a source of V2 API design; keep the local implementation on V2 endpoints.
|
|
||||||
- Stop both sessions after comparison: `termctrl stop opencode-v2-dev` and `termctrl stop opencode-legacy`.
|
|
||||||
|
|
||||||
## Server/API debugging
|
|
||||||
|
|
||||||
- Use `bun dev api --help` from `packages/cli` to inspect the API debugging command. It sends one request to the V2 server using the same daemon discovery/auth path as the CLI.
|
|
||||||
- Use `bun dev api` to introspect the server-side data backing the TUI. This is useful when debugging UI bugs: compare what the screen renders with the raw session, message, event, agent, or health data returned by the API to determine whether the bug is in the server state, the client data layer, or the TUI rendering.
|
|
||||||
- `bun dev api` accepts either an OpenAPI operation ID or a raw HTTP method plus path:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun dev api get /health
|
|
||||||
bun dev api get /openapi.json
|
|
||||||
bun dev api <operationId> --param key=value
|
|
||||||
```
|
|
||||||
|
|
||||||
- Pass JSON request bodies with `--data`/`-d`; the command sets `content-type: application/json` automatically unless you provide a header. Add extra headers with `--header`/`-H name:value`.
|
|
||||||
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
|
|
||||||
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
|
|
||||||
|
|
||||||
## Auditing installed `opencode2` sessions
|
|
||||||
|
|
||||||
Installed next-channel sessions normally use `~/.local/share/opencode/opencode-next.db` and `~/.local/share/opencode/log/opencode.log`; `OPENCODE_DB` can override the database. Before calling `opencode2 api`, inspect `~/.local/state/opencode/service.json` because the command may start a daemon when none is healthy.
|
|
||||||
|
|
||||||
For a supplied `ses_...` ID, compare three sources:
|
|
||||||
|
|
||||||
- `opencode2 api get /api/session/active` and the Session/message endpoints for live server state.
|
|
||||||
- The database's ordered `event` rows for durable history.
|
|
||||||
- `packages/tui/src/context/data.tsx` and the relevant route for client projection and rendering.
|
|
||||||
|
|
||||||
Locate an uncertain database without modifying it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
SESSION=ses_...
|
|
||||||
for db in ~/.local/share/opencode/*.db; do
|
|
||||||
sqlite3 "file:$db?mode=ro" "select 1 from session where id='$SESSION' limit 1" 2>/dev/null | grep -q 1 && printf '%s\n' "$db"
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logs
|
|
||||||
|
|
||||||
- Log files live under `~/.local/share/opencode/log/`. In a local/dev checkout the active file is `opencode-local.log`; `opencode.log` is used for non-local (released) channel installs. Both are append-only, shared across every CLI and server process on the machine.
|
|
||||||
- Each line is structured `key=value` text: `timestamp`, `level`, `run=<id>` (per-process run ID), `message`, and a `role=cli` or `role=server` tag. Use `run=` to isolate one process's activity and `role=` to separate client-side from server-side log lines, since a shared daemon interleaves many processes' output in one file.
|
|
||||||
- Tail the live file while reproducing an issue instead of guessing from stale output:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tail -f ~/.local/share/opencode/log/opencode-local.log
|
|
||||||
```
|
|
||||||
|
|
||||||
- Filter to one run or role when the file is noisy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grep 'run=8fc3b1d5' ~/.local/share/opencode/log/opencode-local.log
|
|
||||||
grep 'role=server' ~/.local/share/opencode/log/opencode-local.log
|
|
||||||
```
|
|
||||||
|
|
||||||
- `OPENCODE_LOG_LEVEL` controls verbosity (default `INFO`); set it before starting `bun dev` or `serve` to get `DEBUG` output for a specific repro.
|
|
||||||
- `OPENCODE_PRINT_LOGS=1` additionally tees log output to stderr of the process that emitted it, which is useful when a process fails before you'd think to check the shared log file.
|
|
||||||
- `termctrl logs <session>` surfaces stdout/stderr for a Terminal Control session specifically (e.g. inspector output or startup failures before the TUI renderer starts) — use the log file above for anything emitted by a separate server/daemon process instead.
|
|
||||||
|
|
||||||
## Debugger
|
|
||||||
|
|
||||||
- To debug the V2 CLI or TUI with Bun's inspector, launch the CLI entrypoint through Terminal Control with an inspector URL, then attach a debugger to that URL:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl start opencode-v2-debug --host opentui --cols 112 --rows 34 -- \
|
|
||||||
bun run --inspect=ws://localhost:6499/ src/index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use `--inspect-wait` or `--inspect-brk` when execution must pause until the debugger attaches.
|
|
||||||
- Use `termctrl logs opencode-v2-debug` for inspector output or startup failures emitted before the TUI renderer starts. Use `termctrl show` for the visible full-screen TUI.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- Run `bun typecheck` from `packages/cli` after CLI adapter changes.
|
|
||||||
- Run `bun typecheck` and `bun test` from `packages/tui` after shared TUI changes. Do not run tests from the repository root.
|
|
||||||
- Treat automated checks and Terminal Control smoke tests as complementary. For user-visible changes, verify initial render, the changed interaction, Ctrl-C exit behavior, and save a screenshot of the corrected state.
|
|
||||||
|
|
@ -19,6 +19,8 @@ Valid types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. Scopes a
|
||||||
|
|
||||||
Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
|
Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
|
||||||
|
|
||||||
|
Never bypass Git hooks. Do not use `--no-verify` or otherwise disable, skip, or circumvent commit or push hooks. If a hook fails, fix the failure or stop and report it to the user.
|
||||||
|
|
||||||
## Style Guide
|
## Style Guide
|
||||||
|
|
||||||
### General Principles
|
### General Principles
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src",
|
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src",
|
||||||
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
|
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck --concurrency=3",
|
||||||
|
"typecheck:profile": "bun script/profile-typecheck.ts",
|
||||||
|
"typecheck:profile:packages": "bun script/profile-typecheck-packages.ts",
|
||||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||||
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const profiles = [
|
||||||
{ name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
|
{ name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
|
||||||
{
|
{
|
||||||
name: "multi patch",
|
name: "multi patch",
|
||||||
tool: "apply_patch",
|
tool: "patch",
|
||||||
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
|
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
||||||
userMessage(),
|
userMessage(),
|
||||||
assistantMessage(
|
assistantMessage(
|
||||||
[
|
[
|
||||||
toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
|
toolPart(patchID, "patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
|
||||||
textPart(followingID, "Following incremental patch"),
|
textPart(followingID, "Following incremental patch"),
|
||||||
],
|
],
|
||||||
{ completed: false },
|
{ completed: false },
|
||||||
|
|
@ -49,7 +49,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
||||||
partUpdated(
|
partUpdated(
|
||||||
toolPart(
|
toolPart(
|
||||||
patchID,
|
patchID,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"running",
|
"running",
|
||||||
{ files: [first.filePath, second.filePath] },
|
{ files: [first.filePath, second.filePath] },
|
||||||
{ metadata: { files: [first, second] } },
|
{ metadata: { files: [first, second] } },
|
||||||
|
|
@ -61,7 +61,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
||||||
partUpdated(
|
partUpdated(
|
||||||
toolPart(
|
toolPart(
|
||||||
patchID,
|
patchID,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: [first.filePath, second.filePath, third.filePath] },
|
{ files: [first.filePath, second.filePath, third.filePath] },
|
||||||
{ metadata: { files: [first, second, third] } },
|
{ metadata: { files: [first, second, third] } },
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,7 @@ function performanceTurn(index: number) {
|
||||||
messageID: assistantID,
|
messageID: assistantID,
|
||||||
type: "tool",
|
type: "tool",
|
||||||
callID: `call_0000_${suffix}_patch`,
|
callID: `call_0000_${suffix}_patch`,
|
||||||
tool: "apply_patch",
|
tool: "patch",
|
||||||
state: {
|
state: {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
input: { patchText: realisticPatch(index) },
|
input: { patchText: realisticPatch(index) },
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ function toolPart(
|
||||||
): MessagePart {
|
): MessagePart {
|
||||||
const metadata =
|
const metadata =
|
||||||
metadataOverride ??
|
metadataOverride ??
|
||||||
(tool === "apply_patch"
|
(tool === "patch"
|
||||||
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
||||||
: tool === "edit" || tool === "write"
|
: tool === "edit" || tool === "write"
|
||||||
? {
|
? {
|
||||||
|
|
@ -219,7 +219,7 @@ function turn(index: number): Message[] {
|
||||||
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 8 === 0
|
...(index % 8 === 0
|
||||||
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 7 === 0
|
...(index % 7 === 0
|
||||||
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
|
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ test("renders a completed single-file patch", async ({ page }) => {
|
||||||
assistantMessage([
|
assistantMessage([
|
||||||
toolPart(
|
toolPart(
|
||||||
id,
|
id,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: ["src/a.ts"] },
|
{ files: ["src/a.ts"] },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ test("preserves nested patch file state through outer collapse and reopen", asyn
|
||||||
assistantMessage([
|
assistantMessage([
|
||||||
toolPart(
|
toolPart(
|
||||||
patchID,
|
patchID,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: files.map((file) => file.filePath) },
|
{ files: files.map((file) => file.filePath) },
|
||||||
{ metadata: { files } },
|
{ metadata: { files } },
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ function editPart(id: string) {
|
||||||
function patchPart(id: string) {
|
function patchPart(id: string) {
|
||||||
return toolPart(
|
return toolPart(
|
||||||
id,
|
id,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: ["src/a.ts", "src/b.ts"] },
|
{ files: ["src/a.ts", "src/b.ts"] },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from "../performance/timeline-stability/fixture"
|
} from "../performance/timeline-stability/fixture"
|
||||||
|
|
||||||
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
|
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
|
||||||
const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
|
const ordinary = ["bash", "edit", "write", "patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
|
||||||
const parts = ordinary.map((tool, index) =>
|
const parts = ordinary.map((tool, index) =>
|
||||||
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
|
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
|
||||||
)
|
)
|
||||||
|
|
@ -90,7 +90,7 @@ function questionInput() {
|
||||||
function errorInput(tool: string) {
|
function errorInput(tool: string) {
|
||||||
if (tool === "bash") return { command: "exit 1" }
|
if (tool === "bash") return { command: "exit 1" }
|
||||||
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
|
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
|
||||||
if (tool === "apply_patch") return { files: ["src/error.ts"] }
|
if (tool === "patch") return { files: ["src/error.ts"] }
|
||||||
if (tool === "webfetch") return { url: "https://example.com" }
|
if (tool === "webfetch") return { url: "https://example.com" }
|
||||||
if (tool === "websearch") return { query: "failure" }
|
if (tool === "websearch") return { query: "failure" }
|
||||||
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
|
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ function toolPart(
|
||||||
outputLength = 160,
|
outputLength = 160,
|
||||||
): MessagePart {
|
): MessagePart {
|
||||||
const metadata =
|
const metadata =
|
||||||
tool === "apply_patch"
|
tool === "patch"
|
||||||
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
||||||
: tool === "edit" || tool === "write"
|
: tool === "edit" || tool === "write"
|
||||||
? {
|
? {
|
||||||
|
|
@ -199,7 +199,7 @@ function turn(index: number): Message[] {
|
||||||
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 8 === 0
|
...(index % 8 === 0
|
||||||
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []),
|
...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []),
|
||||||
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
|
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||||
const path = url.pathname
|
const path = url.pathname
|
||||||
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
|
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
|
||||||
if (path === "/global/health") return json(route, { healthy: true })
|
if (path === "/global/health") return json(route, { healthy: true })
|
||||||
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false })
|
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
|
||||||
if (path === "/permission")
|
if (path === "/permission")
|
||||||
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
|
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
|
||||||
if (path === "/question")
|
if (path === "/question")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
Session,
|
Session,
|
||||||
SessionStatus,
|
SessionStatus,
|
||||||
SnapshotFileDiff,
|
FileDiffInfo,
|
||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
|
|
@ -188,7 +188,7 @@ export function applyDirectoryEvent(input: {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session.diff": {
|
case "session.diff": {
|
||||||
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
|
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
|
||||||
input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" }))
|
input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" }))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type {
|
||||||
PermissionRequest,
|
PermissionRequest,
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
SessionStatus,
|
SessionStatus,
|
||||||
SnapshotFileDiff,
|
FileDiffInfo,
|
||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
|
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
|
||||||
|
|
@ -33,7 +33,7 @@ describe("app session cache", () => {
|
||||||
test("dropSessionCaches clears orphaned parts without message rows", () => {
|
test("dropSessionCaches clears orphaned parts without message rows", () => {
|
||||||
const store: {
|
const store: {
|
||||||
session_status: Record<string, SessionStatus | undefined>
|
session_status: Record<string, SessionStatus | undefined>
|
||||||
session_diff: Record<string, SnapshotFileDiff[] | undefined>
|
session_diff: Record<string, FileDiffInfo[] | undefined>
|
||||||
todo: Record<string, Todo[] | undefined>
|
todo: Record<string, Todo[] | undefined>
|
||||||
message: Record<string, Message[] | undefined>
|
message: Record<string, Message[] | undefined>
|
||||||
part: Record<string, Part[] | undefined>
|
part: Record<string, Part[] | undefined>
|
||||||
|
|
@ -67,7 +67,7 @@ describe("app session cache", () => {
|
||||||
const m = msg("msg_1", "ses_1")
|
const m = msg("msg_1", "ses_1")
|
||||||
const store: {
|
const store: {
|
||||||
session_status: Record<string, SessionStatus | undefined>
|
session_status: Record<string, SessionStatus | undefined>
|
||||||
session_diff: Record<string, SnapshotFileDiff[] | undefined>
|
session_diff: Record<string, FileDiffInfo[] | undefined>
|
||||||
todo: Record<string, Todo[] | undefined>
|
todo: Record<string, Todo[] | undefined>
|
||||||
message: Record<string, Message[] | undefined>
|
message: Record<string, Message[] | undefined>
|
||||||
part: Record<string, Part[] | undefined>
|
part: Record<string, Part[] | undefined>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type {
|
||||||
PermissionRequest,
|
PermissionRequest,
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
SessionStatus,
|
SessionStatus,
|
||||||
SnapshotFileDiff,
|
FileDiffInfo,
|
||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const SESSION_CACHE_LIMIT = 40
|
||||||
|
|
||||||
type SessionCache = {
|
type SessionCache = {
|
||||||
session_status: Record<string, SessionStatus | undefined>
|
session_status: Record<string, SessionStatus | undefined>
|
||||||
session_diff: Record<string, SnapshotFileDiff[] | undefined>
|
session_diff: Record<string, FileDiffInfo[] | undefined>
|
||||||
todo: Record<string, Todo[] | undefined>
|
todo: Record<string, Todo[] | undefined>
|
||||||
message: Record<string, Message[] | undefined>
|
message: Record<string, Message[] | undefined>
|
||||||
part: Record<string, Part[] | undefined>
|
part: Record<string, Part[] | undefined>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import type {
|
||||||
ReferenceInfo,
|
ReferenceInfo,
|
||||||
Session,
|
Session,
|
||||||
SessionStatus,
|
SessionStatus,
|
||||||
SnapshotFileDiff,
|
FileDiffInfo,
|
||||||
Todo,
|
Todo,
|
||||||
VcsInfo,
|
VcsInfo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
@ -51,7 +51,7 @@ export type State = {
|
||||||
}
|
}
|
||||||
session_working(id: string): boolean
|
session_working(id: string): boolean
|
||||||
session_diff: {
|
session_diff: {
|
||||||
[sessionID: string]: SnapshotFileDiff[]
|
[sessionID: string]: FileDiffInfo[]
|
||||||
}
|
}
|
||||||
todo: {
|
todo: {
|
||||||
[sessionID: string]: Todo[]
|
[sessionID: string]: Todo[]
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,9 @@ describe("enqueueServerEvent", () => {
|
||||||
|
|
||||||
enqueue(partUpdated("old"))
|
enqueue(partUpdated("old"))
|
||||||
enqueue({
|
enqueue({
|
||||||
|
id: "event-delete",
|
||||||
type: "session.deleted",
|
type: "session.deleted",
|
||||||
properties: { sessionID: "session", info: { id: "session" } },
|
properties: { sessionID: "session" },
|
||||||
} as Event)
|
} as Event)
|
||||||
enqueue(partUpdated("new"))
|
enqueue(partUpdated("new"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
Session,
|
Session,
|
||||||
SessionStatus,
|
SessionStatus,
|
||||||
SnapshotFileDiff,
|
FileDiffInfo,
|
||||||
Todo,
|
Todo,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { batch } from "solid-js"
|
import { batch } from "solid-js"
|
||||||
|
|
@ -139,7 +139,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
||||||
const [data, setData] = createStore({
|
const [data, setData] = createStore({
|
||||||
info: {} as Record<string, Session | undefined>,
|
info: {} as Record<string, Session | undefined>,
|
||||||
session_status: {} as Record<string, SessionStatus>,
|
session_status: {} as Record<string, SessionStatus>,
|
||||||
session_diff: {} as Record<string, SnapshotFileDiff[]>,
|
session_diff: {} as Record<string, FileDiffInfo[]>,
|
||||||
todo: {} as Record<string, Todo[]>,
|
todo: {} as Record<string, Todo[]>,
|
||||||
permission: {} as Record<string, PermissionRequest[]>,
|
permission: {} as Record<string, PermissionRequest[]>,
|
||||||
question: {} as Record<string, QuestionRequest[]>,
|
question: {} as Record<string, QuestionRequest[]>,
|
||||||
|
|
@ -769,7 +769,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "session.diff": {
|
case "session.diff": {
|
||||||
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
|
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
|
||||||
setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" }))
|
setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createEffect, onCleanup, type JSX } from "solid-js"
|
import { createEffect, onCleanup, type JSX } from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||||
import { SessionReview } from "@opencode-ai/session-ui/session-review"
|
import { SessionReview } from "@opencode-ai/session-ui/session-review"
|
||||||
import type {
|
import type {
|
||||||
SessionReviewCommentActions,
|
SessionReviewCommentActions,
|
||||||
|
|
@ -14,7 +14,7 @@ import type { LineComment } from "@/context/comments"
|
||||||
|
|
||||||
export type DiffStyle = "unified" | "split"
|
export type DiffStyle = "unified" | "split"
|
||||||
|
|
||||||
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
|
type ReviewDiff = FileDiffInfo | VcsFileDiff
|
||||||
|
|
||||||
export interface SessionReviewTabProps {
|
export interface SessionReviewTabProps {
|
||||||
title?: JSX.Element
|
title?: JSX.Element
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
||||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
|
||||||
|
|
@ -23,7 +23,6 @@ import { useFile, type SelectedLineRange } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
|
||||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||||
import {
|
import {
|
||||||
|
|
@ -36,15 +35,9 @@ import {
|
||||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
|
|
||||||
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
|
|
||||||
|
|
||||||
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
|
|
||||||
return typeof value.file === "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SessionSidePanel(props: {
|
export function SessionSidePanel(props: {
|
||||||
canReview: () => boolean
|
canReview: () => boolean
|
||||||
diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
|
diffs: () => (FileDiffInfo | VcsFileDiff)[]
|
||||||
diffsReady: () => boolean
|
diffsReady: () => boolean
|
||||||
empty: () => string
|
empty: () => string
|
||||||
hasReview: () => boolean
|
hasReview: () => boolean
|
||||||
|
|
@ -59,7 +52,6 @@ export function SessionSidePanel(props: {
|
||||||
}) {
|
}) {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const sync = useSync()
|
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
|
|
@ -88,7 +80,7 @@ export function SessionSidePanel(props: {
|
||||||
})
|
})
|
||||||
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
|
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
|
||||||
|
|
||||||
const diffs = createMemo(() => props.diffs().filter(renderDiff))
|
const diffs = createMemo(() => props.diffs())
|
||||||
const diffFiles = createMemo(() => diffs().map((d) => d.file))
|
const diffFiles = createMemo(() => diffs().map((d) => d.file))
|
||||||
const kinds = createMemo(() => {
|
const kinds = createMemo(() => {
|
||||||
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
||||||
|
|
|
||||||
|
|
@ -1245,7 +1245,7 @@ export function MessageTimeline(props: {
|
||||||
const value = row()
|
const value = row()
|
||||||
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
|
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
|
||||||
const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
|
const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
|
||||||
return part?.type === "tool" && ["edit", "write", "apply_patch"].includes(part.tool)
|
return part?.type === "tool" && ["edit", "write", "patch", "apply_patch"].includes(part.tool)
|
||||||
}
|
}
|
||||||
const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
|
const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
|
||||||
let contentMeasureFrame: number | undefined
|
let contentMeasureFrame: number | undefined
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||||
import type { Kind } from "@/components/file-tree-v2"
|
import type { Kind } from "@/components/file-tree-v2"
|
||||||
import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
|
import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
|
||||||
|
|
||||||
export type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
|
export type RenderDiff = FileDiffInfo | VcsFileDiff
|
||||||
|
|
||||||
export function normalizePath(p: string) {
|
export function normalizePath(p: string) {
|
||||||
return normalizeFileTreeV2Path(p)
|
return normalizeFileTreeV2Path(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterRenderableDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
|
|
||||||
return typeof value.file === "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reviewDiffKinds(diffs: RenderDiff[]) {
|
export function reviewDiffKinds(diffs: RenderDiff[]) {
|
||||||
const merge = (a: Kind | undefined, b: Kind) => {
|
const merge = (a: Kind | undefined, b: Kind) => {
|
||||||
if (!a) return b
|
if (!a) return b
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createMemo, createSignal, Show, type JSX } from "solid-js"
|
import { createMemo, createSignal, Show, type JSX } from "solid-js"
|
||||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||||
import {
|
import {
|
||||||
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
|
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
|
||||||
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN,
|
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN,
|
||||||
|
|
@ -21,16 +21,11 @@ import type {
|
||||||
import FileTreeV2 from "@/components/file-tree-v2"
|
import FileTreeV2 from "@/components/file-tree-v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import {
|
import { filterReviewFiles, reviewDiffKinds, type RenderDiff } from "@/pages/session/v2/review-diff-kinds"
|
||||||
filterRenderableDiff,
|
|
||||||
filterReviewFiles,
|
|
||||||
reviewDiffKinds,
|
|
||||||
type RenderDiff,
|
|
||||||
} from "@/pages/session/v2/review-diff-kinds"
|
|
||||||
import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
|
import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
|
||||||
import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
|
import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
|
||||||
|
|
||||||
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
|
type ReviewDiff = FileDiffInfo | VcsFileDiff
|
||||||
|
|
||||||
export type ReviewPanelV2Props = {
|
export type ReviewPanelV2Props = {
|
||||||
title?: JSX.Element
|
title?: JSX.Element
|
||||||
|
|
@ -54,7 +49,7 @@ export type ReviewPanelV2Props = {
|
||||||
export function ReviewPanelV2(props: ReviewPanelV2Props) {
|
export function ReviewPanelV2(props: ReviewPanelV2Props) {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
const diffs = createMemo(() => props.diffs().filter(filterRenderableDiff))
|
const diffs = createMemo(() => props.diffs())
|
||||||
const filteredFiles = createMemo(() =>
|
const filteredFiles = createMemo(() =>
|
||||||
filterReviewFiles(
|
filterReviewFiles(
|
||||||
diffs().map((diff) => diff.file),
|
diffs().map((diff) => diff.file),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
|
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
|
||||||
import type { Message } from "@opencode-ai/sdk/v2/client"
|
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||||
import { diffs, message } from "./diffs"
|
import { diffs, message } from "./diffs"
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ const item = {
|
||||||
additions: 1,
|
additions: 1,
|
||||||
deletions: 1,
|
deletions: 1,
|
||||||
status: "modified",
|
status: "modified",
|
||||||
} satisfies SnapshotFileDiff
|
} satisfies FileDiffInfo
|
||||||
|
|
||||||
describe("diffs", () => {
|
describe("diffs", () => {
|
||||||
test("keeps valid arrays", () => {
|
test("keeps valid arrays", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
|
||||||
import type { Message } from "@opencode-ai/sdk/v2/client"
|
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
type Diff = SnapshotFileDiff | VcsFileDiff
|
type Diff = FileDiffInfo
|
||||||
|
|
||||||
function diff(value: unknown): value is Diff {
|
function diff(value: unknown): value is Diff {
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false
|
||||||
|
|
|
||||||
|
|
@ -3,96 +3,5 @@
|
||||||
## Migration context
|
## Migration context
|
||||||
|
|
||||||
- The TUI is being ported from legacy APIs to the new V2 APIs. New and migrated TUI behavior should use `sdk.client.v2` and the location-scoped data in `packages/tui/src/context/data.tsx` instead of adding dependencies on legacy sync state.
|
- The TUI is being ported from legacy APIs to the new V2 APIs. New and migrated TUI behavior should use `sdk.client.v2` and the location-scoped data in `packages/tui/src/context/data.tsx` instead of adding dependencies on legacy sync state.
|
||||||
- Preserve established TUI behavior unless the task intentionally changes it. When behavior, copy, keyboard interaction, or layout is unclear, compare the local V2 TUI with the latest released legacy TUI.
|
- Preserve established TUI behavior unless the task intentionally changes it.
|
||||||
- Run both versions in separate Terminal Control sessions and save PNG-only captures at equivalent states:
|
- Load the `opencode-dev` skill before interactively running, debugging, or verifying opencode's V2 CLI, TUI, or server.
|
||||||
|
|
||||||
```bash
|
|
||||||
# From packages/cli: local V2 TUI
|
|
||||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
|
||||||
|
|
||||||
# Released legacy TUI behavior reference
|
|
||||||
termctrl start opencode-legacy --host opentui --cols 112 --rows 34 -- bunx opencode-ai@latest
|
|
||||||
|
|
||||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2.png
|
|
||||||
termctrl save opencode-legacy --format png --out /tmp/opencode/legacy.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use the same viewport and send equivalent inputs to both sessions before comparing screenshots. The released CLI is a behavioral reference, not a source of V2 API design; keep the local implementation on V2 endpoints.
|
|
||||||
- Stop both sessions after comparison: `termctrl stop opencode-v2-dev` and `termctrl stop opencode-legacy`.
|
|
||||||
|
|
||||||
## Interactive debugging
|
|
||||||
|
|
||||||
- This package is the V2 CLI adapter. Run its `dev` script when testing the TUI; do not use the repository-root `bun dev`, which launches the legacy `packages/opencode` CLI.
|
|
||||||
- Run commands from `packages/cli`. Use `bun dev` for most debugging so the TUI starts with a private V2 server.
|
|
||||||
- Use `termctrl` for interactive checks instead of starting the TUI as a blocking foreground process. It provides a real PTY, handles OpenTUI's host handshake, and can save reviewable screenshots.
|
|
||||||
- Use a dedicated session name and do not reuse or kill an unrelated session.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
|
||||||
termctrl wait opencode-v2-dev "Ask anything" --timeout 20000
|
|
||||||
termctrl show opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
- Wait for visible text before interacting instead of relying on fixed sleeps. Use the text expected from the screen under test, such as `Ask anything` or `Connect a provider`.
|
|
||||||
- Drive the running TUI with `termctrl send`. Prefix typed input with `text:` and send control keys separately so the interaction matches real terminal input.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl send opencode-v2-dev 'text:example prompt' enter
|
|
||||||
termctrl send opencode-v2-dev ctrl-c
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use `termctrl show` after each meaningful interaction and inspect the full visible screen for rendering errors, stale state, error toasts, and unexpected exits.
|
|
||||||
- Save PNG evidence for every user-visible bug and fix. Do not save text captures; inspect the rendered PNG. Write temporary captures outside the repository unless the artifact is intended to be committed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2-tui.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- For resize-sensitive changes, resize the viewport, wait for the expected content, and capture the screen again:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl resize opencode-v2-dev --cols 100 --rows 30
|
|
||||||
termctrl show opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
- Source changes may require restarting the process. Use `termctrl restart opencode-v2-dev` rather than assuming the running TUI reloaded the change.
|
|
||||||
- To exercise background-service behavior, use `bun dev service start`, `bun dev service status`, and `bun dev service stop`.
|
|
||||||
- Always clean up the Terminal Control session when the check is complete:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl stop opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server/API debugging
|
|
||||||
|
|
||||||
- Use `bun dev api --help` from `packages/cli` to inspect the API debugging command. It sends one request to the V2 server using the same daemon discovery/auth path as the CLI.
|
|
||||||
- Use `bun dev api` to introspect the server-side data backing the TUI. This is useful when debugging UI bugs: compare what the screen renders with the raw session, message, event, agent, or health data returned by the API to determine whether the bug is in the server state, the client data layer, or the TUI rendering.
|
|
||||||
- `bun dev api` accepts either an OpenAPI operation ID or a raw HTTP method plus path:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun dev api get /health
|
|
||||||
bun dev api get /openapi.json
|
|
||||||
bun dev api <operationId> --param key=value
|
|
||||||
```
|
|
||||||
|
|
||||||
- Pass JSON request bodies with `--data`/`-d`; the command sets `content-type: application/json` automatically unless you provide a header. Add extra headers with `--header`/`-H name:value`.
|
|
||||||
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
|
|
||||||
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
|
|
||||||
|
|
||||||
## Debugger
|
|
||||||
|
|
||||||
- To debug the V2 CLI or TUI with Bun's inspector, launch the CLI entrypoint through Terminal Control with an inspector URL, then attach a debugger to that URL:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl start opencode-v2-debug --host opentui --cols 112 --rows 34 -- \
|
|
||||||
bun run --inspect=ws://localhost:6499/ src/index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use `--inspect-wait` or `--inspect-brk` when execution must pause until the debugger attaches.
|
|
||||||
- Use `termctrl logs opencode-v2-debug` for inspector output or startup failures emitted before the TUI renderer starts. Use `termctrl show` for the visible full-screen TUI.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- Run `bun typecheck` from `packages/cli` after CLI adapter changes.
|
|
||||||
- Run `bun typecheck` and `bun test` from `packages/tui` after shared TUI changes. Do not run tests from the repository root.
|
|
||||||
- Treat automated checks and Terminal Control smoke tests as complementary. For user-visible changes, verify initial render, the changed interaction, Ctrl-C exit behavior, and save a screenshot of the corrected state.
|
|
||||||
|
|
|
||||||
0
packages/cli/bin/opencode2.cjs
Normal file → Executable file
0
packages/cli/bin/opencode2.cjs
Normal file → Executable file
|
|
@ -18,7 +18,6 @@ await rm("dist", { recursive: true, force: true })
|
||||||
const singleFlag = process.argv.includes("--single")
|
const singleFlag = process.argv.includes("--single")
|
||||||
const baselineFlag = process.argv.includes("--baseline")
|
const baselineFlag = process.argv.includes("--baseline")
|
||||||
const skipInstall = process.argv.includes("--skip-install")
|
const skipInstall = process.argv.includes("--skip-install")
|
||||||
const sourcemapsFlag = process.argv.includes("--sourcemaps")
|
|
||||||
const plugin = createSolidTransformPlugin()
|
const plugin = createSolidTransformPlugin()
|
||||||
|
|
||||||
const allTargets: {
|
const allTargets: {
|
||||||
|
|
@ -74,7 +73,7 @@ for (const item of targets) {
|
||||||
external: ["node-gyp"],
|
external: ["node-gyp"],
|
||||||
format: "esm",
|
format: "esm",
|
||||||
minify: true,
|
minify: true,
|
||||||
sourcemap: sourcemapsFlag ? "linked" : "none",
|
sourcemap: "inline",
|
||||||
splitting: true,
|
splitting: true,
|
||||||
compile: {
|
compile: {
|
||||||
autoloadBunfig: false,
|
autoloadBunfig: false,
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@ function defaultCost(model: CurrentModel) {
|
||||||
|
|
||||||
export function runAgent(input: CurrentAgent): RunAgent {
|
export function runAgent(input: CurrentAgent): RunAgent {
|
||||||
return {
|
return {
|
||||||
name: input.id,
|
id: input.id,
|
||||||
|
name: input.name,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
mode: input.mode,
|
mode: input.mode,
|
||||||
hidden: input.hidden,
|
hidden: input.hidden,
|
||||||
|
|
@ -53,7 +54,7 @@ export function runCommand(input: CurrentCommand): RunCommand {
|
||||||
|
|
||||||
export function runSkill(input: CurrentSkill): RunCommand {
|
export function runSkill(input: CurrentSkill): RunCommand {
|
||||||
return {
|
return {
|
||||||
name: input.name,
|
name: input.id,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
source: "skill",
|
source: "skill",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -628,11 +628,11 @@ function emitEdit(state: State): void {
|
||||||
|
|
||||||
function emitPatch(state: State): void {
|
function emitPatch(state: State): void {
|
||||||
const file = path.join(process.cwd(), "src", "demo-format.ts")
|
const file = path.join(process.cwd(), "src", "demo-format.ts")
|
||||||
const ref = make(state, "apply_patch", {
|
const ref = make(state, "patch", {
|
||||||
patchText: "*** Begin Patch\n*** End Patch",
|
patchText: "*** Begin Patch\n*** End Patch",
|
||||||
})
|
})
|
||||||
doneTool(state, ref, {
|
doneTool(state, ref, {
|
||||||
title: "apply_patch",
|
title: "patch",
|
||||||
output: "",
|
output: "",
|
||||||
metadata: {
|
metadata: {
|
||||||
files: [
|
files: [
|
||||||
|
|
|
||||||
|
|
@ -333,10 +333,10 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
kind: "mention",
|
kind: "mention",
|
||||||
display: "@" + item.name,
|
display: "@" + item.name,
|
||||||
value: item.name,
|
value: item.id,
|
||||||
part: {
|
part: {
|
||||||
type: "agent",
|
type: "agent",
|
||||||
name: item.name,
|
name: item.id,
|
||||||
source: {
|
source: {
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,6 @@ type RunFooterOptions = {
|
||||||
theme: RunTheme
|
theme: RunTheme
|
||||||
keymap: Keymap<Renderable, KeyEvent>
|
keymap: Keymap<Renderable, KeyEvent>
|
||||||
tuiConfig: RunTuiConfig
|
tuiConfig: RunTuiConfig
|
||||||
backgroundSubagents: boolean
|
|
||||||
diffStyle: RunDiffStyle
|
diffStyle: RunDiffStyle
|
||||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||||
onQuestionReply: (input: QuestionReply) => void | Promise<void>
|
onQuestionReply: (input: QuestionReply) => void | Promise<void>
|
||||||
|
|
@ -326,7 +325,6 @@ export class RunFooter implements FooterApi {
|
||||||
theme: footer.theme,
|
theme: footer.theme,
|
||||||
diffStyle: options.diffStyle,
|
diffStyle: options.diffStyle,
|
||||||
tuiConfig: options.tuiConfig,
|
tuiConfig: options.tuiConfig,
|
||||||
backgroundSubagents: options.backgroundSubagents,
|
|
||||||
history: footer.history,
|
history: footer.history,
|
||||||
agent: options.agentLabel,
|
agent: options.agentLabel,
|
||||||
onSubmit: footer.handlePrompt,
|
onSubmit: footer.handlePrompt,
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,6 @@ type RunFooterViewProps = {
|
||||||
theme: () => RunTheme
|
theme: () => RunTheme
|
||||||
diffStyle?: RunDiffStyle
|
diffStyle?: RunDiffStyle
|
||||||
tuiConfig: RunTuiConfig
|
tuiConfig: RunTuiConfig
|
||||||
backgroundSubagents: boolean
|
|
||||||
history?: () => RunPrompt[]
|
history?: () => RunPrompt[]
|
||||||
agent: string
|
agent: string
|
||||||
onSubmit: (input: RunPrompt) => boolean
|
onSubmit: (input: RunPrompt) => boolean
|
||||||
|
|
@ -169,9 +168,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
|
|
||||||
return tabs().findIndex((item) => item.sessionID === sessionID) + 1
|
return tabs().findIndex((item) => item.sessionID === sessionID) + 1
|
||||||
})
|
})
|
||||||
const foregroundSubagents = createMemo(
|
const foregroundSubagents = createMemo(() => activeTabs().some((item) => !item.background))
|
||||||
() => props.backgroundSubagents && activeTabs().some((item) => !item.background),
|
|
||||||
)
|
|
||||||
const model = createMemo(() => {
|
const model = createMemo(() => {
|
||||||
const current = props.currentModel()
|
const current = props.currentModel()
|
||||||
return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined }
|
return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
|
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
import { truthy } from "@opencode-ai/core/flag/flag"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
|
@ -84,9 +83,6 @@ export async function runMini(input: MiniCommandInput) {
|
||||||
files: [],
|
files: [],
|
||||||
initialInput,
|
initialInput,
|
||||||
thinking: true,
|
thinking: true,
|
||||||
backgroundSubagents:
|
|
||||||
truthy("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS") ||
|
|
||||||
(process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS === undefined && truthy("OPENCODE_EXPERIMENTAL")),
|
|
||||||
replay: input.replay ?? true,
|
replay: input.replay ?? true,
|
||||||
replayLimit: input.replayLimit,
|
replayLimit: input.replayLimit,
|
||||||
demo: input.demo,
|
demo: input.demo,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,5 @@
|
||||||
import type {
|
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
EventSubscribeOutput,
|
import type { ReasoningPart, StepFinishPart, StepStartPart, TextPart, ToolPart } from "@opencode-ai/sdk/v2"
|
||||||
OpenCodeClient,
|
|
||||||
} from "@opencode-ai/client/promise"
|
|
||||||
import type {
|
|
||||||
ReasoningPart,
|
|
||||||
StepFinishPart,
|
|
||||||
StepStartPart,
|
|
||||||
TextPart,
|
|
||||||
ToolPart,
|
|
||||||
} from "@opencode-ai/sdk/v2"
|
|
||||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
import { UI } from "./ui"
|
import { UI } from "./ui"
|
||||||
|
|
@ -169,8 +160,8 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
event.type === "session.execution.settled" &&
|
event.type === "session.execution.interrupted" &&
|
||||||
event.data.outcome === "interrupted" &&
|
event.data.reason === "user" &&
|
||||||
(interrupted || permissionRejected || questionRejected || formCancelled)
|
(interrupted || permissionRejected || questionRejected || formCancelled)
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
|
|
@ -194,11 +185,12 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "session.text.started") {
|
if (event.type === "session.text.started") {
|
||||||
starts.set(event.data.textID, { id: partID(event.id), timestamp: time })
|
starts.set("text", { id: partID(event.id), timestamp: time })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (event.type === "session.text.ended") {
|
if (event.type === "session.text.ended") {
|
||||||
const started = starts.get(event.data.textID)
|
const started = starts.get("text")
|
||||||
|
starts.delete("text")
|
||||||
const part: TextPart = {
|
const part: TextPart = {
|
||||||
id: started?.id ?? partID(event.id),
|
id: started?.id ?? partID(event.id),
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
|
|
@ -212,18 +204,19 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "session.reasoning.started") {
|
if (event.type === "session.reasoning.started") {
|
||||||
starts.set(event.data.reasoningID, { id: partID(event.id), timestamp: time })
|
starts.set("reasoning", { id: partID(event.id), timestamp: time })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (event.type === "session.reasoning.ended" && input.thinking) {
|
if (event.type === "session.reasoning.ended" && input.thinking) {
|
||||||
const started = starts.get(event.data.reasoningID)
|
const started = starts.get("reasoning")
|
||||||
|
starts.delete("reasoning")
|
||||||
const part: ReasoningPart = {
|
const part: ReasoningPart = {
|
||||||
id: started?.id ?? partID(event.id),
|
id: started?.id ?? partID(event.id),
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
type: "reasoning",
|
type: "reasoning",
|
||||||
text: event.data.text,
|
text: event.data.text,
|
||||||
metadata: event.data.providerMetadata,
|
metadata: event.data.state,
|
||||||
time: { start: started?.timestamp ?? time, end: time },
|
time: { start: started?.timestamp ?? time, end: time },
|
||||||
}
|
}
|
||||||
if (emit("reasoning", time, { part })) continue
|
if (emit("reasoning", time, { part })) continue
|
||||||
|
|
@ -261,10 +254,10 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
id: current?.id ?? partID(event.id),
|
id: current?.id ?? partID(event.id),
|
||||||
timestamp: current?.timestamp ?? time,
|
timestamp: current?.timestamp ?? time,
|
||||||
assistantMessageID: event.data.assistantMessageID,
|
assistantMessageID: event.data.assistantMessageID,
|
||||||
tool: event.data.tool,
|
tool: current?.tool ?? "tool",
|
||||||
input: event.data.input,
|
input: event.data.input,
|
||||||
raw: current?.raw,
|
raw: current?.raw,
|
||||||
provider: event.data.provider,
|
provider: { executed: event.data.executed, state: event.data.state },
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -288,10 +281,9 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
metadata: {
|
metadata: {
|
||||||
structured: event.data.structured,
|
structured: event.data.structured,
|
||||||
content: event.data.content,
|
content: event.data.content,
|
||||||
outputPaths: event.data.outputPaths,
|
|
||||||
result: event.data.result,
|
result: event.data.result,
|
||||||
providerCall: current.provider,
|
providerCall: current.provider,
|
||||||
providerResult: event.data.provider,
|
providerResult: { executed: event.data.executed, state: event.data.resultState },
|
||||||
rawInput: current.raw,
|
rawInput: current.raw,
|
||||||
},
|
},
|
||||||
time: { start: current.timestamp, end: time },
|
time: { start: current.timestamp, end: time },
|
||||||
|
|
@ -318,7 +310,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
metadata: {
|
metadata: {
|
||||||
result: event.data.result,
|
result: event.data.result,
|
||||||
providerCall: current.provider,
|
providerCall: current.provider,
|
||||||
providerResult: event.data.provider,
|
providerResult: { executed: event.data.executed, state: event.data.resultState },
|
||||||
rawInput: current.raw,
|
rawInput: current.raw,
|
||||||
},
|
},
|
||||||
time: { start: current.timestamp, end: time },
|
time: { start: current.timestamp, end: time },
|
||||||
|
|
@ -353,16 +345,25 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message)
|
if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (event.type === "session.execution.settled") {
|
if (event.type === "session.execution.failed") {
|
||||||
if (event.data.outcome === "failure" && !emittedError && !questionRejected && !formCancelled) {
|
if (!emittedError && !questionRejected && !formCancelled) {
|
||||||
emittedError = true
|
emittedError = true
|
||||||
process.exitCode = 1
|
process.exitCode = 1
|
||||||
const error = event.data.error ?? { type: "unknown", message: "Session execution failed" }
|
if (!emit("error", time, { error: event.data.error })) UI.error(event.data.error.message)
|
||||||
if (!emit("error", time, { error })) UI.error(error.message)
|
|
||||||
}
|
}
|
||||||
if (event.data.outcome === "interrupted" && interrupted) process.exitCode = 130
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.type === "session.execution.interrupted") {
|
||||||
|
if (event.data.reason === "user" && interrupted) process.exitCode = 130
|
||||||
|
if (event.data.reason !== "user" && !emittedError) {
|
||||||
|
emittedError = true
|
||||||
|
process.exitCode = 1
|
||||||
|
const error = { type: "aborted" as const, message: `Session interrupted: ${event.data.reason}` }
|
||||||
|
if (!emit("error", time, { error })) UI.error(error.message)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.type === "session.execution.succeeded") return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,11 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
|
||||||
!explicitModel && !sessionModel
|
!explicitModel && !sessionModel
|
||||||
? await client.model
|
? await client.model
|
||||||
.default({ location: { directory: cwd, workspace } })
|
.default({ location: { directory: cwd, workspace } })
|
||||||
.then((result) =>
|
.then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined))
|
||||||
result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined,
|
|
||||||
)
|
|
||||||
: undefined
|
: undefined
|
||||||
const model = pickRunModel(explicitModel, input.variant, sessionModel, defaultModel)
|
const model = pickRunModel(explicitModel, input.variant, sessionModel, defaultModel)
|
||||||
if (input.variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id)
|
if (input.variant && !model)
|
||||||
|
return reportError(input, "Cannot select a variant before selecting a model", session?.id)
|
||||||
if (model) {
|
if (model) {
|
||||||
await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model })
|
await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model })
|
||||||
const available = await client.model.list({ location: { directory: cwd, workspace } })
|
const available = await client.model.list({ location: { directory: cwd, workspace } })
|
||||||
|
|
@ -112,9 +111,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
|
||||||
if (!session && input.title !== undefined) {
|
if (!session && input.title !== undefined) {
|
||||||
await client.session.rename({
|
await client.session.rename({
|
||||||
sessionID: selected.id,
|
sessionID: selected.id,
|
||||||
title:
|
title: input.title || prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""),
|
||||||
input.title ||
|
|
||||||
prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,7 +197,7 @@ async function validateAgent(client: OpenCodeClient, directory: string, name?: s
|
||||||
warning(`failed to list agents${server ? ` from ${server}` : ""}. Falling back to default agent`)
|
warning(`failed to list agents${server ? ` from ${server}` : ""}. Falling back to default agent`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const agent = agents.find((item) => item.name === name)
|
const agent = agents.find((item) => item.id === name)
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
warning(`agent "${name}" not found. Falling back to default agent`)
|
warning(`agent "${name}" not found. Falling back to default agent`)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,6 @@ export type LifecycleInput = {
|
||||||
model: RunInput["model"]
|
model: RunInput["model"]
|
||||||
variant: string | undefined
|
variant: string | undefined
|
||||||
tuiConfig: RunTuiConfig | Promise<RunTuiConfig>
|
tuiConfig: RunTuiConfig | Promise<RunTuiConfig>
|
||||||
backgroundSubagents: boolean
|
|
||||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||||
onQuestionReply: (input: QuestionReply) => void | Promise<void>
|
onQuestionReply: (input: QuestionReply) => void | Promise<void>
|
||||||
onQuestionReject: (input: QuestionReject) => void | Promise<void>
|
onQuestionReject: (input: QuestionReject) => void | Promise<void>
|
||||||
|
|
@ -236,7 +235,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
wrote,
|
wrote,
|
||||||
keymap,
|
keymap,
|
||||||
tuiConfig,
|
tuiConfig,
|
||||||
backgroundSubagents: input.backgroundSubagents,
|
|
||||||
diffStyle: tuiConfig.diff_style ?? "auto",
|
diffStyle: tuiConfig.diff_style ?? "auto",
|
||||||
onPermissionReply: input.onPermissionReply,
|
onPermissionReply: input.onPermissionReply,
|
||||||
onQuestionReply: input.onQuestionReply,
|
onQuestionReply: input.onQuestionReply,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ type RunRuntimeInput = {
|
||||||
files: RunInput["files"]
|
files: RunInput["files"]
|
||||||
initialInput?: string
|
initialInput?: string
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
backgroundSubagents: boolean
|
|
||||||
replay?: boolean
|
replay?: boolean
|
||||||
replayLimit?: number
|
replayLimit?: number
|
||||||
demo?: RunInput["demo"]
|
demo?: RunInput["demo"]
|
||||||
|
|
@ -75,7 +74,6 @@ type RunDeferredInput = {
|
||||||
files: RunInput["files"]
|
files: RunInput["files"]
|
||||||
initialInput?: string
|
initialInput?: string
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
backgroundSubagents: boolean
|
|
||||||
replay?: boolean
|
replay?: boolean
|
||||||
replayLimit?: number
|
replayLimit?: number
|
||||||
demo?: RunInput["demo"]
|
demo?: RunInput["demo"]
|
||||||
|
|
@ -270,7 +268,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
model: state.model,
|
model: state.model,
|
||||||
variant: state.activeVariant,
|
variant: state.activeVariant,
|
||||||
tuiConfig: tuiConfigTask,
|
tuiConfig: tuiConfigTask,
|
||||||
backgroundSubagents: input.backgroundSubagents,
|
|
||||||
onPermissionReply: async (next) => {
|
onPermissionReply: async (next) => {
|
||||||
if (state.demo?.permission(next)) {
|
if (state.demo?.permission(next)) {
|
||||||
return
|
return
|
||||||
|
|
@ -876,7 +873,6 @@ export async function runInteractiveDeferredMode(input: RunDeferredInput, deps?:
|
||||||
files: input.files,
|
files: input.files,
|
||||||
initialInput: input.initialInput,
|
initialInput: input.initialInput,
|
||||||
thinking: input.thinking,
|
thinking: input.thinking,
|
||||||
backgroundSubagents: input.backgroundSubagents,
|
|
||||||
replay: input.replay,
|
replay: input.replay,
|
||||||
replayLimit: input.replayLimit,
|
replayLimit: input.replayLimit,
|
||||||
demo: input.demo,
|
demo: input.demo,
|
||||||
|
|
@ -928,7 +924,6 @@ export async function runInteractiveMode(
|
||||||
files: input.files,
|
files: input.files,
|
||||||
initialInput: input.initialInput,
|
initialInput: input.initialInput,
|
||||||
thinking: input.thinking,
|
thinking: input.thinking,
|
||||||
backgroundSubagents: input.backgroundSubagents,
|
|
||||||
replay: input.replay,
|
replay: input.replay,
|
||||||
replayLimit: input.replayLimit,
|
replayLimit: input.replayLimit,
|
||||||
demo: input.demo,
|
demo: input.demo,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
// backgrounding is intentionally absent: subagent jobs block the parent
|
// backgrounding is intentionally absent: subagent jobs block the parent
|
||||||
// session, so only whole-session `v2.session.background(parentID)` exists.
|
// session, so only whole-session `v2.session.background(parentID)` exists.
|
||||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
import type { SessionMessage, SessionMessageAssistantTool, ToolPart } from "@opencode-ai/sdk/v2"
|
import type { SessionMessageAssistantTool, SessionMessageInfo, ToolPart } from "@opencode-ai/sdk/v2"
|
||||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||||
import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, StreamCommit } from "./types"
|
import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, StreamCommit } from "./types"
|
||||||
|
|
||||||
|
|
@ -35,54 +35,59 @@ export function outputText(content: ReadonlyArray<{ type: string; text?: string
|
||||||
export function legacyTool(input: {
|
export function legacyTool(input: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
callID: string
|
tool: SessionMessageAssistantTool
|
||||||
name: string
|
|
||||||
state: SessionMessageAssistantTool["state"]
|
|
||||||
time: SessionMessageAssistantTool["time"]
|
|
||||||
provider?: SessionMessageAssistantTool["provider"]
|
|
||||||
}): ToolPart {
|
}): ToolPart {
|
||||||
|
const tool = input.tool
|
||||||
|
const providerCall =
|
||||||
|
tool.executed === undefined && tool.providerState === undefined
|
||||||
|
? undefined
|
||||||
|
: { executed: tool.executed, state: tool.providerState }
|
||||||
|
const providerResult =
|
||||||
|
tool.executed === undefined && tool.providerResultState === undefined
|
||||||
|
? undefined
|
||||||
|
: { executed: tool.executed, state: tool.providerResultState }
|
||||||
const base = {
|
const base = {
|
||||||
id: `prt_${input.callID}`,
|
id: `prt_${tool.id}`,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: input.messageID,
|
messageID: input.messageID,
|
||||||
type: "tool" as const,
|
type: "tool" as const,
|
||||||
callID: input.callID,
|
callID: tool.id,
|
||||||
tool: input.name,
|
tool: tool.name,
|
||||||
}
|
}
|
||||||
if (input.state.status === "pending") {
|
if (tool.state.status === "streaming") {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
state: { status: "pending", input: {}, raw: input.state.input },
|
state: { status: "pending", input: {}, raw: tool.state.input },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (input.state.status === "running") {
|
if (tool.state.status === "running") {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
state: {
|
state: {
|
||||||
status: "running",
|
status: "running",
|
||||||
input: input.state.input,
|
input: tool.state.input,
|
||||||
title: input.name,
|
title: tool.name,
|
||||||
metadata: { structured: input.state.structured, content: input.state.content, providerCall: input.provider },
|
metadata: { structured: tool.state.structured, content: tool.state.content, providerCall },
|
||||||
time: { start: input.time.ran ?? input.time.created },
|
time: { start: tool.time.ran ?? tool.time.created },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (input.state.status === "completed") {
|
if (tool.state.status === "completed") {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
state: {
|
state: {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
input: input.state.input,
|
input: tool.state.input,
|
||||||
output: outputText(input.state.content),
|
output: outputText(tool.state.content),
|
||||||
title: input.name,
|
title: tool.name,
|
||||||
metadata: {
|
metadata: {
|
||||||
structured: input.state.structured,
|
structured: tool.state.structured,
|
||||||
content: input.state.content,
|
content: tool.state.content,
|
||||||
outputPaths: input.state.outputPaths,
|
result: tool.state.result,
|
||||||
result: input.state.result,
|
providerCall,
|
||||||
providerCall: input.provider,
|
providerResult,
|
||||||
},
|
},
|
||||||
time: { start: input.time.ran ?? input.time.created, end: input.time.completed ?? input.time.created },
|
time: { start: tool.time.ran ?? tool.time.created, end: tool.time.completed ?? tool.time.created },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,15 +95,16 @@ export function legacyTool(input: {
|
||||||
...base,
|
...base,
|
||||||
state: {
|
state: {
|
||||||
status: "error",
|
status: "error",
|
||||||
input: input.state.input,
|
input: tool.state.input,
|
||||||
error: input.state.error.message,
|
error: tool.state.error.message,
|
||||||
metadata: {
|
metadata: {
|
||||||
structured: input.state.structured,
|
structured: tool.state.structured,
|
||||||
content: input.state.content,
|
content: tool.state.content,
|
||||||
result: input.state.result,
|
result: tool.state.result,
|
||||||
providerCall: input.provider,
|
providerCall,
|
||||||
|
providerResult,
|
||||||
},
|
},
|
||||||
time: { start: input.time.ran ?? input.time.created, end: input.time.completed ?? input.time.created },
|
time: { start: tool.time.ran ?? tool.time.created, end: tool.time.completed ?? tool.time.created },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +144,7 @@ type ToolTrack = {
|
||||||
name: string
|
name: string
|
||||||
input: Record<string, unknown>
|
input: Record<string, unknown>
|
||||||
started: number
|
started: number
|
||||||
|
providerState?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChildState = {
|
type ChildState = {
|
||||||
|
|
@ -171,7 +178,7 @@ export type SubagentTrackerInput = {
|
||||||
export type SubagentTracker = {
|
export type SubagentTracker = {
|
||||||
main(event: V2Event): void
|
main(event: V2Event): void
|
||||||
foreign(sessionID: string, event: V2Event): void
|
foreign(sessionID: string, event: V2Event): void
|
||||||
hydrate(next: { messages: SessionMessage[]; active: Record<string, unknown> }): Promise<void>
|
hydrate(next: { messages: SessionMessageInfo[]; active: Record<string, unknown> }): Promise<void>
|
||||||
select(sessionID: string | undefined): void
|
select(sessionID: string | undefined): void
|
||||||
snapshot(): FooterSubagentState
|
snapshot(): FooterSubagentState
|
||||||
}
|
}
|
||||||
|
|
@ -225,6 +232,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
const hydrationOverflow = new Set<string>()
|
const hydrationOverflow = new Set<string>()
|
||||||
const hydrations = new Map<string, Promise<void>>()
|
const hydrations = new Map<string, Promise<void>>()
|
||||||
let selected: string | undefined
|
let selected: string | undefined
|
||||||
|
const fragmentKey = (messageID: string, partID: string) => `${messageID}\u0000${partID}`
|
||||||
|
|
||||||
const ensureChild = (sessionID: string): ChildState => {
|
const ensureChild = (sessionID: string): ChildState => {
|
||||||
const existing = children.get(sessionID)
|
const existing = children.get(sessionID)
|
||||||
|
|
@ -305,13 +313,9 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
const part = legacyTool({
|
const part = legacyTool({
|
||||||
sessionID: child.sessionID,
|
sessionID: child.sessionID,
|
||||||
messageID,
|
messageID,
|
||||||
callID: item.id,
|
tool: item,
|
||||||
name: item.name,
|
|
||||||
state: item.state,
|
|
||||||
time: item.time,
|
|
||||||
provider: item.provider,
|
|
||||||
})
|
})
|
||||||
if (item.state.status === "pending") return
|
if (item.state.status === "streaming") return
|
||||||
child.callIDs.add(item.id)
|
child.callIDs.add(item.id)
|
||||||
if (item.state.status === "running") {
|
if (item.state.status === "running") {
|
||||||
setFrame(child, `tool:${item.id}`, toolCommit(part, "start"))
|
setFrame(child, `tool:${item.id}`, toolCommit(part, "start"))
|
||||||
|
|
@ -322,7 +326,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
setFrame(child, `tool:${item.id}`, toolCommit(part, "final"))
|
setFrame(child, `tool:${item.id}`, toolCommit(part, "final"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const rebuild = (child: ChildState, messages: SessionMessage[]) => {
|
const rebuild = (child: ChildState, messages: SessionMessageInfo[]) => {
|
||||||
child.frames = []
|
child.frames = []
|
||||||
child.text.clear()
|
child.text.clear()
|
||||||
child.projectedText.clear()
|
child.projectedText.clear()
|
||||||
|
|
@ -339,31 +343,37 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
}
|
}
|
||||||
if (message.type !== "assistant") continue
|
if (message.type !== "assistant") continue
|
||||||
child.messageIDs.add(message.id)
|
child.messageIDs.add(message.id)
|
||||||
|
let textOrdinal = 0
|
||||||
|
let reasoningOrdinal = 0
|
||||||
for (const item of message.content) {
|
for (const item of message.content) {
|
||||||
if (item.type === "text") {
|
if (item.type === "text") {
|
||||||
child.text.set(item.id, item.text)
|
const id = `text:${textOrdinal++}`
|
||||||
child.projectedText.set(item.id, item.text)
|
const key = fragmentKey(message.id, id)
|
||||||
setFrame(child, `text:${item.id}`, {
|
child.text.set(key, item.text)
|
||||||
|
child.projectedText.set(key, item.text)
|
||||||
|
setFrame(child, key, {
|
||||||
kind: "assistant",
|
kind: "assistant",
|
||||||
source: "assistant",
|
source: "assistant",
|
||||||
text: item.text,
|
text: item.text,
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: message.id,
|
messageID: message.id,
|
||||||
partID: item.id,
|
partID: id,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (item.type === "reasoning") {
|
if (item.type === "reasoning") {
|
||||||
child.reasoning.set(item.id, item.text)
|
const id = `reasoning:${reasoningOrdinal++}`
|
||||||
child.projectedReasoning.set(item.id, item.text)
|
const key = fragmentKey(message.id, id)
|
||||||
|
child.reasoning.set(key, item.text)
|
||||||
|
child.projectedReasoning.set(key, item.text)
|
||||||
if (input.thinking)
|
if (input.thinking)
|
||||||
setFrame(child, `reasoning:${item.id}`, {
|
setFrame(child, key, {
|
||||||
kind: "reasoning",
|
kind: "reasoning",
|
||||||
source: "reasoning",
|
source: "reasoning",
|
||||||
text: `Thinking: ${item.text}`,
|
text: `Thinking: ${item.text}`,
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: message.id,
|
messageID: message.id,
|
||||||
partID: item.id,
|
partID: id,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -401,7 +411,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
for (const [id, prompt] of pendingPrompts) {
|
for (const [id, prompt] of pendingPrompts) {
|
||||||
if (!child.prompts.has(id)) child.prompts.set(id, prompt)
|
if (!child.prompts.has(id)) child.prompts.set(id, prompt)
|
||||||
}
|
}
|
||||||
rebuild(child, structuredClone(response.data).toReversed() as SessionMessage[])
|
rebuild(child, structuredClone(response.data).toReversed() as SessionMessageInfo[])
|
||||||
for (const [id, tool] of pendingTools) {
|
for (const [id, tool] of pendingTools) {
|
||||||
if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool)
|
if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool)
|
||||||
}
|
}
|
||||||
|
|
@ -467,74 +477,88 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
input.emit()
|
input.emit()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.type === "session.text.started") {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event.type === "session.text.delta") {
|
if (event.type === "session.text.delta") {
|
||||||
const projected = child.projectedText.get(event.data.textID)
|
const id = `text:${event.data.ordinal}`
|
||||||
|
const key = fragmentKey(event.data.assistantMessageID, id)
|
||||||
|
const projected = child.projectedText.get(key)
|
||||||
const covered = projected?.indexOf(event.data.delta) ?? -1
|
const covered = projected?.indexOf(event.data.delta) ?? -1
|
||||||
if (projected && covered >= 0) {
|
if (projected && covered >= 0) {
|
||||||
child.projectedText.set(event.data.textID, projected.slice(covered + event.data.delta.length))
|
child.projectedText.set(key, projected.slice(covered + event.data.delta.length))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const next = (child.text.get(event.data.textID) ?? "") + event.data.delta
|
const next = (child.text.get(key) ?? "") + event.data.delta
|
||||||
child.text.set(event.data.textID, next)
|
child.text.set(key, next)
|
||||||
setFrame(child, `text:${event.data.textID}`, {
|
setFrame(child, key, {
|
||||||
kind: "assistant",
|
kind: "assistant",
|
||||||
source: "assistant",
|
source: "assistant",
|
||||||
text: next,
|
text: next,
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
partID: event.data.textID,
|
partID: id,
|
||||||
})
|
})
|
||||||
touch(child, event.created)
|
touch(child, event.created)
|
||||||
notifyDetail(child)
|
notifyDetail(child)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "session.text.ended") {
|
if (event.type === "session.text.ended") {
|
||||||
child.text.set(event.data.textID, event.data.text)
|
const id = `text:${event.data.ordinal}`
|
||||||
child.projectedText.delete(event.data.textID)
|
const key = fragmentKey(event.data.assistantMessageID, id)
|
||||||
setFrame(child, `text:${event.data.textID}`, {
|
child.text.set(key, event.data.text)
|
||||||
|
child.projectedText.delete(key)
|
||||||
|
setFrame(child, key, {
|
||||||
kind: "assistant",
|
kind: "assistant",
|
||||||
source: "assistant",
|
source: "assistant",
|
||||||
text: event.data.text,
|
text: event.data.text,
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
partID: event.data.textID,
|
partID: id,
|
||||||
})
|
})
|
||||||
touch(child, event.created)
|
touch(child, event.created)
|
||||||
notifyDetail(child)
|
notifyDetail(child)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.type === "session.reasoning.started") {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event.type === "session.reasoning.delta") {
|
if (event.type === "session.reasoning.delta") {
|
||||||
const projected = child.projectedReasoning.get(event.data.reasoningID)
|
const id = `reasoning:${event.data.ordinal}`
|
||||||
|
const key = fragmentKey(event.data.assistantMessageID, id)
|
||||||
|
const projected = child.projectedReasoning.get(key)
|
||||||
const covered = projected?.indexOf(event.data.delta) ?? -1
|
const covered = projected?.indexOf(event.data.delta) ?? -1
|
||||||
if (projected && covered >= 0) {
|
if (projected && covered >= 0) {
|
||||||
child.projectedReasoning.set(event.data.reasoningID, projected.slice(covered + event.data.delta.length))
|
child.projectedReasoning.set(key, projected.slice(covered + event.data.delta.length))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const next = (child.reasoning.get(event.data.reasoningID) ?? "") + event.data.delta
|
const next = (child.reasoning.get(key) ?? "") + event.data.delta
|
||||||
child.reasoning.set(event.data.reasoningID, next)
|
child.reasoning.set(key, next)
|
||||||
if (!input.thinking) return
|
if (!input.thinking) return
|
||||||
setFrame(child, `reasoning:${event.data.reasoningID}`, {
|
setFrame(child, key, {
|
||||||
kind: "reasoning",
|
kind: "reasoning",
|
||||||
source: "reasoning",
|
source: "reasoning",
|
||||||
text: `Thinking: ${next}`,
|
text: `Thinking: ${next}`,
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
partID: event.data.reasoningID,
|
partID: id,
|
||||||
})
|
})
|
||||||
notifyDetail(child)
|
notifyDetail(child)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "session.reasoning.ended") {
|
if (event.type === "session.reasoning.ended") {
|
||||||
child.reasoning.set(event.data.reasoningID, event.data.text)
|
const id = `reasoning:${event.data.ordinal}`
|
||||||
child.projectedReasoning.delete(event.data.reasoningID)
|
const key = fragmentKey(event.data.assistantMessageID, id)
|
||||||
|
child.reasoning.set(key, event.data.text)
|
||||||
|
child.projectedReasoning.delete(key)
|
||||||
if (!input.thinking) return
|
if (!input.thinking) return
|
||||||
setFrame(child, `reasoning:${event.data.reasoningID}`, {
|
setFrame(child, key, {
|
||||||
kind: "reasoning",
|
kind: "reasoning",
|
||||||
source: "reasoning",
|
source: "reasoning",
|
||||||
text: `Thinking: ${event.data.text}`,
|
text: `Thinking: ${event.data.text}`,
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
partID: event.data.reasoningID,
|
partID: id,
|
||||||
})
|
})
|
||||||
notifyDetail(child)
|
notifyDetail(child)
|
||||||
return
|
return
|
||||||
|
|
@ -548,17 +572,19 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
if (child.finishedTools.has(event.data.callID)) return
|
if (child.finishedTools.has(event.data.callID)) return
|
||||||
const current = child.tools.get(event.data.callID)
|
const current = child.tools.get(event.data.callID)
|
||||||
child.tools.set(event.data.callID, {
|
child.tools.set(event.data.callID, {
|
||||||
name: event.data.tool,
|
name: current?.name ?? "tool",
|
||||||
input: event.data.input,
|
input: event.data.input,
|
||||||
started: current?.started ?? event.created,
|
started: current?.started ?? event.created,
|
||||||
|
providerState: event.data.state,
|
||||||
})
|
})
|
||||||
childTool(
|
childTool(
|
||||||
child,
|
child,
|
||||||
structuredClone({
|
structuredClone({
|
||||||
type: "tool",
|
type: "tool",
|
||||||
id: event.data.callID,
|
id: event.data.callID,
|
||||||
name: event.data.tool,
|
name: current?.name ?? "tool",
|
||||||
provider: event.data.provider,
|
executed: event.data.executed,
|
||||||
|
providerState: event.data.state,
|
||||||
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
||||||
time: { created: current?.started ?? event.created, ran: event.created },
|
time: { created: current?.started ?? event.created, ran: event.created },
|
||||||
}) as SessionMessageAssistantTool,
|
}) as SessionMessageAssistantTool,
|
||||||
|
|
@ -578,7 +604,9 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
type: "tool",
|
type: "tool",
|
||||||
id: event.data.callID,
|
id: event.data.callID,
|
||||||
name: current?.name ?? "tool",
|
name: current?.name ?? "tool",
|
||||||
provider: event.data.provider,
|
executed: event.data.executed,
|
||||||
|
providerState: current?.providerState,
|
||||||
|
providerResultState: event.data.resultState,
|
||||||
state: failed
|
state: failed
|
||||||
? {
|
? {
|
||||||
status: "error",
|
status: "error",
|
||||||
|
|
@ -593,7 +621,6 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
input: current?.input ?? {},
|
input: current?.input ?? {},
|
||||||
structured: event.data.structured,
|
structured: event.data.structured,
|
||||||
content: event.data.content,
|
content: event.data.content,
|
||||||
outputPaths: event.data.outputPaths,
|
|
||||||
result: event.data.result,
|
result: event.data.result,
|
||||||
},
|
},
|
||||||
time: {
|
time: {
|
||||||
|
|
@ -608,6 +635,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
notifyDetail(child)
|
notifyDetail(child)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.type === "session.step.ended") return
|
||||||
if (event.type === "session.step.failed") {
|
if (event.type === "session.step.failed") {
|
||||||
setFrame(child, `error:step:${event.data.assistantMessageID}`, {
|
setFrame(child, `error:step:${event.data.assistantMessageID}`, {
|
||||||
kind: "error",
|
kind: "error",
|
||||||
|
|
@ -620,9 +648,23 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
notifyDetail(child)
|
notifyDetail(child)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "session.execution.settled") {
|
if (event.type === "session.execution.started") {
|
||||||
|
child.status = "running"
|
||||||
|
touch(child, event.created)
|
||||||
|
input.emit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.type === "session.execution.succeeded" ||
|
||||||
|
event.type === "session.execution.failed" ||
|
||||||
|
event.type === "session.execution.interrupted"
|
||||||
|
) {
|
||||||
child.status =
|
child.status =
|
||||||
event.data.outcome === "success" ? "completed" : event.data.outcome === "interrupted" ? "cancelled" : "error"
|
event.type === "session.execution.succeeded"
|
||||||
|
? "completed"
|
||||||
|
: event.type === "session.execution.interrupted"
|
||||||
|
? "cancelled"
|
||||||
|
: "error"
|
||||||
touch(child, event.created)
|
touch(child, event.created)
|
||||||
input.emit()
|
input.emit()
|
||||||
}
|
}
|
||||||
|
|
@ -644,8 +686,12 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
|
|
||||||
return {
|
return {
|
||||||
main(event) {
|
main(event) {
|
||||||
|
if (event.type === "session.tool.input.started") {
|
||||||
|
if (event.data.name === "subagent") pendingCalls.set(event.data.callID, {})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event.type === "session.tool.called") {
|
if (event.type === "session.tool.called") {
|
||||||
if (event.data.tool === "subagent") pendingCalls.set(event.data.callID, event.data.input)
|
if (pendingCalls.has(event.data.callID)) pendingCalls.set(event.data.callID, event.data.input)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "session.tool.failed") {
|
if (event.type === "session.tool.failed") {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import type {
|
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
EventSubscribeOutput,
|
|
||||||
OpenCodeClient,
|
|
||||||
} from "@opencode-ai/client/promise"
|
|
||||||
import type {
|
import type {
|
||||||
PermissionRequest,
|
PermissionRequest,
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
SessionMessage,
|
SessionMessageInfo,
|
||||||
SessionMessageAssistant,
|
|
||||||
SessionMessageAssistantTool,
|
SessionMessageAssistantTool,
|
||||||
} from "@opencode-ai/sdk/v2"
|
} from "@opencode-ai/sdk/v2"
|
||||||
import { Event } from "@opencode-ai/schema/event"
|
import { Event } from "@opencode-ai/schema/event"
|
||||||
|
|
@ -101,6 +97,7 @@ type ToolState = {
|
||||||
input: Record<string, unknown>
|
input: Record<string, unknown>
|
||||||
started: number
|
started: number
|
||||||
running: boolean
|
running: boolean
|
||||||
|
providerState?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
|
@ -264,8 +261,7 @@ function shellTerminal(
|
||||||
: shell.status === "exited"
|
: shell.status === "exited"
|
||||||
? `Shell exited with code ${shell.exit ?? "unknown"}`
|
? `Shell exited with code ${shell.exit ?? "unknown"}`
|
||||||
: `Shell ${shell.status}`
|
: `Shell ${shell.status}`
|
||||||
if (!error)
|
if (!error) return [shellCommit(callID, command, { text, phase: "progress", toolState: "completed" })]
|
||||||
return [shellCommit(callID, command, { text, phase: "progress", toolState: "completed" })]
|
|
||||||
return [
|
return [
|
||||||
...(text ? [shellCommit(callID, command, { text, phase: "progress", toolState: "running" })] : []),
|
...(text ? [shellCommit(callID, command, { text, phase: "progress", toolState: "running" })] : []),
|
||||||
shellCommit(callID, command, { text: error, phase: "final", toolState: "error", toolError: error }),
|
shellCommit(callID, command, { text: error, phase: "final", toolState: "error", toolError: error }),
|
||||||
|
|
@ -310,9 +306,7 @@ async function resolveSelectedModel(input: StreamInput, next: Pick<SessionTurnIn
|
||||||
.then((response) => response.model)
|
.then((response) => response.model)
|
||||||
if (session) return { ...session, variant: next.variant }
|
if (session) return { ...session, variant: next.variant }
|
||||||
|
|
||||||
const fallback = await input.sdk.model
|
const fallback = await input.sdk.model.default(undefined, { signal: next.signal }).then((response) => response.data)
|
||||||
.default(undefined, { signal: next.signal })
|
|
||||||
.then((response) => response.data)
|
|
||||||
if (!fallback) return
|
if (!fallback) return
|
||||||
return { providerID: fallback.providerID, id: fallback.id, variant: next.variant }
|
return { providerID: fallback.providerID, id: fallback.id, variant: next.variant }
|
||||||
}
|
}
|
||||||
|
|
@ -393,13 +387,9 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
const part = legacyTool({
|
const part = legacyTool({
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID,
|
messageID,
|
||||||
callID: item.id,
|
tool: item,
|
||||||
name: item.name,
|
|
||||||
state: item.state,
|
|
||||||
time: item.time,
|
|
||||||
provider: item.provider,
|
|
||||||
})
|
})
|
||||||
if (item.state.status === "pending") return
|
if (item.state.status === "streaming") return
|
||||||
if (item.state.status === "running") {
|
if (item.state.status === "running") {
|
||||||
if (state.tools.get(item.id)?.running) return
|
if (state.tools.get(item.id)?.running) return
|
||||||
state.tools.set(item.id, {
|
state.tools.set(item.id, {
|
||||||
|
|
@ -408,6 +398,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
input: item.state.input,
|
input: item.state.input,
|
||||||
started: item.time.ran ?? item.time.created,
|
started: item.time.ran ?? item.time.created,
|
||||||
running: true,
|
running: true,
|
||||||
|
providerState: item.providerState,
|
||||||
})
|
})
|
||||||
write([toolCommit(part, "start")], { phase: "running", status: `running ${item.name}` })
|
write([toolCommit(part, "start")], { phase: "running", status: `running ${item.name}` })
|
||||||
return
|
return
|
||||||
|
|
@ -426,7 +417,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMessage = (message: SessionMessage, render: boolean, reuseVisibleWait: boolean) => {
|
const renderMessage = (message: SessionMessageInfo, render: boolean, reuseVisibleWait: boolean) => {
|
||||||
if (message.type === "user") {
|
if (message.type === "user") {
|
||||||
const waiting = state.wait?.messageID === message.id
|
const waiting = state.wait?.messageID === message.id
|
||||||
if (waiting && state.wait) state.wait.promoted = true
|
if (waiting && state.wait) state.wait.promoted = true
|
||||||
|
|
@ -447,41 +438,44 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (message.type === "shell") {
|
if (message.type === "shell") {
|
||||||
state.shellCommands.set(message.shell.id, message.shell.command)
|
state.shellCommands.set(message.shellID, message.command)
|
||||||
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shell.id
|
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shellID
|
||||||
const completed = message.time.completed !== undefined
|
const completed = message.time.completed !== undefined
|
||||||
if (!render) {
|
if (!render) {
|
||||||
// Suppressed history: mark settled shells rendered so live redelivery
|
// Suppressed history: mark settled shells rendered so live redelivery
|
||||||
// stays silent. A still-running shell stays unmarked and renders in
|
// stays silent. A still-running shell stays unmarked and renders in
|
||||||
// full when its live shell.ended event arrives.
|
// full when its live shell.ended event arrives.
|
||||||
if (completed) {
|
if (completed) {
|
||||||
state.shellStarted.add(message.shell.id)
|
state.shellStarted.add(message.shellID)
|
||||||
state.shellEnded.add(message.shell.id)
|
state.shellEnded.add(message.shellID)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!state.shellStarted.has(message.shell.id)) {
|
if (!state.shellStarted.has(message.shellID)) {
|
||||||
state.shellStarted.add(message.shell.id)
|
state.shellStarted.add(message.shellID)
|
||||||
write([
|
write([
|
||||||
shellCommit(message.shell.id, message.shell.command, {
|
shellCommit(message.shellID, message.command, {
|
||||||
text: "running shell",
|
text: "running shell",
|
||||||
phase: "start",
|
phase: "start",
|
||||||
toolState: "running",
|
toolState: "running",
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
if (completed && message.output && !state.shellEnded.has(message.shell.id)) {
|
if (completed && message.output && !state.shellEnded.has(message.shellID)) {
|
||||||
state.shellEnded.add(message.shell.id)
|
state.shellEnded.add(message.shellID)
|
||||||
write(shellTerminal(message.shell.id, message.shell.command, message.shell, message.output))
|
write(shellTerminal(message.shellID, message.command, message, message.output))
|
||||||
}
|
}
|
||||||
if (completed && state.shellWait?.callID === message.shell.id) state.shellWait.resolve()
|
if (completed && state.shellWait?.callID === message.shellID) state.shellWait.resolve()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (message.type !== "assistant") return
|
if (message.type !== "assistant") return
|
||||||
state.messageIDs.add(message.id)
|
state.messageIDs.add(message.id)
|
||||||
|
let textOrdinal = 0
|
||||||
|
let reasoningOrdinal = 0
|
||||||
for (const item of message.content) {
|
for (const item of message.content) {
|
||||||
if (item.type === "text") {
|
if (item.type === "text") {
|
||||||
const key = streamPartKey(message.id, item.id)
|
const id = `text:${textOrdinal++}`
|
||||||
|
const key = streamPartKey(message.id, id)
|
||||||
const sent = state.text.get(key)?.length ?? 0
|
const sent = state.text.get(key)?.length ?? 0
|
||||||
state.text.set(key, item.text)
|
state.text.set(key, item.text)
|
||||||
if (render) state.projectedText.set(key, item.text)
|
if (render) state.projectedText.set(key, item.text)
|
||||||
|
|
@ -493,13 +487,14 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
text: item.text.slice(sent),
|
text: item.text.slice(sent),
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: message.id,
|
messageID: message.id,
|
||||||
partID: item.id,
|
partID: id,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (item.type === "reasoning") {
|
if (item.type === "reasoning") {
|
||||||
const key = streamPartKey(message.id, item.id)
|
const id = `reasoning:${reasoningOrdinal++}`
|
||||||
|
const key = streamPartKey(message.id, id)
|
||||||
const sent = state.reasoning.get(key)?.length ?? 0
|
const sent = state.reasoning.get(key)?.length ?? 0
|
||||||
state.reasoning.set(key, item.text)
|
state.reasoning.set(key, item.text)
|
||||||
if (render) state.projectedReasoning.set(key, item.text)
|
if (render) state.projectedReasoning.set(key, item.text)
|
||||||
|
|
@ -511,7 +506,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
text: sent === 0 ? `Thinking: ${item.text}` : item.text.slice(sent),
|
text: sent === 0 ? `Thinking: ${item.text}` : item.text.slice(sent),
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: message.id,
|
messageID: message.id,
|
||||||
partID: item.id,
|
partID: id,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
continue
|
continue
|
||||||
|
|
@ -539,7 +534,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
input.sdk.question.list({ sessionID: input.sessionID }),
|
input.sdk.question.list({ sessionID: input.sessionID }),
|
||||||
input.sdk.session.active(),
|
input.sdk.session.active(),
|
||||||
])
|
])
|
||||||
const projected = structuredClone(messages.data).toReversed() as SessionMessage[]
|
const projected = structuredClone(messages.data).toReversed() as SessionMessageInfo[]
|
||||||
for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait)
|
for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait)
|
||||||
state.permissions = permissions.map(permission)
|
state.permissions = permissions.map(permission)
|
||||||
state.questions = questions.map(question)
|
state.questions = questions.map(question)
|
||||||
|
|
@ -626,8 +621,12 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
if (owned) wait.resolve()
|
if (owned) wait.resolve()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.type === "session.text.started") {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event.type === "session.text.delta") {
|
if (event.type === "session.text.delta") {
|
||||||
const key = streamPartKey(event.data.assistantMessageID, event.data.textID)
|
const id = `text:${event.data.ordinal}`
|
||||||
|
const key = streamPartKey(event.data.assistantMessageID, id)
|
||||||
const projected = state.projectedText.get(key)
|
const projected = state.projectedText.get(key)
|
||||||
const covered = projected?.indexOf(event.data.delta) ?? -1
|
const covered = projected?.indexOf(event.data.delta) ?? -1
|
||||||
if (projected && covered >= 0) {
|
if (projected && covered >= 0) {
|
||||||
|
|
@ -643,13 +642,14 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
text: event.data.delta,
|
text: event.data.delta,
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
partID: event.data.textID,
|
partID: id,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "session.text.ended") {
|
if (event.type === "session.text.ended") {
|
||||||
const key = streamPartKey(event.data.assistantMessageID, event.data.textID)
|
const id = `text:${event.data.ordinal}`
|
||||||
|
const key = streamPartKey(event.data.assistantMessageID, id)
|
||||||
const previous = state.text.get(key) ?? ""
|
const previous = state.text.get(key) ?? ""
|
||||||
if (event.data.text.length > previous.length)
|
if (event.data.text.length > previous.length)
|
||||||
write([
|
write([
|
||||||
|
|
@ -659,15 +659,19 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
text: event.data.text.slice(previous.length),
|
text: event.data.text.slice(previous.length),
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
partID: event.data.textID,
|
partID: id,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
state.text.set(key, event.data.text)
|
state.text.set(key, event.data.text)
|
||||||
state.projectedText.delete(key)
|
state.projectedText.delete(key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.type === "session.reasoning.started") {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event.type === "session.reasoning.delta") {
|
if (event.type === "session.reasoning.delta") {
|
||||||
const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID)
|
const id = `reasoning:${event.data.ordinal}`
|
||||||
|
const key = streamPartKey(event.data.assistantMessageID, id)
|
||||||
const projected = state.projectedReasoning.get(key)
|
const projected = state.projectedReasoning.get(key)
|
||||||
const covered = projected?.indexOf(event.data.delta) ?? -1
|
const covered = projected?.indexOf(event.data.delta) ?? -1
|
||||||
if (projected && covered >= 0) {
|
if (projected && covered >= 0) {
|
||||||
|
|
@ -684,13 +688,14 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
text: previous ? event.data.delta : `Thinking: ${event.data.delta}`,
|
text: previous ? event.data.delta : `Thinking: ${event.data.delta}`,
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
partID: event.data.reasoningID,
|
partID: id,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "session.reasoning.ended") {
|
if (event.type === "session.reasoning.ended") {
|
||||||
const key = streamPartKey(event.data.assistantMessageID, event.data.reasoningID)
|
const id = `reasoning:${event.data.ordinal}`
|
||||||
|
const key = streamPartKey(event.data.assistantMessageID, id)
|
||||||
const previous = state.reasoning.get(key) ?? ""
|
const previous = state.reasoning.get(key) ?? ""
|
||||||
if (input.thinking && event.data.text.length > previous.length)
|
if (input.thinking && event.data.text.length > previous.length)
|
||||||
write([
|
write([
|
||||||
|
|
@ -700,7 +705,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
text: previous ? event.data.text.slice(previous.length) : `Thinking: ${event.data.text}`,
|
text: previous ? event.data.text.slice(previous.length) : `Thinking: ${event.data.text}`,
|
||||||
phase: "progress",
|
phase: "progress",
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
partID: event.data.reasoningID,
|
partID: id,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
state.reasoning.set(key, event.data.text)
|
state.reasoning.set(key, event.data.text)
|
||||||
|
|
@ -723,8 +728,9 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
const item = structuredClone({
|
const item = structuredClone({
|
||||||
type: "tool",
|
type: "tool",
|
||||||
id: event.data.callID,
|
id: event.data.callID,
|
||||||
name: event.data.tool,
|
name: current?.name ?? "tool",
|
||||||
provider: event.data.provider,
|
executed: event.data.executed,
|
||||||
|
providerState: event.data.state,
|
||||||
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
||||||
time: { created: current?.started ?? event.created, ran: event.created },
|
time: { created: current?.started ?? event.created, ran: event.created },
|
||||||
}) as SessionMessageAssistantTool
|
}) as SessionMessageAssistantTool
|
||||||
|
|
@ -739,7 +745,9 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
type: "tool",
|
type: "tool",
|
||||||
id: event.data.callID,
|
id: event.data.callID,
|
||||||
name: current?.name ?? "tool",
|
name: current?.name ?? "tool",
|
||||||
provider: event.data.provider,
|
executed: event.data.executed,
|
||||||
|
providerState: current?.providerState,
|
||||||
|
providerResultState: event.data.resultState,
|
||||||
state: failed
|
state: failed
|
||||||
? {
|
? {
|
||||||
status: "error",
|
status: "error",
|
||||||
|
|
@ -754,7 +762,6 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
input: current?.input ?? {},
|
input: current?.input ?? {},
|
||||||
structured: event.data.structured,
|
structured: event.data.structured,
|
||||||
content: event.data.content,
|
content: event.data.content,
|
||||||
outputPaths: event.data.outputPaths,
|
|
||||||
result: event.data.result,
|
result: event.data.result,
|
||||||
},
|
},
|
||||||
time: { created: current?.started ?? event.created, ran: current?.started, completed: event.created },
|
time: { created: current?.started ?? event.created, ran: current?.started, completed: event.created },
|
||||||
|
|
@ -791,7 +798,6 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
event.data.tokens.cache.write
|
event.data.tokens.cache.write
|
||||||
const usage = total > 0 ? total.toLocaleString() : ""
|
const usage = total > 0 ? total.toLocaleString() : ""
|
||||||
write([], {
|
write([], {
|
||||||
phase: event.data.finish === "tool-calls" ? "running" : "idle",
|
|
||||||
usage: event.data.cost ? `${usage} · ${money.format(event.data.cost)}` : usage,
|
usage: event.data.cost ? `${usage} · ${money.format(event.data.cost)}` : usage,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
@ -802,21 +808,33 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
write([{ kind: "error", source: "system", text: errorMessage(event.data.error), phase: "start" }])
|
write([{ kind: "error", source: "system", text: errorMessage(event.data.error), phase: "start" }])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "session.execution.settled") {
|
if (event.type === "session.execution.started") {
|
||||||
|
write([], { phase: "running" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.type === "session.execution.succeeded" ||
|
||||||
|
event.type === "session.execution.failed" ||
|
||||||
|
event.type === "session.execution.interrupted"
|
||||||
|
) {
|
||||||
write([], { phase: "idle", status: "" })
|
write([], { phase: "idle", status: "" })
|
||||||
const current = state.wait
|
const current = state.wait
|
||||||
if (!current || (!current.promoted && !current.interrupted)) return
|
if (!current || (!current.promoted && !current.interrupted)) return
|
||||||
state.wait = undefined
|
state.wait = undefined
|
||||||
if (current.interrupted) {
|
if (current.interrupted && event.type === "session.execution.interrupted" && event.data.reason === "user") {
|
||||||
current.resolve()
|
current.resolve()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.data.outcome === "failure") {
|
if (event.type === "session.execution.failed") {
|
||||||
if (current.failureRendered) {
|
if (current.failureRendered) {
|
||||||
current.resolve()
|
current.resolve()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
current.reject(new Error(event.data.error ? errorMessage(event.data.error) : "Session execution failed"))
|
current.reject(new Error(errorMessage(event.data.error)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.type === "session.execution.interrupted") {
|
||||||
|
current.reject(new Error(`Session interrupted: ${event.data.reason}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
current.resolve()
|
current.resolve()
|
||||||
|
|
@ -1014,18 +1032,12 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next.agent) {
|
if (next.agent) {
|
||||||
await input.sdk.session.switchAgent(
|
await input.sdk.session.switchAgent({ sessionID: input.sessionID, agent: next.agent }, { signal: next.signal })
|
||||||
{ sessionID: input.sessionID, agent: next.agent },
|
|
||||||
{ signal: next.signal },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const selected = await resolveSelectedModel(input, next)
|
const selected = await resolveSelectedModel(input, next)
|
||||||
if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model")
|
if (next.variant && !selected) throw new Error("Cannot select a variant before selecting a model")
|
||||||
if (selected)
|
if (selected)
|
||||||
await input.sdk.session.switchModel(
|
await input.sdk.session.switchModel({ sessionID: input.sessionID, model: selected }, { signal: next.signal })
|
||||||
{ sessionID: input.sessionID, model: selected },
|
|
||||||
{ signal: next.signal },
|
|
||||||
)
|
|
||||||
|
|
||||||
const prepared = await Promise.all((next.includeFiles ? next.files : []).map(prepareFile))
|
const prepared = await Promise.all((next.includeFiles ? next.files : []).map(prepareFile))
|
||||||
const attachments = [
|
const attachments = [
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ type ToolName =
|
||||||
| "bash"
|
| "bash"
|
||||||
| "write"
|
| "write"
|
||||||
| "edit"
|
| "edit"
|
||||||
| "apply_patch"
|
| "patch"
|
||||||
| "batch"
|
| "batch"
|
||||||
| "task"
|
| "task"
|
||||||
| "todowrite"
|
| "todowrite"
|
||||||
|
|
@ -1094,7 +1094,7 @@ const TOOL_RULES = {
|
||||||
},
|
},
|
||||||
permission: permEdit,
|
permission: permEdit,
|
||||||
},
|
},
|
||||||
apply_patch: {
|
patch: {
|
||||||
view: {
|
view: {
|
||||||
output: false,
|
output: false,
|
||||||
final: true,
|
final: true,
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ export type FooterQueuedPrompt = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RunAgent = {
|
export type RunAgent = {
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
mode: "subagent" | "primary" | "all"
|
mode: "subagent" | "primary" | "all"
|
||||||
|
|
@ -135,7 +136,6 @@ export type RunInput = {
|
||||||
files: RunFilePart[]
|
files: RunFilePart[]
|
||||||
initialInput?: string
|
initialInput?: string
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
backgroundSubagents: boolean
|
|
||||||
demo?: boolean
|
demo?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { AppProcess } from "@opencode-ai/core/process"
|
import { AppProcess } from "@opencode-ai/core/process"
|
||||||
import { Flock } from "@opencode-ai/core/util/flock"
|
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||||
import { start } from "@opencode-ai/server/process"
|
import { start } from "@opencode-ai/server/process"
|
||||||
import { randomBytes, randomUUID } from "node:crypto"
|
import { randomBytes, randomUUID } from "node:crypto"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
import { Effect, Exit, FileSystem, Logger, Option, Redacted, Schedule, Schema, Scope } from "effect"
|
||||||
import { HttpServer } from "effect/unstable/http"
|
import { HttpServer } from "effect/unstable/http"
|
||||||
import { Env } from "./env"
|
import { Env } from "./env"
|
||||||
import { ServiceConfig } from "./services/service-config"
|
import { ServiceConfig } from "./services/service-config"
|
||||||
|
|
@ -28,7 +28,7 @@ export type Options = {
|
||||||
export const run = Effect.fn("cli.server-process.run")((options: Options) =>
|
export const run = Effect.fn("cli.server-process.run")((options: Options) =>
|
||||||
processEffect(options).pipe(
|
processEffect(options).pipe(
|
||||||
Effect.provide(Updater.layer),
|
Effect.provide(Updater.layer),
|
||||||
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node]))),
|
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node, EffectFlock.node]))),
|
||||||
Effect.provide(NodeServices.layer),
|
Effect.provide(NodeServices.layer),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -37,13 +37,15 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||||
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
||||||
return yield* Effect.scoped(
|
return yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (options.mode === "service") {
|
const serviceOptions = options.mode === "service" ? yield* ServiceConfig.options() : undefined
|
||||||
const service = yield* ServiceConfig.options()
|
const lockScope = serviceOptions === undefined ? undefined : yield* acquireServiceLock(serviceOptions.file)
|
||||||
yield* Flock.effect(path.basename(service.file, ".json") + "-process", {
|
if (
|
||||||
dir: path.dirname(service.file),
|
serviceOptions !== undefined &&
|
||||||
staleMs: 3_000,
|
lockScope !== undefined &&
|
||||||
timeoutMs: 15_000,
|
(yield* Service.discover(serviceOptions)) !== undefined
|
||||||
})
|
) {
|
||||||
|
yield* Scope.close(lockScope, Exit.void)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
const environmentPassword = yield* Env.password
|
const environmentPassword = yield* Env.password
|
||||||
// Keep the lease credential out of the environment inherited by tools.
|
// Keep the lease credential out of the environment inherited by tools.
|
||||||
|
|
@ -64,7 +66,10 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||||
port: Option.fromNullishOr(options.port ?? config.port),
|
port: Option.fromNullishOr(options.port ?? config.port),
|
||||||
password,
|
password,
|
||||||
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
||||||
if (options.mode === "service") yield* register(address, password)
|
if (lockScope !== undefined) {
|
||||||
|
yield* register(address, password)
|
||||||
|
yield* Scope.close(lockScope, Exit.void)
|
||||||
|
}
|
||||||
const url = HttpServer.formatAddress(address)
|
const url = HttpServer.formatAddress(address)
|
||||||
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
|
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||||
if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`)
|
if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`)
|
||||||
|
|
@ -75,6 +80,16 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const acquireServiceLock = Effect.fnUntraced(function* (file: string) {
|
||||||
|
const flock = yield* EffectFlock.Service
|
||||||
|
const scope = yield* Scope.make()
|
||||||
|
yield* Effect.addFinalizer((exit) => Scope.close(scope, exit))
|
||||||
|
yield* flock
|
||||||
|
.acquire(`service:${file}`, undefined, { staleMs: 3_000, timeoutMs: 3_000 })
|
||||||
|
.pipe(Effect.provideService(Scope.Scope, scope))
|
||||||
|
return scope
|
||||||
|
})
|
||||||
|
|
||||||
// The latest atomic registration wins. A displaced process notices the new id,
|
// The latest atomic registration wins. A displaced process notices the new id,
|
||||||
// exits, and cannot remove its successor's registration from its finalizer.
|
// exits, and cannot remove its successor's registration from its finalizer.
|
||||||
const infoJson = Schema.fromJsonString(Service.Info)
|
const infoJson = Schema.fromJsonString(Service.Info)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import fs from "node:fs/promises"
|
import fs from "node:fs/promises"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
|
@ -24,3 +25,47 @@ test("local channel stores service config with the local service filename", asyn
|
||||||
await fs.rm(root, { recursive: true, force: true })
|
await fs.rm(root, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("concurrent service processes elect one server", async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-election-"))
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
HOME: root,
|
||||||
|
OPENCODE_DB: path.join(root, "opencode.db"),
|
||||||
|
OPENCODE_TEST_HOME: root,
|
||||||
|
XDG_CACHE_HOME: path.join(root, "cache"),
|
||||||
|
XDG_CONFIG_HOME: path.join(root, "config"),
|
||||||
|
XDG_DATA_HOME: path.join(root, "data"),
|
||||||
|
XDG_STATE_HOME: path.join(root, "state"),
|
||||||
|
}
|
||||||
|
const command = [process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"]
|
||||||
|
const first = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })
|
||||||
|
const second = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const registration = path.join(root, "state", "opencode", "service-local.json")
|
||||||
|
const info = await waitForInfo(registration)
|
||||||
|
const winner = info.pid === first.pid ? first : second
|
||||||
|
const loser = info.pid === first.pid ? second : first
|
||||||
|
const exited = await Promise.race([loser.exited.then(() => true), Bun.sleep(10_000).then(() => false)])
|
||||||
|
|
||||||
|
expect(exited).toBe(true)
|
||||||
|
expect(winner.exitCode).toBe(null)
|
||||||
|
} finally {
|
||||||
|
first.kill("SIGTERM")
|
||||||
|
second.kill("SIGTERM")
|
||||||
|
await Promise.all([first.exited, second.exited])
|
||||||
|
await fs.rm(root, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function waitForInfo(file: string) {
|
||||||
|
for (let attempt = 0; attempt < 200; attempt++) {
|
||||||
|
const value = await Bun.file(file)
|
||||||
|
.json()
|
||||||
|
.catch(() => undefined)
|
||||||
|
if (value !== undefined) return Schema.decodeUnknownPromise(Service.Info)(value)
|
||||||
|
await Bun.sleep(50)
|
||||||
|
}
|
||||||
|
throw new Error("Timed out waiting for service registration")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,14 @@ import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from
|
||||||
import {
|
import {
|
||||||
ClientApi,
|
ClientApi,
|
||||||
effectOmitEndpoints,
|
effectOmitEndpoints,
|
||||||
endpointNames,
|
|
||||||
groupNames,
|
groupNames,
|
||||||
promiseOmitEndpoints,
|
promiseOmitEndpoints,
|
||||||
} from "@opencode-ai/protocol/client"
|
} from "@opencode-ai/protocol/client"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
|
|
||||||
const promiseContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
|
const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
|
||||||
const effectContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: effectOmitEndpoints })
|
const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints })
|
||||||
|
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
Effect.all(
|
Effect.all(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
export {
|
export {
|
||||||
ClientApi,
|
ClientApi,
|
||||||
effectOmitEndpoints,
|
effectOmitEndpoints,
|
||||||
endpointNames,
|
|
||||||
groupNames,
|
groupNames,
|
||||||
promiseOmitEndpoints,
|
promiseOmitEndpoints,
|
||||||
} from "@opencode-ai/protocol/client"
|
} from "@opencode-ai/protocol/client"
|
||||||
|
|
|
||||||
|
|
@ -74,196 +74,216 @@ export type Endpoint4_3Input = { readonly sessionID: Endpoint4_3Request["params"
|
||||||
export type Endpoint4_3Output = EffectValue<ReturnType<RawClient["server.session"]["session.get"]>>["data"]
|
export type Endpoint4_3Output = EffectValue<ReturnType<RawClient["server.session"]["session.get"]>>["data"]
|
||||||
export type SessionGetOperation<E = never> = (input: Endpoint4_3Input) => Effect.Effect<Endpoint4_3Output, E>
|
export type SessionGetOperation<E = never> = (input: Endpoint4_3Input) => Effect.Effect<Endpoint4_3Output, E>
|
||||||
|
|
||||||
type Endpoint4_4Request = Parameters<RawClient["server.session"]["session.fork"]>[0]
|
type Endpoint4_4Request = Parameters<RawClient["server.session"]["session.remove"]>[0]
|
||||||
export type Endpoint4_4Input = {
|
export type Endpoint4_4Input = { readonly sessionID: Endpoint4_4Request["params"]["sessionID"] }
|
||||||
readonly sessionID: Endpoint4_4Request["params"]["sessionID"]
|
export type Endpoint4_4Output = EffectValue<ReturnType<RawClient["server.session"]["session.remove"]>>
|
||||||
readonly messageID?: Endpoint4_4Request["payload"]["messageID"]
|
export type SessionRemoveOperation<E = never> = (input: Endpoint4_4Input) => Effect.Effect<Endpoint4_4Output, E>
|
||||||
}
|
|
||||||
export type Endpoint4_4Output = EffectValue<ReturnType<RawClient["server.session"]["session.fork"]>>["data"]
|
|
||||||
export type SessionForkOperation<E = never> = (input: Endpoint4_4Input) => Effect.Effect<Endpoint4_4Output, E>
|
|
||||||
|
|
||||||
type Endpoint4_5Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
type Endpoint4_5Request = Parameters<RawClient["server.session"]["session.fork"]>[0]
|
||||||
export type Endpoint4_5Input = {
|
export type Endpoint4_5Input = {
|
||||||
readonly sessionID: Endpoint4_5Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_5Request["params"]["sessionID"]
|
||||||
readonly agent: Endpoint4_5Request["payload"]["agent"]
|
readonly messageID?: Endpoint4_5Request["payload"]["messageID"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_5Output = EffectValue<ReturnType<RawClient["server.session"]["session.switchAgent"]>>
|
export type Endpoint4_5Output = EffectValue<ReturnType<RawClient["server.session"]["session.fork"]>>["data"]
|
||||||
export type SessionSwitchAgentOperation<E = never> = (input: Endpoint4_5Input) => Effect.Effect<Endpoint4_5Output, E>
|
export type SessionForkOperation<E = never> = (input: Endpoint4_5Input) => Effect.Effect<Endpoint4_5Output, E>
|
||||||
|
|
||||||
type Endpoint4_6Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
type Endpoint4_6Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
||||||
export type Endpoint4_6Input = {
|
export type Endpoint4_6Input = {
|
||||||
readonly sessionID: Endpoint4_6Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_6Request["params"]["sessionID"]
|
||||||
readonly model: Endpoint4_6Request["payload"]["model"]
|
readonly agent: Endpoint4_6Request["payload"]["agent"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_6Output = EffectValue<ReturnType<RawClient["server.session"]["session.switchModel"]>>
|
export type Endpoint4_6Output = EffectValue<ReturnType<RawClient["server.session"]["session.switchAgent"]>>
|
||||||
export type SessionSwitchModelOperation<E = never> = (input: Endpoint4_6Input) => Effect.Effect<Endpoint4_6Output, E>
|
export type SessionSwitchAgentOperation<E = never> = (input: Endpoint4_6Input) => Effect.Effect<Endpoint4_6Output, E>
|
||||||
|
|
||||||
type Endpoint4_7Request = Parameters<RawClient["server.session"]["session.rename"]>[0]
|
type Endpoint4_7Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
||||||
export type Endpoint4_7Input = {
|
export type Endpoint4_7Input = {
|
||||||
readonly sessionID: Endpoint4_7Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_7Request["params"]["sessionID"]
|
||||||
readonly title: Endpoint4_7Request["payload"]["title"]
|
readonly model: Endpoint4_7Request["payload"]["model"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_7Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
|
export type Endpoint4_7Output = EffectValue<ReturnType<RawClient["server.session"]["session.switchModel"]>>
|
||||||
export type SessionRenameOperation<E = never> = (input: Endpoint4_7Input) => Effect.Effect<Endpoint4_7Output, E>
|
export type SessionSwitchModelOperation<E = never> = (input: Endpoint4_7Input) => Effect.Effect<Endpoint4_7Output, E>
|
||||||
|
|
||||||
type Endpoint4_8Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
type Endpoint4_8Request = Parameters<RawClient["server.session"]["session.rename"]>[0]
|
||||||
export type Endpoint4_8Input = {
|
export type Endpoint4_8Input = {
|
||||||
readonly sessionID: Endpoint4_8Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_8Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_8Request["payload"]["id"]
|
readonly title: Endpoint4_8Request["payload"]["title"]
|
||||||
readonly prompt: Endpoint4_8Request["payload"]["prompt"]
|
|
||||||
readonly delivery?: Endpoint4_8Request["payload"]["delivery"]
|
|
||||||
readonly resume?: Endpoint4_8Request["payload"]["resume"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
|
export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
|
||||||
export type SessionPromptOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E>
|
export type SessionRenameOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E>
|
||||||
|
|
||||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
|
||||||
export type Endpoint4_9Input = {
|
export type Endpoint4_9Input = {
|
||||||
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_9Request["payload"]["id"]
|
readonly destination: Endpoint4_9Request["payload"]["destination"]
|
||||||
readonly command: Endpoint4_9Request["payload"]["command"]
|
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
|
||||||
readonly arguments?: Endpoint4_9Request["payload"]["arguments"]
|
|
||||||
readonly agent?: Endpoint4_9Request["payload"]["agent"]
|
|
||||||
readonly model?: Endpoint4_9Request["payload"]["model"]
|
|
||||||
readonly files?: Endpoint4_9Request["payload"]["files"]
|
|
||||||
readonly agents?: Endpoint4_9Request["payload"]["agents"]
|
|
||||||
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
|
|
||||||
readonly resume?: Endpoint4_9Request["payload"]["resume"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
|
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.move"]>>
|
||||||
export type SessionCommandOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
|
export type SessionMoveOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
|
||||||
|
|
||||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||||
export type Endpoint4_10Input = {
|
export type Endpoint4_10Input = {
|
||||||
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_10Request["payload"]["id"]
|
readonly id?: Endpoint4_10Request["payload"]["id"]
|
||||||
readonly skill: Endpoint4_10Request["payload"]["skill"]
|
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
|
||||||
|
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
|
||||||
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
|
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
|
||||||
export type SessionSkillOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
|
export type SessionPromptOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
|
||||||
|
|
||||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
||||||
export type Endpoint4_11Input = {
|
export type Endpoint4_11Input = {
|
||||||
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
||||||
readonly text: Endpoint4_11Request["payload"]["text"]
|
readonly id?: Endpoint4_11Request["payload"]["id"]
|
||||||
readonly description?: Endpoint4_11Request["payload"]["description"]
|
readonly command: Endpoint4_11Request["payload"]["command"]
|
||||||
readonly metadata?: Endpoint4_11Request["payload"]["metadata"]
|
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
|
||||||
|
readonly agent?: Endpoint4_11Request["payload"]["agent"]
|
||||||
|
readonly model?: Endpoint4_11Request["payload"]["model"]
|
||||||
|
readonly files?: Endpoint4_11Request["payload"]["files"]
|
||||||
|
readonly agents?: Endpoint4_11Request["payload"]["agents"]
|
||||||
|
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
|
||||||
|
readonly resume?: Endpoint4_11Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
|
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
|
||||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
|
export type SessionCommandOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
|
||||||
|
|
||||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||||
export type Endpoint4_12Input = {
|
export type Endpoint4_12Input = {
|
||||||
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_12Request["payload"]["id"]
|
readonly id?: Endpoint4_12Request["payload"]["id"]
|
||||||
readonly command: Endpoint4_12Request["payload"]["command"]
|
readonly skill: Endpoint4_12Request["payload"]["skill"]
|
||||||
|
readonly resume?: Endpoint4_12Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
|
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
|
||||||
export type SessionShellOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
|
export type SessionSkillOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
|
||||||
|
|
||||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||||
export type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
|
export type Endpoint4_13Input = {
|
||||||
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>
|
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
|
||||||
export type SessionCompactOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
|
readonly text: Endpoint4_13Request["payload"]["text"]
|
||||||
|
readonly description?: Endpoint4_13Request["payload"]["description"]
|
||||||
|
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
|
||||||
|
readonly resume?: Endpoint4_13Request["payload"]["resume"]
|
||||||
|
}
|
||||||
|
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
|
||||||
|
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
|
||||||
|
|
||||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||||
export type Endpoint4_14Input = { readonly sessionID: Endpoint4_14Request["params"]["sessionID"] }
|
export type Endpoint4_14Input = {
|
||||||
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
|
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
||||||
export type SessionWaitOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
|
readonly id?: Endpoint4_14Request["payload"]["id"]
|
||||||
|
readonly command: Endpoint4_14Request["payload"]["command"]
|
||||||
|
}
|
||||||
|
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
|
||||||
|
export type SessionShellOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
|
||||||
|
|
||||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||||
export type Endpoint4_15Input = {
|
export type Endpoint4_15Input = {
|
||||||
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint4_15Request["payload"]["messageID"]
|
readonly id?: Endpoint4_15Request["payload"]["id"]
|
||||||
readonly files?: Endpoint4_15Request["payload"]["files"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
|
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
|
||||||
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
|
export type SessionCompactOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
|
||||||
|
|
||||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||||
export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
|
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
|
||||||
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
export type SessionWaitOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
||||||
|
|
||||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||||
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
export type Endpoint4_17Input = {
|
||||||
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
|
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
|
||||||
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
|
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
|
||||||
|
readonly files?: Endpoint4_17Request["payload"]["files"]
|
||||||
|
}
|
||||||
|
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
|
||||||
|
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
|
||||||
|
|
||||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||||
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
|
||||||
export type SessionContextOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
|
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
|
||||||
|
|
||||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||||
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_19Output = EffectValue<
|
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
|
||||||
|
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||||
|
export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
||||||
|
export type SessionContextOperation<E = never> = (input: Endpoint4_20Input) => Effect.Effect<Endpoint4_20Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
||||||
|
export type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint4_21Output = EffectValue<
|
||||||
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
|
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
|
||||||
>["data"]
|
>["data"]
|
||||||
export type SessionInstructionsEntryListOperation<E = never> = (
|
export type SessionInstructionsEntryListOperation<E = never> = (
|
||||||
input: Endpoint4_19Input,
|
|
||||||
) => Effect.Effect<Endpoint4_19Output, E>
|
|
||||||
|
|
||||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
|
||||||
export type Endpoint4_20Input = {
|
|
||||||
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
|
||||||
readonly key: Endpoint4_20Request["params"]["key"]
|
|
||||||
readonly value: Endpoint4_20Request["payload"]["value"]
|
|
||||||
}
|
|
||||||
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
|
|
||||||
export type SessionInstructionsEntryPutOperation<E = never> = (
|
|
||||||
input: Endpoint4_20Input,
|
|
||||||
) => Effect.Effect<Endpoint4_20Output, E>
|
|
||||||
|
|
||||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
|
||||||
export type Endpoint4_21Input = {
|
|
||||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
|
||||||
readonly key: Endpoint4_21Request["params"]["key"]
|
|
||||||
}
|
|
||||||
export type Endpoint4_21Output = EffectValue<
|
|
||||||
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
|
|
||||||
>
|
|
||||||
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
|
||||||
input: Endpoint4_21Input,
|
input: Endpoint4_21Input,
|
||||||
) => Effect.Effect<Endpoint4_21Output, E>
|
) => Effect.Effect<Endpoint4_21Output, E>
|
||||||
|
|
||||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||||
export type Endpoint4_22Input = {
|
export type Endpoint4_22Input = {
|
||||||
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint4_22Request["query"]["after"]
|
readonly key: Endpoint4_22Request["params"]["key"]
|
||||||
readonly follow?: Endpoint4_22Request["query"]["follow"]
|
readonly value: Endpoint4_22Request["payload"]["value"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_22Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
export type Endpoint4_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
|
||||||
export type SessionLogOperation<E = never> = (input: Endpoint4_22Input) => Stream.Stream<Endpoint4_22Output, E>
|
export type SessionInstructionsEntryPutOperation<E = never> = (
|
||||||
|
input: Endpoint4_22Input,
|
||||||
|
) => Effect.Effect<Endpoint4_22Output, E>
|
||||||
|
|
||||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||||
export type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
|
export type Endpoint4_23Input = {
|
||||||
export type Endpoint4_23Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
||||||
export type SessionInterruptOperation<E = never> = (input: Endpoint4_23Input) => Effect.Effect<Endpoint4_23Output, E>
|
readonly key: Endpoint4_23Request["params"]["key"]
|
||||||
|
|
||||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
|
||||||
export type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
|
|
||||||
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
|
||||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E>
|
|
||||||
|
|
||||||
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
|
||||||
export type Endpoint4_25Input = {
|
|
||||||
readonly sessionID: Endpoint4_25Request["params"]["sessionID"]
|
|
||||||
readonly messageID: Endpoint4_25Request["params"]["messageID"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
export type Endpoint4_23Output = EffectValue<
|
||||||
export type SessionMessageOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
|
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
|
||||||
|
>
|
||||||
|
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||||
|
input: Endpoint4_23Input,
|
||||||
|
) => Effect.Effect<Endpoint4_23Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||||
|
export type Endpoint4_24Input = {
|
||||||
|
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
||||||
|
readonly after?: Endpoint4_24Request["query"]["after"]
|
||||||
|
readonly follow?: Endpoint4_24Request["query"]["follow"]
|
||||||
|
}
|
||||||
|
export type Endpoint4_24Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
||||||
|
export type SessionLogOperation<E = never> = (input: Endpoint4_24Input) => Stream.Stream<Endpoint4_24Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
|
export type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
||||||
|
export type SessionInterruptOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||||
|
export type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
||||||
|
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
|
export type Endpoint4_27Input = {
|
||||||
|
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
|
||||||
|
readonly messageID: Endpoint4_27Request["params"]["messageID"]
|
||||||
|
}
|
||||||
|
export type Endpoint4_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
||||||
|
export type SessionMessageOperation<E = never> = (input: Endpoint4_27Input) => Effect.Effect<Endpoint4_27Output, E>
|
||||||
|
|
||||||
export interface SessionApi<E = never> {
|
export interface SessionApi<E = never> {
|
||||||
readonly list: SessionListOperation<E>
|
readonly list: SessionListOperation<E>
|
||||||
readonly create: SessionCreateOperation<E>
|
readonly create: SessionCreateOperation<E>
|
||||||
readonly active: SessionActiveOperation<E>
|
readonly active: SessionActiveOperation<E>
|
||||||
readonly get: SessionGetOperation<E>
|
readonly get: SessionGetOperation<E>
|
||||||
|
readonly remove: SessionRemoveOperation<E>
|
||||||
readonly fork: SessionForkOperation<E>
|
readonly fork: SessionForkOperation<E>
|
||||||
readonly switchAgent: SessionSwitchAgentOperation<E>
|
readonly switchAgent: SessionSwitchAgentOperation<E>
|
||||||
readonly switchModel: SessionSwitchModelOperation<E>
|
readonly switchModel: SessionSwitchModelOperation<E>
|
||||||
readonly rename: SessionRenameOperation<E>
|
readonly rename: SessionRenameOperation<E>
|
||||||
|
readonly move: SessionMoveOperation<E>
|
||||||
readonly prompt: SessionPromptOperation<E>
|
readonly prompt: SessionPromptOperation<E>
|
||||||
readonly command: SessionCommandOperation<E>
|
readonly command: SessionCommandOperation<E>
|
||||||
readonly skill: SessionSkillOperation<E>
|
readonly skill: SessionSkillOperation<E>
|
||||||
|
|
@ -271,9 +291,11 @@ export interface SessionApi<E = never> {
|
||||||
readonly shell: SessionShellOperation<E>
|
readonly shell: SessionShellOperation<E>
|
||||||
readonly compact: SessionCompactOperation<E>
|
readonly compact: SessionCompactOperation<E>
|
||||||
readonly wait: SessionWaitOperation<E>
|
readonly wait: SessionWaitOperation<E>
|
||||||
readonly revertStage: SessionRevertStageOperation<E>
|
readonly revert: {
|
||||||
readonly revertClear: SessionRevertClearOperation<E>
|
readonly stage: SessionRevertStageOperation<E>
|
||||||
readonly revertCommit: SessionRevertCommitOperation<E>
|
readonly clear: SessionRevertClearOperation<E>
|
||||||
|
readonly commit: SessionRevertCommitOperation<E>
|
||||||
|
}
|
||||||
readonly context: SessionContextOperation<E>
|
readonly context: SessionContextOperation<E>
|
||||||
readonly instructions: {
|
readonly instructions: {
|
||||||
readonly entry: {
|
readonly entry: {
|
||||||
|
|
@ -418,11 +440,15 @@ export type IntegrationAttemptCancelOperation<E = never> = (
|
||||||
export interface IntegrationApi<E = never> {
|
export interface IntegrationApi<E = never> {
|
||||||
readonly list: IntegrationListOperation<E>
|
readonly list: IntegrationListOperation<E>
|
||||||
readonly get: IntegrationGetOperation<E>
|
readonly get: IntegrationGetOperation<E>
|
||||||
readonly connectKey: IntegrationConnectKeyOperation<E>
|
readonly connect: {
|
||||||
readonly connectOauth: IntegrationConnectOauthOperation<E>
|
readonly key: IntegrationConnectKeyOperation<E>
|
||||||
readonly attemptStatus: IntegrationAttemptStatusOperation<E>
|
readonly oauth: IntegrationConnectOauthOperation<E>
|
||||||
readonly attemptComplete: IntegrationAttemptCompleteOperation<E>
|
}
|
||||||
readonly attemptCancel: IntegrationAttemptCancelOperation<E>
|
readonly attempt: {
|
||||||
|
readonly status: IntegrationAttemptStatusOperation<E>
|
||||||
|
readonly complete: IntegrationAttemptCompleteOperation<E>
|
||||||
|
readonly cancel: IntegrationAttemptCancelOperation<E>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
||||||
|
|
@ -430,8 +456,16 @@ export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query
|
||||||
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
|
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
|
||||||
export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
|
export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
|
||||||
|
export type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
|
||||||
|
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
|
||||||
|
export type ServerMcpResourceCatalogOperation<E = never> = (
|
||||||
|
input?: Endpoint10_1Input,
|
||||||
|
) => Effect.Effect<Endpoint10_1Output, E>
|
||||||
|
|
||||||
export interface ServerMcpApi<E = never> {
|
export interface ServerMcpApi<E = never> {
|
||||||
readonly list: ServerMcpListOperation<E>
|
readonly list: ServerMcpListOperation<E>
|
||||||
|
readonly resource: { readonly catalog: ServerMcpResourceCatalogOperation<E> }
|
||||||
}
|
}
|
||||||
|
|
||||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||||
|
|
@ -481,7 +515,7 @@ export interface ProjectApi<E = never> {
|
||||||
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
|
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
|
||||||
export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
|
export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
|
||||||
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.form"]["form.request.list"]>>
|
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.form"]["form.request.list"]>>
|
||||||
export type FormListRequestsOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
|
export type FormRequestListOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
|
||||||
|
|
||||||
type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
|
type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
|
||||||
export type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] }
|
export type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] }
|
||||||
|
|
@ -535,7 +569,7 @@ export type Endpoint13_6Output = EffectValue<ReturnType<RawClient["server.form"]
|
||||||
export type FormCancelOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
|
export type FormCancelOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
|
||||||
|
|
||||||
export interface FormApi<E = never> {
|
export interface FormApi<E = never> {
|
||||||
readonly listRequests: FormListRequestsOperation<E>
|
readonly request: { readonly list: FormRequestListOperation<E> }
|
||||||
readonly list: FormListOperation<E>
|
readonly list: FormListOperation<E>
|
||||||
readonly create: FormCreateOperation<E>
|
readonly create: FormCreateOperation<E>
|
||||||
readonly get: FormGetOperation<E>
|
readonly get: FormGetOperation<E>
|
||||||
|
|
@ -547,7 +581,7 @@ export interface FormApi<E = never> {
|
||||||
type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
||||||
export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
|
export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
|
||||||
export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
|
export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
|
||||||
export type PermissionListRequestsOperation<E = never> = (
|
export type PermissionRequestListOperation<E = never> = (
|
||||||
input?: Endpoint14_0Input,
|
input?: Endpoint14_0Input,
|
||||||
) => Effect.Effect<Endpoint14_0Output, E>
|
) => Effect.Effect<Endpoint14_0Output, E>
|
||||||
|
|
||||||
|
|
@ -556,14 +590,14 @@ export type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["quer
|
||||||
export type Endpoint14_1Output = EffectValue<
|
export type Endpoint14_1Output = EffectValue<
|
||||||
ReturnType<RawClient["server.permission"]["permission.saved.list"]>
|
ReturnType<RawClient["server.permission"]["permission.saved.list"]>
|
||||||
>["data"]
|
>["data"]
|
||||||
export type PermissionListSavedOperation<E = never> = (
|
export type PermissionSavedListOperation<E = never> = (
|
||||||
input?: Endpoint14_1Input,
|
input?: Endpoint14_1Input,
|
||||||
) => Effect.Effect<Endpoint14_1Output, E>
|
) => Effect.Effect<Endpoint14_1Output, E>
|
||||||
|
|
||||||
type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
||||||
export type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
|
export type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
|
||||||
export type Endpoint14_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
|
export type Endpoint14_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
|
||||||
export type PermissionRemoveSavedOperation<E = never> = (
|
export type PermissionSavedRemoveOperation<E = never> = (
|
||||||
input: Endpoint14_2Input,
|
input: Endpoint14_2Input,
|
||||||
) => Effect.Effect<Endpoint14_2Output, E>
|
) => Effect.Effect<Endpoint14_2Output, E>
|
||||||
|
|
||||||
|
|
@ -611,9 +645,8 @@ export type Endpoint14_6Output = EffectValue<ReturnType<RawClient["server.permis
|
||||||
export type PermissionReplyOperation<E = never> = (input: Endpoint14_6Input) => Effect.Effect<Endpoint14_6Output, E>
|
export type PermissionReplyOperation<E = never> = (input: Endpoint14_6Input) => Effect.Effect<Endpoint14_6Output, E>
|
||||||
|
|
||||||
export interface PermissionApi<E = never> {
|
export interface PermissionApi<E = never> {
|
||||||
readonly listRequests: PermissionListRequestsOperation<E>
|
readonly request: { readonly list: PermissionRequestListOperation<E> }
|
||||||
readonly listSaved: PermissionListSavedOperation<E>
|
readonly saved: { readonly list: PermissionSavedListOperation<E>; readonly remove: PermissionSavedRemoveOperation<E> }
|
||||||
readonly removeSaved: PermissionRemoveSavedOperation<E>
|
|
||||||
readonly create: PermissionCreateOperation<E>
|
readonly create: PermissionCreateOperation<E>
|
||||||
readonly list: PermissionListOperation<E>
|
readonly list: PermissionListOperation<E>
|
||||||
readonly get: PermissionGetOperation<E>
|
readonly get: PermissionGetOperation<E>
|
||||||
|
|
@ -729,7 +762,7 @@ export type Endpoint20_1Input = {
|
||||||
readonly location?: Endpoint20_1Request["query"]["location"]
|
readonly location?: Endpoint20_1Request["query"]["location"]
|
||||||
readonly command: Endpoint20_1Request["payload"]["command"]
|
readonly command: Endpoint20_1Request["payload"]["command"]
|
||||||
readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
|
readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
|
||||||
readonly timeout?: Endpoint20_1Request["payload"]["timeout"]
|
readonly timeout: Endpoint20_1Request["payload"]["timeout"]
|
||||||
readonly metadata?: Endpoint20_1Request["payload"]["metadata"]
|
readonly metadata?: Endpoint20_1Request["payload"]["metadata"]
|
||||||
}
|
}
|
||||||
export type Endpoint20_1Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.create"]>>
|
export type Endpoint20_1Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.create"]>>
|
||||||
|
|
@ -743,28 +776,38 @@ export type Endpoint20_2Input = {
|
||||||
export type Endpoint20_2Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.get"]>>
|
export type Endpoint20_2Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.get"]>>
|
||||||
export type ShellGetOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
|
export type ShellGetOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
|
||||||
|
|
||||||
type Endpoint20_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
|
type Endpoint20_3Request = Parameters<RawClient["server.shell"]["shell.timeout"]>[0]
|
||||||
export type Endpoint20_3Input = {
|
export type Endpoint20_3Input = {
|
||||||
readonly id: Endpoint20_3Request["params"]["id"]
|
readonly id: Endpoint20_3Request["params"]["id"]
|
||||||
readonly location?: Endpoint20_3Request["query"]["location"]
|
readonly location?: Endpoint20_3Request["query"]["location"]
|
||||||
readonly cursor?: Endpoint20_3Request["query"]["cursor"]
|
readonly timeout: Endpoint20_3Request["payload"]["timeout"]
|
||||||
readonly limit?: Endpoint20_3Request["query"]["limit"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint20_3Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.output"]>>
|
export type Endpoint20_3Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.timeout"]>>
|
||||||
export type ShellOutputOperation<E = never> = (input: Endpoint20_3Input) => Effect.Effect<Endpoint20_3Output, E>
|
export type ShellTimeoutOperation<E = never> = (input: Endpoint20_3Input) => Effect.Effect<Endpoint20_3Output, E>
|
||||||
|
|
||||||
type Endpoint20_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
|
type Endpoint20_4Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
|
||||||
export type Endpoint20_4Input = {
|
export type Endpoint20_4Input = {
|
||||||
readonly id: Endpoint20_4Request["params"]["id"]
|
readonly id: Endpoint20_4Request["params"]["id"]
|
||||||
readonly location?: Endpoint20_4Request["query"]["location"]
|
readonly location?: Endpoint20_4Request["query"]["location"]
|
||||||
|
readonly cursor?: Endpoint20_4Request["query"]["cursor"]
|
||||||
|
readonly limit?: Endpoint20_4Request["query"]["limit"]
|
||||||
}
|
}
|
||||||
export type Endpoint20_4Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.remove"]>>
|
export type Endpoint20_4Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.output"]>>
|
||||||
export type ShellRemoveOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E>
|
export type ShellOutputOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E>
|
||||||
|
|
||||||
|
type Endpoint20_5Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
|
||||||
|
export type Endpoint20_5Input = {
|
||||||
|
readonly id: Endpoint20_5Request["params"]["id"]
|
||||||
|
readonly location?: Endpoint20_5Request["query"]["location"]
|
||||||
|
}
|
||||||
|
export type Endpoint20_5Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.remove"]>>
|
||||||
|
export type ShellRemoveOperation<E = never> = (input: Endpoint20_5Input) => Effect.Effect<Endpoint20_5Output, E>
|
||||||
|
|
||||||
export interface ShellApi<E = never> {
|
export interface ShellApi<E = never> {
|
||||||
readonly list: ShellListOperation<E>
|
readonly list: ShellListOperation<E>
|
||||||
readonly create: ShellCreateOperation<E>
|
readonly create: ShellCreateOperation<E>
|
||||||
readonly get: ShellGetOperation<E>
|
readonly get: ShellGetOperation<E>
|
||||||
|
readonly timeout: ShellTimeoutOperation<E>
|
||||||
readonly output: ShellOutputOperation<E>
|
readonly output: ShellOutputOperation<E>
|
||||||
readonly remove: ShellRemoveOperation<E>
|
readonly remove: ShellRemoveOperation<E>
|
||||||
}
|
}
|
||||||
|
|
@ -772,7 +815,7 @@ export interface ShellApi<E = never> {
|
||||||
type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
||||||
export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
|
export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
|
||||||
export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
|
export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
|
||||||
export type QuestionListRequestsOperation<E = never> = (
|
export type QuestionRequestListOperation<E = never> = (
|
||||||
input?: Endpoint21_0Input,
|
input?: Endpoint21_0Input,
|
||||||
) => Effect.Effect<Endpoint21_0Output, E>
|
) => Effect.Effect<Endpoint21_0Output, E>
|
||||||
|
|
||||||
|
|
@ -799,7 +842,7 @@ export type Endpoint21_3Output = EffectValue<ReturnType<RawClient["server.questi
|
||||||
export type QuestionRejectOperation<E = never> = (input: Endpoint21_3Input) => Effect.Effect<Endpoint21_3Output, E>
|
export type QuestionRejectOperation<E = never> = (input: Endpoint21_3Input) => Effect.Effect<Endpoint21_3Output, E>
|
||||||
|
|
||||||
export interface QuestionApi<E = never> {
|
export interface QuestionApi<E = never> {
|
||||||
readonly listRequests: QuestionListRequestsOperation<E>
|
readonly request: { readonly list: QuestionRequestListOperation<E> }
|
||||||
readonly list: QuestionListOperation<E>
|
readonly list: QuestionListOperation<E>
|
||||||
readonly reply: QuestionReplyOperation<E>
|
readonly reply: QuestionReplyOperation<E>
|
||||||
readonly reject: QuestionRejectOperation<E>
|
readonly reject: QuestionRejectOperation<E>
|
||||||
|
|
@ -869,10 +912,15 @@ export interface VcsApi<E = never> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
|
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
|
||||||
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
|
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
|
||||||
|
export type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
|
||||||
|
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
|
||||||
|
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
|
||||||
|
|
||||||
export interface DebugApi<E = never> {
|
export interface DebugApi<E = never> {
|
||||||
readonly location: DebugLocationOperation<E>
|
readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppApi<E = never> {
|
export interface AppApi<E = never> {
|
||||||
|
|
|
||||||
|
|
@ -95,56 +95,73 @@ const Endpoint4_3 = (raw: RawClient["server.session"]) => (input: Endpoint4_3Inp
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_4Request = Parameters<RawClient["server.session"]["session.fork"]>[0]
|
type Endpoint4_4Request = Parameters<RawClient["server.session"]["session.remove"]>[0]
|
||||||
type Endpoint4_4Input = {
|
type Endpoint4_4Input = { readonly sessionID: Endpoint4_4Request["params"]["sessionID"] }
|
||||||
readonly sessionID: Endpoint4_4Request["params"]["sessionID"]
|
|
||||||
readonly messageID?: Endpoint4_4Request["payload"]["messageID"]
|
|
||||||
}
|
|
||||||
const Endpoint4_4 = (raw: RawClient["server.session"]) => (input: Endpoint4_4Input) =>
|
const Endpoint4_4 = (raw: RawClient["server.session"]) => (input: Endpoint4_4Input) =>
|
||||||
|
raw["session.remove"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint4_5Request = Parameters<RawClient["server.session"]["session.fork"]>[0]
|
||||||
|
type Endpoint4_5Input = {
|
||||||
|
readonly sessionID: Endpoint4_5Request["params"]["sessionID"]
|
||||||
|
readonly messageID?: Endpoint4_5Request["payload"]["messageID"]
|
||||||
|
}
|
||||||
|
const Endpoint4_5 = (raw: RawClient["server.session"]) => (input: Endpoint4_5Input) =>
|
||||||
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe(
|
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_5Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
type Endpoint4_6Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
|
||||||
type Endpoint4_5Input = {
|
type Endpoint4_6Input = {
|
||||||
readonly sessionID: Endpoint4_5Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_6Request["params"]["sessionID"]
|
||||||
readonly agent: Endpoint4_5Request["payload"]["agent"]
|
readonly agent: Endpoint4_6Request["payload"]["agent"]
|
||||||
}
|
}
|
||||||
const Endpoint4_5 = (raw: RawClient["server.session"]) => (input: Endpoint4_5Input) =>
|
const Endpoint4_6 = (raw: RawClient["server.session"]) => (input: Endpoint4_6Input) =>
|
||||||
raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe(
|
raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_6Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
type Endpoint4_7Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
|
||||||
type Endpoint4_6Input = {
|
type Endpoint4_7Input = {
|
||||||
readonly sessionID: Endpoint4_6Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_7Request["params"]["sessionID"]
|
||||||
readonly model: Endpoint4_6Request["payload"]["model"]
|
readonly model: Endpoint4_7Request["payload"]["model"]
|
||||||
}
|
}
|
||||||
const Endpoint4_6 = (raw: RawClient["server.session"]) => (input: Endpoint4_6Input) =>
|
const Endpoint4_7 = (raw: RawClient["server.session"]) => (input: Endpoint4_7Input) =>
|
||||||
raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe(
|
raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_7Request = Parameters<RawClient["server.session"]["session.rename"]>[0]
|
type Endpoint4_8Request = Parameters<RawClient["server.session"]["session.rename"]>[0]
|
||||||
type Endpoint4_7Input = {
|
type Endpoint4_8Input = {
|
||||||
readonly sessionID: Endpoint4_7Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_8Request["params"]["sessionID"]
|
||||||
readonly title: Endpoint4_7Request["payload"]["title"]
|
readonly title: Endpoint4_8Request["payload"]["title"]
|
||||||
}
|
}
|
||||||
const Endpoint4_7 = (raw: RawClient["server.session"]) => (input: Endpoint4_7Input) =>
|
const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Input) =>
|
||||||
raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe(
|
raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_8Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
|
||||||
type Endpoint4_8Input = {
|
type Endpoint4_9Input = {
|
||||||
readonly sessionID: Endpoint4_8Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_8Request["payload"]["id"]
|
readonly destination: Endpoint4_9Request["payload"]["destination"]
|
||||||
readonly prompt: Endpoint4_8Request["payload"]["prompt"]
|
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
|
||||||
readonly delivery?: Endpoint4_8Request["payload"]["delivery"]
|
|
||||||
readonly resume?: Endpoint4_8Request["payload"]["resume"]
|
|
||||||
}
|
}
|
||||||
const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Input) =>
|
const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) =>
|
||||||
|
raw["session.move"]({
|
||||||
|
params: { sessionID: input["sessionID"] },
|
||||||
|
payload: { destination: input["destination"], moveChanges: input["moveChanges"] },
|
||||||
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||||
|
type Endpoint4_10Input = {
|
||||||
|
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
||||||
|
readonly id?: Endpoint4_10Request["payload"]["id"]
|
||||||
|
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
|
||||||
|
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
|
||||||
|
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
||||||
|
}
|
||||||
|
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
|
||||||
raw["session.prompt"]({
|
raw["session.prompt"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
|
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
|
||||||
|
|
@ -153,20 +170,20 @@ const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Inp
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
||||||
type Endpoint4_9Input = {
|
type Endpoint4_11Input = {
|
||||||
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_9Request["payload"]["id"]
|
readonly id?: Endpoint4_11Request["payload"]["id"]
|
||||||
readonly command: Endpoint4_9Request["payload"]["command"]
|
readonly command: Endpoint4_11Request["payload"]["command"]
|
||||||
readonly arguments?: Endpoint4_9Request["payload"]["arguments"]
|
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
|
||||||
readonly agent?: Endpoint4_9Request["payload"]["agent"]
|
readonly agent?: Endpoint4_11Request["payload"]["agent"]
|
||||||
readonly model?: Endpoint4_9Request["payload"]["model"]
|
readonly model?: Endpoint4_11Request["payload"]["model"]
|
||||||
readonly files?: Endpoint4_9Request["payload"]["files"]
|
readonly files?: Endpoint4_11Request["payload"]["files"]
|
||||||
readonly agents?: Endpoint4_9Request["payload"]["agents"]
|
readonly agents?: Endpoint4_11Request["payload"]["agents"]
|
||||||
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
|
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
|
||||||
readonly resume?: Endpoint4_9Request["payload"]["resume"]
|
readonly resume?: Endpoint4_11Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) =>
|
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
|
||||||
raw["session.command"]({
|
raw["session.command"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: {
|
payload: {
|
||||||
|
|
@ -185,61 +202,73 @@ const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Inp
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||||
type Endpoint4_10Input = {
|
type Endpoint4_12Input = {
|
||||||
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_10Request["payload"]["id"]
|
readonly id?: Endpoint4_12Request["payload"]["id"]
|
||||||
readonly skill: Endpoint4_10Request["payload"]["skill"]
|
readonly skill: Endpoint4_12Request["payload"]["skill"]
|
||||||
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
readonly resume?: Endpoint4_12Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
|
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
|
||||||
raw["session.skill"]({
|
raw["session.skill"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
|
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||||
type Endpoint4_11Input = {
|
type Endpoint4_13Input = {
|
||||||
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
|
||||||
readonly text: Endpoint4_11Request["payload"]["text"]
|
readonly text: Endpoint4_13Request["payload"]["text"]
|
||||||
readonly description?: Endpoint4_11Request["payload"]["description"]
|
readonly description?: Endpoint4_13Request["payload"]["description"]
|
||||||
readonly metadata?: Endpoint4_11Request["payload"]["metadata"]
|
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
|
||||||
|
readonly resume?: Endpoint4_13Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
|
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
||||||
raw["session.synthetic"]({
|
raw["session.synthetic"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
|
payload: {
|
||||||
|
text: input["text"],
|
||||||
|
description: input["description"],
|
||||||
|
metadata: input["metadata"],
|
||||||
|
resume: input["resume"],
|
||||||
|
},
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||||
type Endpoint4_12Input = {
|
type Endpoint4_14Input = {
|
||||||
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_12Request["payload"]["id"]
|
readonly id?: Endpoint4_14Request["payload"]["id"]
|
||||||
readonly command: Endpoint4_12Request["payload"]["command"]
|
readonly command: Endpoint4_14Request["payload"]["command"]
|
||||||
}
|
}
|
||||||
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
|
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
||||||
raw["session.shell"]({
|
raw["session.shell"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { id: input["id"], command: input["command"] },
|
payload: { id: input["id"], command: input["command"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||||
type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
|
|
||||||
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
|
||||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
|
||||||
type Endpoint4_14Input = { readonly sessionID: Endpoint4_14Request["params"]["sessionID"] }
|
|
||||||
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
|
||||||
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
|
||||||
type Endpoint4_15Input = {
|
type Endpoint4_15Input = {
|
||||||
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint4_15Request["payload"]["messageID"]
|
readonly id?: Endpoint4_15Request["payload"]["id"]
|
||||||
readonly files?: Endpoint4_15Request["payload"]["files"]
|
|
||||||
}
|
}
|
||||||
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
|
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
|
||||||
|
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
Effect.map((value) => value.data),
|
||||||
|
)
|
||||||
|
|
||||||
|
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||||
|
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
||||||
|
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
||||||
|
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||||
|
type Endpoint4_17Input = {
|
||||||
|
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
|
||||||
|
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
|
||||||
|
readonly files?: Endpoint4_17Request["payload"]["files"]
|
||||||
|
}
|
||||||
|
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
||||||
raw["session.revert.stage"]({
|
raw["session.revert.stage"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { messageID: input["messageID"], files: input["files"] },
|
payload: { messageID: input["messageID"], files: input["files"] },
|
||||||
|
|
@ -248,61 +277,61 @@ const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15I
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||||
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
|
||||||
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
|
||||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
|
||||||
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
|
||||||
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
|
||||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
|
||||||
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||||
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
||||||
|
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||||
|
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||||
|
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
||||||
|
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||||
|
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
||||||
|
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
||||||
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
||||||
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
|
||||||
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
||||||
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||||
type Endpoint4_20Input = {
|
type Endpoint4_22Input = {
|
||||||
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
||||||
readonly key: Endpoint4_20Request["params"]["key"]
|
readonly key: Endpoint4_22Request["params"]["key"]
|
||||||
readonly value: Endpoint4_20Request["payload"]["value"]
|
readonly value: Endpoint4_22Request["payload"]["value"]
|
||||||
}
|
}
|
||||||
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
||||||
raw["session.instructions.entry.put"]({
|
raw["session.instructions.entry.put"]({
|
||||||
params: { sessionID: input["sessionID"], key: input["key"] },
|
params: { sessionID: input["sessionID"], key: input["key"] },
|
||||||
payload: { value: input["value"] },
|
payload: { value: input["value"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||||
type Endpoint4_21Input = {
|
type Endpoint4_23Input = {
|
||||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
||||||
readonly key: Endpoint4_21Request["params"]["key"]
|
readonly key: Endpoint4_23Request["params"]["key"]
|
||||||
}
|
}
|
||||||
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
||||||
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||||
type Endpoint4_22Input = {
|
type Endpoint4_24Input = {
|
||||||
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint4_22Request["query"]["after"]
|
readonly after?: Endpoint4_24Request["query"]["after"]
|
||||||
readonly follow?: Endpoint4_22Request["query"]["follow"]
|
readonly follow?: Endpoint4_24Request["query"]["follow"]
|
||||||
}
|
}
|
||||||
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
|
||||||
Stream.unwrap(
|
Stream.unwrap(
|
||||||
raw["session.log"]({
|
raw["session.log"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
|
|
@ -313,22 +342,22 @@ const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22I
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
|
type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
|
||||||
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
|
||||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||||
type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
|
type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
|
||||||
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
|
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) =>
|
||||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
type Endpoint4_25Input = {
|
type Endpoint4_27Input = {
|
||||||
readonly sessionID: Endpoint4_25Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint4_25Request["params"]["messageID"]
|
readonly messageID: Endpoint4_27Request["params"]["messageID"]
|
||||||
}
|
}
|
||||||
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
|
const Endpoint4_27 = (raw: RawClient["server.session"]) => (input: Endpoint4_27Input) =>
|
||||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
|
|
@ -339,26 +368,26 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
|
||||||
create: Endpoint4_1(raw),
|
create: Endpoint4_1(raw),
|
||||||
active: Endpoint4_2(raw),
|
active: Endpoint4_2(raw),
|
||||||
get: Endpoint4_3(raw),
|
get: Endpoint4_3(raw),
|
||||||
fork: Endpoint4_4(raw),
|
remove: Endpoint4_4(raw),
|
||||||
switchAgent: Endpoint4_5(raw),
|
fork: Endpoint4_5(raw),
|
||||||
switchModel: Endpoint4_6(raw),
|
switchAgent: Endpoint4_6(raw),
|
||||||
rename: Endpoint4_7(raw),
|
switchModel: Endpoint4_7(raw),
|
||||||
prompt: Endpoint4_8(raw),
|
rename: Endpoint4_8(raw),
|
||||||
command: Endpoint4_9(raw),
|
move: Endpoint4_9(raw),
|
||||||
skill: Endpoint4_10(raw),
|
prompt: Endpoint4_10(raw),
|
||||||
synthetic: Endpoint4_11(raw),
|
command: Endpoint4_11(raw),
|
||||||
shell: Endpoint4_12(raw),
|
skill: Endpoint4_12(raw),
|
||||||
compact: Endpoint4_13(raw),
|
synthetic: Endpoint4_13(raw),
|
||||||
wait: Endpoint4_14(raw),
|
shell: Endpoint4_14(raw),
|
||||||
revertStage: Endpoint4_15(raw),
|
compact: Endpoint4_15(raw),
|
||||||
revertClear: Endpoint4_16(raw),
|
wait: Endpoint4_16(raw),
|
||||||
revertCommit: Endpoint4_17(raw),
|
revert: { stage: Endpoint4_17(raw), clear: Endpoint4_18(raw), commit: Endpoint4_19(raw) },
|
||||||
context: Endpoint4_18(raw),
|
context: Endpoint4_20(raw),
|
||||||
instructions: { entry: { list: Endpoint4_19(raw), put: Endpoint4_20(raw), remove: Endpoint4_21(raw) } },
|
instructions: { entry: { list: Endpoint4_21(raw), put: Endpoint4_22(raw), remove: Endpoint4_23(raw) } },
|
||||||
log: Endpoint4_22(raw),
|
log: Endpoint4_24(raw),
|
||||||
interrupt: Endpoint4_23(raw),
|
interrupt: Endpoint4_25(raw),
|
||||||
background: Endpoint4_24(raw),
|
background: Endpoint4_26(raw),
|
||||||
message: Endpoint4_25(raw),
|
message: Endpoint4_27(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||||
|
|
@ -505,11 +534,8 @@ const Endpoint9_6 = (raw: RawClient["server.integration"]) => (input: Endpoint9_
|
||||||
const adaptGroup9 = (raw: RawClient["server.integration"]) => ({
|
const adaptGroup9 = (raw: RawClient["server.integration"]) => ({
|
||||||
list: Endpoint9_0(raw),
|
list: Endpoint9_0(raw),
|
||||||
get: Endpoint9_1(raw),
|
get: Endpoint9_1(raw),
|
||||||
connectKey: Endpoint9_2(raw),
|
connect: { key: Endpoint9_2(raw), oauth: Endpoint9_3(raw) },
|
||||||
connectOauth: Endpoint9_3(raw),
|
attempt: { status: Endpoint9_4(raw), complete: Endpoint9_5(raw), cancel: Endpoint9_6(raw) },
|
||||||
attemptStatus: Endpoint9_4(raw),
|
|
||||||
attemptComplete: Endpoint9_5(raw),
|
|
||||||
attemptCancel: Endpoint9_6(raw),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
||||||
|
|
@ -517,7 +543,15 @@ type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["loc
|
||||||
const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) =>
|
const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) =>
|
||||||
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw) })
|
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
|
||||||
|
type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
|
||||||
|
const Endpoint10_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_1Input) =>
|
||||||
|
raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({
|
||||||
|
list: Endpoint10_0(raw),
|
||||||
|
resource: { catalog: Endpoint10_1(raw) },
|
||||||
|
})
|
||||||
|
|
||||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||||
type Endpoint11_0Input = {
|
type Endpoint11_0Input = {
|
||||||
|
|
@ -654,7 +688,7 @@ const Endpoint13_6 = (raw: RawClient["server.form"]) => (input: Endpoint13_6Inpu
|
||||||
)
|
)
|
||||||
|
|
||||||
const adaptGroup13 = (raw: RawClient["server.form"]) => ({
|
const adaptGroup13 = (raw: RawClient["server.form"]) => ({
|
||||||
listRequests: Endpoint13_0(raw),
|
request: { list: Endpoint13_0(raw) },
|
||||||
list: Endpoint13_1(raw),
|
list: Endpoint13_1(raw),
|
||||||
create: Endpoint13_2(raw),
|
create: Endpoint13_2(raw),
|
||||||
get: Endpoint13_3(raw),
|
get: Endpoint13_3(raw),
|
||||||
|
|
@ -742,9 +776,8 @@ const Endpoint14_6 = (raw: RawClient["server.permission"]) => (input: Endpoint14
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup14 = (raw: RawClient["server.permission"]) => ({
|
const adaptGroup14 = (raw: RawClient["server.permission"]) => ({
|
||||||
listRequests: Endpoint14_0(raw),
|
request: { list: Endpoint14_0(raw) },
|
||||||
listSaved: Endpoint14_1(raw),
|
saved: { list: Endpoint14_1(raw), remove: Endpoint14_2(raw) },
|
||||||
removeSaved: Endpoint14_2(raw),
|
|
||||||
create: Endpoint14_3(raw),
|
create: Endpoint14_3(raw),
|
||||||
list: Endpoint14_4(raw),
|
list: Endpoint14_4(raw),
|
||||||
get: Endpoint14_5(raw),
|
get: Endpoint14_5(raw),
|
||||||
|
|
@ -877,7 +910,7 @@ type Endpoint20_1Input = {
|
||||||
readonly location?: Endpoint20_1Request["query"]["location"]
|
readonly location?: Endpoint20_1Request["query"]["location"]
|
||||||
readonly command: Endpoint20_1Request["payload"]["command"]
|
readonly command: Endpoint20_1Request["payload"]["command"]
|
||||||
readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
|
readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
|
||||||
readonly timeout?: Endpoint20_1Request["payload"]["timeout"]
|
readonly timeout: Endpoint20_1Request["payload"]["timeout"]
|
||||||
readonly metadata?: Endpoint20_1Request["payload"]["metadata"]
|
readonly metadata?: Endpoint20_1Request["payload"]["metadata"]
|
||||||
}
|
}
|
||||||
const Endpoint20_1 = (raw: RawClient["server.shell"]) => (input: Endpoint20_1Input) =>
|
const Endpoint20_1 = (raw: RawClient["server.shell"]) => (input: Endpoint20_1Input) =>
|
||||||
|
|
@ -896,25 +929,38 @@ const Endpoint20_2 = (raw: RawClient["server.shell"]) => (input: Endpoint20_2Inp
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint20_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
|
type Endpoint20_3Request = Parameters<RawClient["server.shell"]["shell.timeout"]>[0]
|
||||||
type Endpoint20_3Input = {
|
type Endpoint20_3Input = {
|
||||||
readonly id: Endpoint20_3Request["params"]["id"]
|
readonly id: Endpoint20_3Request["params"]["id"]
|
||||||
readonly location?: Endpoint20_3Request["query"]["location"]
|
readonly location?: Endpoint20_3Request["query"]["location"]
|
||||||
readonly cursor?: Endpoint20_3Request["query"]["cursor"]
|
readonly timeout: Endpoint20_3Request["payload"]["timeout"]
|
||||||
readonly limit?: Endpoint20_3Request["query"]["limit"]
|
|
||||||
}
|
}
|
||||||
const Endpoint20_3 = (raw: RawClient["server.shell"]) => (input: Endpoint20_3Input) =>
|
const Endpoint20_3 = (raw: RawClient["server.shell"]) => (input: Endpoint20_3Input) =>
|
||||||
|
raw["shell.timeout"]({
|
||||||
|
params: { id: input["id"] },
|
||||||
|
query: { location: input["location"] },
|
||||||
|
payload: { timeout: input["timeout"] },
|
||||||
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint20_4Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
|
||||||
|
type Endpoint20_4Input = {
|
||||||
|
readonly id: Endpoint20_4Request["params"]["id"]
|
||||||
|
readonly location?: Endpoint20_4Request["query"]["location"]
|
||||||
|
readonly cursor?: Endpoint20_4Request["query"]["cursor"]
|
||||||
|
readonly limit?: Endpoint20_4Request["query"]["limit"]
|
||||||
|
}
|
||||||
|
const Endpoint20_4 = (raw: RawClient["server.shell"]) => (input: Endpoint20_4Input) =>
|
||||||
raw["shell.output"]({
|
raw["shell.output"]({
|
||||||
params: { id: input["id"] },
|
params: { id: input["id"] },
|
||||||
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
|
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint20_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
|
type Endpoint20_5Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
|
||||||
type Endpoint20_4Input = {
|
type Endpoint20_5Input = {
|
||||||
readonly id: Endpoint20_4Request["params"]["id"]
|
readonly id: Endpoint20_5Request["params"]["id"]
|
||||||
readonly location?: Endpoint20_4Request["query"]["location"]
|
readonly location?: Endpoint20_5Request["query"]["location"]
|
||||||
}
|
}
|
||||||
const Endpoint20_4 = (raw: RawClient["server.shell"]) => (input: Endpoint20_4Input) =>
|
const Endpoint20_5 = (raw: RawClient["server.shell"]) => (input: Endpoint20_5Input) =>
|
||||||
raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
|
raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
@ -923,8 +969,9 @@ const adaptGroup20 = (raw: RawClient["server.shell"]) => ({
|
||||||
list: Endpoint20_0(raw),
|
list: Endpoint20_0(raw),
|
||||||
create: Endpoint20_1(raw),
|
create: Endpoint20_1(raw),
|
||||||
get: Endpoint20_2(raw),
|
get: Endpoint20_2(raw),
|
||||||
output: Endpoint20_3(raw),
|
timeout: Endpoint20_3(raw),
|
||||||
remove: Endpoint20_4(raw),
|
output: Endpoint20_4(raw),
|
||||||
|
remove: Endpoint20_5(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
||||||
|
|
@ -963,7 +1010,7 @@ const Endpoint21_3 = (raw: RawClient["server.question"]) => (input: Endpoint21_3
|
||||||
)
|
)
|
||||||
|
|
||||||
const adaptGroup21 = (raw: RawClient["server.question"]) => ({
|
const adaptGroup21 = (raw: RawClient["server.question"]) => ({
|
||||||
listRequests: Endpoint21_0(raw),
|
request: { list: Endpoint21_0(raw) },
|
||||||
list: Endpoint21_1(raw),
|
list: Endpoint21_1(raw),
|
||||||
reply: Endpoint21_2(raw),
|
reply: Endpoint21_2(raw),
|
||||||
reject: Endpoint21_3(raw),
|
reject: Endpoint21_3(raw),
|
||||||
|
|
@ -1043,7 +1090,14 @@ const adaptGroup24 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint24_0(r
|
||||||
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
|
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
|
||||||
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
|
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: Endpoint25_0(raw) })
|
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
|
||||||
|
type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
|
||||||
|
const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1Input) =>
|
||||||
|
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
|
||||||
|
location: { list: Endpoint25_0(raw), evict: Endpoint25_1(raw) },
|
||||||
|
})
|
||||||
|
|
||||||
const adaptClient = (raw: RawClient) => ({
|
const adaptClient = (raw: RawClient) => ({
|
||||||
health: adaptGroup0(raw["server.health"]),
|
health: adaptGroup0(raw["server.health"]),
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import type {
|
||||||
SessionActiveOutput,
|
SessionActiveOutput,
|
||||||
SessionGetInput,
|
SessionGetInput,
|
||||||
SessionGetOutput,
|
SessionGetOutput,
|
||||||
|
SessionRemoveInput,
|
||||||
|
SessionRemoveOutput,
|
||||||
SessionForkInput,
|
SessionForkInput,
|
||||||
SessionForkOutput,
|
SessionForkOutput,
|
||||||
SessionSwitchAgentInput,
|
SessionSwitchAgentInput,
|
||||||
|
|
@ -21,6 +23,8 @@ import type {
|
||||||
SessionSwitchModelOutput,
|
SessionSwitchModelOutput,
|
||||||
SessionRenameInput,
|
SessionRenameInput,
|
||||||
SessionRenameOutput,
|
SessionRenameOutput,
|
||||||
|
SessionMoveInput,
|
||||||
|
SessionMoveOutput,
|
||||||
SessionPromptInput,
|
SessionPromptInput,
|
||||||
SessionPromptOutput,
|
SessionPromptOutput,
|
||||||
SessionCommandInput,
|
SessionCommandInput,
|
||||||
|
|
@ -85,6 +89,8 @@ import type {
|
||||||
IntegrationAttemptCancelOutput,
|
IntegrationAttemptCancelOutput,
|
||||||
ServerMcpListInput,
|
ServerMcpListInput,
|
||||||
ServerMcpListOutput,
|
ServerMcpListOutput,
|
||||||
|
ServerMcpResourceCatalogInput,
|
||||||
|
ServerMcpResourceCatalogOutput,
|
||||||
CredentialUpdateInput,
|
CredentialUpdateInput,
|
||||||
CredentialUpdateOutput,
|
CredentialUpdateOutput,
|
||||||
CredentialRemoveInput,
|
CredentialRemoveInput,
|
||||||
|
|
@ -94,8 +100,8 @@ import type {
|
||||||
ProjectCurrentOutput,
|
ProjectCurrentOutput,
|
||||||
ProjectDirectoriesInput,
|
ProjectDirectoriesInput,
|
||||||
ProjectDirectoriesOutput,
|
ProjectDirectoriesOutput,
|
||||||
FormListRequestsInput,
|
FormRequestListInput,
|
||||||
FormListRequestsOutput,
|
FormRequestListOutput,
|
||||||
FormListInput,
|
FormListInput,
|
||||||
FormListOutput,
|
FormListOutput,
|
||||||
FormCreateInput,
|
FormCreateInput,
|
||||||
|
|
@ -108,12 +114,12 @@ import type {
|
||||||
FormReplyOutput,
|
FormReplyOutput,
|
||||||
FormCancelInput,
|
FormCancelInput,
|
||||||
FormCancelOutput,
|
FormCancelOutput,
|
||||||
PermissionListRequestsInput,
|
PermissionRequestListInput,
|
||||||
PermissionListRequestsOutput,
|
PermissionRequestListOutput,
|
||||||
PermissionListSavedInput,
|
PermissionSavedListInput,
|
||||||
PermissionListSavedOutput,
|
PermissionSavedListOutput,
|
||||||
PermissionRemoveSavedInput,
|
PermissionSavedRemoveInput,
|
||||||
PermissionRemoveSavedOutput,
|
PermissionSavedRemoveOutput,
|
||||||
PermissionCreateInput,
|
PermissionCreateInput,
|
||||||
PermissionCreateOutput,
|
PermissionCreateOutput,
|
||||||
PermissionListInput,
|
PermissionListInput,
|
||||||
|
|
@ -149,12 +155,14 @@ import type {
|
||||||
ShellCreateOutput,
|
ShellCreateOutput,
|
||||||
ShellGetInput,
|
ShellGetInput,
|
||||||
ShellGetOutput,
|
ShellGetOutput,
|
||||||
|
ShellTimeoutInput,
|
||||||
|
ShellTimeoutOutput,
|
||||||
ShellOutputInput,
|
ShellOutputInput,
|
||||||
ShellOutputOutput,
|
ShellOutputOutput,
|
||||||
ShellRemoveInput,
|
ShellRemoveInput,
|
||||||
ShellRemoveOutput,
|
ShellRemoveOutput,
|
||||||
QuestionListRequestsInput,
|
QuestionRequestListInput,
|
||||||
QuestionListRequestsOutput,
|
QuestionRequestListOutput,
|
||||||
QuestionListInput,
|
QuestionListInput,
|
||||||
QuestionListOutput,
|
QuestionListOutput,
|
||||||
QuestionReplyInput,
|
QuestionReplyInput,
|
||||||
|
|
@ -173,7 +181,9 @@ import type {
|
||||||
VcsStatusOutput,
|
VcsStatusOutput,
|
||||||
VcsDiffInput,
|
VcsDiffInput,
|
||||||
VcsDiffOutput,
|
VcsDiffOutput,
|
||||||
DebugLocationOutput,
|
DebugLocationListOutput,
|
||||||
|
DebugLocationEvictInput,
|
||||||
|
DebugLocationEvictOutput,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { ClientError } from "./client-error"
|
import { ClientError } from "./client-error"
|
||||||
|
|
||||||
|
|
@ -422,6 +432,17 @@ export function make(options: ClientOptions) {
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
).then((value) => value.data),
|
).then((value) => value.data),
|
||||||
|
remove: (input: SessionRemoveInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<SessionRemoveOutput>(
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
path: `/api/session/${encodeURIComponent(input.sessionID)}`,
|
||||||
|
successStatus: 204,
|
||||||
|
declaredStatuses: [404, 400, 401],
|
||||||
|
empty: true,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
fork: (input: SessionForkInput, requestOptions?: RequestOptions) =>
|
fork: (input: SessionForkInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: SessionForkOutput }>(
|
request<{ readonly data: SessionForkOutput }>(
|
||||||
{
|
{
|
||||||
|
|
@ -470,6 +491,18 @@ export function make(options: ClientOptions) {
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
|
move: (input: SessionMoveInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<SessionMoveOutput>(
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
|
||||||
|
body: { destination: input["destination"], moveChanges: input["moveChanges"] },
|
||||||
|
successStatus: 204,
|
||||||
|
declaredStatuses: [404, 400, 401],
|
||||||
|
empty: true,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) =>
|
prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: SessionPromptOutput }>(
|
request<{ readonly data: SessionPromptOutput }>(
|
||||||
{
|
{
|
||||||
|
|
@ -521,7 +554,12 @@ export function make(options: ClientOptions) {
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
|
||||||
body: { text: input["text"], description: input["description"], metadata: input["metadata"] },
|
body: {
|
||||||
|
text: input["text"],
|
||||||
|
description: input["description"],
|
||||||
|
metadata: input["metadata"],
|
||||||
|
resume: input["resume"],
|
||||||
|
},
|
||||||
successStatus: 204,
|
successStatus: 204,
|
||||||
declaredStatuses: [404, 400, 401],
|
declaredStatuses: [404, 400, 401],
|
||||||
empty: true,
|
empty: true,
|
||||||
|
|
@ -541,16 +579,17 @@ export function make(options: ClientOptions) {
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
compact: (input: SessionCompactInput, requestOptions?: RequestOptions) =>
|
compact: (input: SessionCompactInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionCompactOutput>(
|
request<{ readonly data: SessionCompactOutput }>(
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
|
||||||
successStatus: 204,
|
body: { id: input["id"] },
|
||||||
declaredStatuses: [404, 409, 503, 500, 400, 401],
|
successStatus: 200,
|
||||||
empty: true,
|
declaredStatuses: [409, 404, 400, 401],
|
||||||
|
empty: false,
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
).then((value) => value.data),
|
||||||
wait: (input: SessionWaitInput, requestOptions?: RequestOptions) =>
|
wait: (input: SessionWaitInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionWaitOutput>(
|
request<SessionWaitOutput>(
|
||||||
{
|
{
|
||||||
|
|
@ -562,40 +601,42 @@ export function make(options: ClientOptions) {
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
revertStage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
|
revert: {
|
||||||
request<{ readonly data: SessionRevertStageOutput }>(
|
stage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
|
||||||
{
|
request<{ readonly data: SessionRevertStageOutput }>(
|
||||||
method: "POST",
|
{
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
method: "POST",
|
||||||
body: { messageID: input["messageID"], files: input["files"] },
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
||||||
successStatus: 200,
|
body: { messageID: input["messageID"], files: input["files"] },
|
||||||
declaredStatuses: [404, 409, 500, 400, 401],
|
successStatus: 200,
|
||||||
empty: false,
|
declaredStatuses: [404, 409, 500, 400, 401],
|
||||||
},
|
empty: false,
|
||||||
requestOptions,
|
},
|
||||||
).then((value) => value.data),
|
requestOptions,
|
||||||
revertClear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
|
).then((value) => value.data),
|
||||||
request<SessionRevertClearOutput>(
|
clear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
|
||||||
{
|
request<SessionRevertClearOutput>(
|
||||||
method: "POST",
|
{
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
method: "POST",
|
||||||
successStatus: 204,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
||||||
declaredStatuses: [404, 409, 500, 400, 401],
|
successStatus: 204,
|
||||||
empty: true,
|
declaredStatuses: [404, 409, 500, 400, 401],
|
||||||
},
|
empty: true,
|
||||||
requestOptions,
|
},
|
||||||
),
|
requestOptions,
|
||||||
revertCommit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
|
),
|
||||||
request<SessionRevertCommitOutput>(
|
commit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
|
||||||
{
|
request<SessionRevertCommitOutput>(
|
||||||
method: "POST",
|
{
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
method: "POST",
|
||||||
successStatus: 204,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
||||||
declaredStatuses: [404, 409, 400, 401],
|
successStatus: 204,
|
||||||
empty: true,
|
declaredStatuses: [404, 409, 400, 401],
|
||||||
},
|
empty: true,
|
||||||
requestOptions,
|
},
|
||||||
),
|
requestOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
context: (input: SessionContextInput, requestOptions?: RequestOptions) =>
|
context: (input: SessionContextInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: SessionContextOutput }>(
|
request<{ readonly data: SessionContextOutput }>(
|
||||||
{
|
{
|
||||||
|
|
@ -797,69 +838,73 @@ export function make(options: ClientOptions) {
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
connectKey: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
|
connect: {
|
||||||
request<IntegrationConnectKeyOutput>(
|
key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
|
||||||
{
|
request<IntegrationConnectKeyOutput>(
|
||||||
method: "POST",
|
{
|
||||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
|
method: "POST",
|
||||||
query: { location: input["location"] },
|
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
|
||||||
body: { key: input["key"], label: input["label"] },
|
query: { location: input["location"] },
|
||||||
successStatus: 204,
|
body: { key: input["key"], label: input["label"] },
|
||||||
declaredStatuses: [400, 401],
|
successStatus: 204,
|
||||||
empty: true,
|
declaredStatuses: [400, 401],
|
||||||
},
|
empty: true,
|
||||||
requestOptions,
|
},
|
||||||
),
|
requestOptions,
|
||||||
connectOauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
|
),
|
||||||
request<IntegrationConnectOauthOutput>(
|
oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
|
||||||
{
|
request<IntegrationConnectOauthOutput>(
|
||||||
method: "POST",
|
{
|
||||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
|
method: "POST",
|
||||||
query: { location: input["location"] },
|
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
|
||||||
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
query: { location: input["location"] },
|
||||||
successStatus: 200,
|
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
||||||
declaredStatuses: [400, 401],
|
successStatus: 200,
|
||||||
empty: false,
|
declaredStatuses: [400, 401],
|
||||||
},
|
empty: false,
|
||||||
requestOptions,
|
},
|
||||||
),
|
requestOptions,
|
||||||
attemptStatus: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
|
),
|
||||||
request<IntegrationAttemptStatusOutput>(
|
},
|
||||||
{
|
attempt: {
|
||||||
method: "GET",
|
status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
|
||||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
request<IntegrationAttemptStatusOutput>(
|
||||||
query: { location: input["location"] },
|
{
|
||||||
successStatus: 200,
|
method: "GET",
|
||||||
declaredStatuses: [401, 400],
|
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||||
empty: false,
|
query: { location: input["location"] },
|
||||||
},
|
successStatus: 200,
|
||||||
requestOptions,
|
declaredStatuses: [401, 400],
|
||||||
),
|
empty: false,
|
||||||
attemptComplete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
|
},
|
||||||
request<IntegrationAttemptCompleteOutput>(
|
requestOptions,
|
||||||
{
|
),
|
||||||
method: "POST",
|
complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
|
||||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
|
request<IntegrationAttemptCompleteOutput>(
|
||||||
query: { location: input["location"] },
|
{
|
||||||
body: { code: input["code"] },
|
method: "POST",
|
||||||
successStatus: 204,
|
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
|
||||||
declaredStatuses: [400, 401],
|
query: { location: input["location"] },
|
||||||
empty: true,
|
body: { code: input["code"] },
|
||||||
},
|
successStatus: 204,
|
||||||
requestOptions,
|
declaredStatuses: [400, 401],
|
||||||
),
|
empty: true,
|
||||||
attemptCancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
|
},
|
||||||
request<IntegrationAttemptCancelOutput>(
|
requestOptions,
|
||||||
{
|
),
|
||||||
method: "DELETE",
|
cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
|
||||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
request<IntegrationAttemptCancelOutput>(
|
||||||
query: { location: input["location"] },
|
{
|
||||||
successStatus: 204,
|
method: "DELETE",
|
||||||
declaredStatuses: [401, 400],
|
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||||
empty: true,
|
query: { location: input["location"] },
|
||||||
},
|
successStatus: 204,
|
||||||
requestOptions,
|
declaredStatuses: [401, 400],
|
||||||
),
|
empty: true,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"server.mcp": {
|
"server.mcp": {
|
||||||
list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) =>
|
list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) =>
|
||||||
|
|
@ -874,6 +919,20 @@ export function make(options: ClientOptions) {
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
|
resource: {
|
||||||
|
catalog: (input?: ServerMcpResourceCatalogInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<ServerMcpResourceCatalogOutput>(
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/api/mcp/resource`,
|
||||||
|
query: { location: input?.["location"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [401, 400],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
credential: {
|
credential: {
|
||||||
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
|
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
|
||||||
|
|
@ -934,18 +993,20 @@ export function make(options: ClientOptions) {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
listRequests: (input?: FormListRequestsInput, requestOptions?: RequestOptions) =>
|
request: {
|
||||||
request<FormListRequestsOutput>(
|
list: (input?: FormRequestListInput, requestOptions?: RequestOptions) =>
|
||||||
{
|
request<FormRequestListOutput>(
|
||||||
method: "GET",
|
{
|
||||||
path: `/api/form/request`,
|
method: "GET",
|
||||||
query: { location: input?.["location"] },
|
path: `/api/form/request`,
|
||||||
successStatus: 200,
|
query: { location: input?.["location"] },
|
||||||
declaredStatuses: [401, 400],
|
successStatus: 200,
|
||||||
empty: false,
|
declaredStatuses: [401, 400],
|
||||||
},
|
empty: false,
|
||||||
requestOptions,
|
},
|
||||||
),
|
requestOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
list: (input: FormListInput, requestOptions?: RequestOptions) =>
|
list: (input: FormListInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: FormListOutput }>(
|
request<{ readonly data: FormListOutput }>(
|
||||||
{
|
{
|
||||||
|
|
@ -1023,41 +1084,45 @@ export function make(options: ClientOptions) {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
permission: {
|
permission: {
|
||||||
listRequests: (input?: PermissionListRequestsInput, requestOptions?: RequestOptions) =>
|
request: {
|
||||||
request<PermissionListRequestsOutput>(
|
list: (input?: PermissionRequestListInput, requestOptions?: RequestOptions) =>
|
||||||
{
|
request<PermissionRequestListOutput>(
|
||||||
method: "GET",
|
{
|
||||||
path: `/api/permission/request`,
|
method: "GET",
|
||||||
query: { location: input?.["location"] },
|
path: `/api/permission/request`,
|
||||||
successStatus: 200,
|
query: { location: input?.["location"] },
|
||||||
declaredStatuses: [401, 400],
|
successStatus: 200,
|
||||||
empty: false,
|
declaredStatuses: [401, 400],
|
||||||
},
|
empty: false,
|
||||||
requestOptions,
|
},
|
||||||
),
|
requestOptions,
|
||||||
listSaved: (input?: PermissionListSavedInput, requestOptions?: RequestOptions) =>
|
),
|
||||||
request<{ readonly data: PermissionListSavedOutput }>(
|
},
|
||||||
{
|
saved: {
|
||||||
method: "GET",
|
list: (input?: PermissionSavedListInput, requestOptions?: RequestOptions) =>
|
||||||
path: `/api/permission/saved`,
|
request<{ readonly data: PermissionSavedListOutput }>(
|
||||||
query: { projectID: input?.["projectID"] },
|
{
|
||||||
successStatus: 200,
|
method: "GET",
|
||||||
declaredStatuses: [401, 400],
|
path: `/api/permission/saved`,
|
||||||
empty: false,
|
query: { projectID: input?.["projectID"] },
|
||||||
},
|
successStatus: 200,
|
||||||
requestOptions,
|
declaredStatuses: [401, 400],
|
||||||
).then((value) => value.data),
|
empty: false,
|
||||||
removeSaved: (input: PermissionRemoveSavedInput, requestOptions?: RequestOptions) =>
|
},
|
||||||
request<PermissionRemoveSavedOutput>(
|
requestOptions,
|
||||||
{
|
).then((value) => value.data),
|
||||||
method: "DELETE",
|
remove: (input: PermissionSavedRemoveInput, requestOptions?: RequestOptions) =>
|
||||||
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
|
request<PermissionSavedRemoveOutput>(
|
||||||
successStatus: 204,
|
{
|
||||||
declaredStatuses: [401, 400],
|
method: "DELETE",
|
||||||
empty: true,
|
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
|
||||||
},
|
successStatus: 204,
|
||||||
requestOptions,
|
declaredStatuses: [401, 400],
|
||||||
),
|
empty: true,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
create: (input: PermissionCreateInput, requestOptions?: RequestOptions) =>
|
create: (input: PermissionCreateInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: PermissionCreateOutput }>(
|
request<{ readonly data: PermissionCreateOutput }>(
|
||||||
{
|
{
|
||||||
|
|
@ -1300,6 +1365,19 @@ export function make(options: ClientOptions) {
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
|
timeout: (input: ShellTimeoutInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<ShellTimeoutOutput>(
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
path: `/api/shell/${encodeURIComponent(input.id)}/timeout`,
|
||||||
|
query: { location: input["location"] },
|
||||||
|
body: { timeout: input["timeout"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [404, 401, 400],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
output: (input: ShellOutputInput, requestOptions?: RequestOptions) =>
|
output: (input: ShellOutputInput, requestOptions?: RequestOptions) =>
|
||||||
request<ShellOutputOutput>(
|
request<ShellOutputOutput>(
|
||||||
{
|
{
|
||||||
|
|
@ -1326,18 +1404,20 @@ export function make(options: ClientOptions) {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
question: {
|
question: {
|
||||||
listRequests: (input?: QuestionListRequestsInput, requestOptions?: RequestOptions) =>
|
request: {
|
||||||
request<QuestionListRequestsOutput>(
|
list: (input?: QuestionRequestListInput, requestOptions?: RequestOptions) =>
|
||||||
{
|
request<QuestionRequestListOutput>(
|
||||||
method: "GET",
|
{
|
||||||
path: `/api/question/request`,
|
method: "GET",
|
||||||
query: { location: input?.["location"] },
|
path: `/api/question/request`,
|
||||||
successStatus: 200,
|
query: { location: input?.["location"] },
|
||||||
declaredStatuses: [401, 400],
|
successStatus: 200,
|
||||||
empty: false,
|
declaredStatuses: [401, 400],
|
||||||
},
|
empty: false,
|
||||||
requestOptions,
|
},
|
||||||
),
|
requestOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
list: (input: QuestionListInput, requestOptions?: RequestOptions) =>
|
list: (input: QuestionListInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: QuestionListOutput }>(
|
request<{ readonly data: QuestionListOutput }>(
|
||||||
{
|
{
|
||||||
|
|
@ -1454,17 +1534,31 @@ export function make(options: ClientOptions) {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
debug: {
|
debug: {
|
||||||
location: (requestOptions?: RequestOptions) =>
|
location: {
|
||||||
request<DebugLocationOutput>(
|
list: (requestOptions?: RequestOptions) =>
|
||||||
{
|
request<DebugLocationListOutput>(
|
||||||
method: "GET",
|
{
|
||||||
path: `/api/debug/location`,
|
method: "GET",
|
||||||
successStatus: 200,
|
path: `/api/debug/location`,
|
||||||
declaredStatuses: [401, 400],
|
successStatus: 200,
|
||||||
empty: false,
|
declaredStatuses: [401, 400],
|
||||||
},
|
empty: false,
|
||||||
requestOptions,
|
},
|
||||||
),
|
requestOptions,
|
||||||
|
),
|
||||||
|
evict: (input?: DebugLocationEvictInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<DebugLocationEvictOutput>(
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
path: `/api/debug/location`,
|
||||||
|
query: { location: input?.["location"] },
|
||||||
|
successStatus: 204,
|
||||||
|
declaredStatuses: [401, 400],
|
||||||
|
empty: true,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -19,7 +19,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||||
import { Api } from "@opencode-ai/server/api"
|
import { Api } from "@opencode-ai/server/api"
|
||||||
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
||||||
import { ClientApi, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract"
|
import { ClientApi, groupNames, promiseOmitEndpoints } from "../src/contract"
|
||||||
|
|
||||||
const Client = await import("../src/effect")
|
const Client = await import("../src/effect")
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
|
||||||
expect(ProjectV2.Directory).toBe(Project.Directory)
|
expect(ProjectV2.Directory).toBe(Project.Directory)
|
||||||
expect(ProjectV2.Directories).toBe(Project.Directories)
|
expect(ProjectV2.Directories).toBe(Project.Directories)
|
||||||
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
|
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
|
||||||
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
|
expect(CoreSessionMessage.Info).toBe(SessionMessage.Info)
|
||||||
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
||||||
expect(Api.groups["server.project"].identifier).toBe("server.project")
|
expect(Api.groups["server.project"].identifier).toBe("server.project")
|
||||||
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
|
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
|
||||||
|
|
@ -49,8 +49,8 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
|
||||||
})
|
})
|
||||||
|
|
||||||
test("client and Server contracts generate identically", () => {
|
test("client and Server contracts generate identically", () => {
|
||||||
const server = compile(Api, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
|
const server = compile(Api, { groupNames, omitEndpoints: promiseOmitEndpoints })
|
||||||
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
|
const client = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
|
||||||
|
|
||||||
expect(emitPromise(client)).toEqual(emitPromise(server))
|
expect(emitPromise(client)).toEqual(emitPromise(server))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,9 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||||
if (url.includes("/prompt")) {
|
if (url.includes("/prompt")) {
|
||||||
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
|
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
|
||||||
}
|
}
|
||||||
|
if (url.endsWith("/compact")) {
|
||||||
|
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(compactionAdmission)))
|
||||||
|
}
|
||||||
if (url.includes("/context")) {
|
if (url.includes("/context")) {
|
||||||
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: [] })))
|
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: [] })))
|
||||||
}
|
}
|
||||||
|
|
@ -148,10 +151,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||||
}
|
}
|
||||||
if (url.endsWith("/api/session/active")) {
|
if (url.endsWith("/api/session/active")) {
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
HttpClientResponse.fromWeb(
|
HttpClientResponse.fromWeb(request, Response.json({ data: { ses_test: { type: "running" } } })),
|
||||||
request,
|
|
||||||
Response.json({ data: { ses_test: { type: "running" } } }),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (request.method === "POST" && url.endsWith("/api/session")) {
|
if (request.method === "POST" && url.endsWith("/api/session")) {
|
||||||
|
|
@ -161,10 +161,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||||
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })))
|
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })))
|
||||||
}
|
}
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
HttpClientResponse.fromWeb(
|
HttpClientResponse.fromWeb(request, Response.json({ data: [session.data], cursor: { next: "next" } })),
|
||||||
request,
|
|
||||||
Response.json({ data: [session.data], cursor: { next: "next" } }),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
const result = await Effect.gen(function* () {
|
const result = await Effect.gen(function* () {
|
||||||
|
|
@ -268,6 +265,16 @@ const admission = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compactionAdmission = {
|
||||||
|
data: {
|
||||||
|
type: "compaction",
|
||||||
|
admittedSeq: 1,
|
||||||
|
id: "msg_compaction",
|
||||||
|
sessionID: "ses_test",
|
||||||
|
timeCreated: 1_717_171_717_000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const modelSwitchedMessage = {
|
const modelSwitchedMessage = {
|
||||||
id: "msg_model",
|
id: "msg_model",
|
||||||
type: "model-switched",
|
type: "model-switched",
|
||||||
|
|
|
||||||
|
|
@ -33,23 +33,41 @@ test("exposes every standard HTTP API group", () => {
|
||||||
"debug",
|
"debug",
|
||||||
])
|
])
|
||||||
expect(Object.keys(client.debug)).toEqual(["location"])
|
expect(Object.keys(client.debug)).toEqual(["location"])
|
||||||
|
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
|
||||||
expect(Object.keys(client.message)).toEqual(["list"])
|
expect(Object.keys(client.message)).toEqual(["list"])
|
||||||
expect(Object.keys(client.integration)).toEqual([
|
expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "attempt"])
|
||||||
"list",
|
expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"])
|
||||||
"get",
|
expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"])
|
||||||
"connectKey",
|
|
||||||
"connectOauth",
|
|
||||||
"attemptStatus",
|
|
||||||
"attemptComplete",
|
|
||||||
"attemptCancel",
|
|
||||||
])
|
|
||||||
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
|
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
|
||||||
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
|
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
|
||||||
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
|
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
|
||||||
expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "output", "remove"])
|
expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"])
|
||||||
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
|
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("MCP resource catalog uses the public HTTP contract", async () => {
|
||||||
|
let request: Request | undefined
|
||||||
|
const client = OpenCode.make({
|
||||||
|
baseUrl: "http://localhost:3000",
|
||||||
|
fetch: async (input) => {
|
||||||
|
request = input instanceof Request ? input : new Request(input)
|
||||||
|
return Response.json({
|
||||||
|
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
|
||||||
|
data: {
|
||||||
|
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
|
||||||
|
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await client["server.mcp"].resource.catalog({ location: { directory: "/tmp/project" } })
|
||||||
|
|
||||||
|
expect(result.data.resources[0]?.uri).toBe("docs://readme")
|
||||||
|
expect(request?.method).toBe("GET")
|
||||||
|
expect(request?.url).toBe("http://localhost:3000/api/mcp/resource?location%5Bdirectory%5D=%2Ftmp%2Fproject")
|
||||||
|
})
|
||||||
|
|
||||||
test("file.read returns binary content from the public HTTP contract", async () => {
|
test("file.read returns binary content from the public HTTP contract", async () => {
|
||||||
let request: Request | undefined
|
let request: Request | undefined
|
||||||
const client = OpenCode.make({
|
const client = OpenCode.make({
|
||||||
|
|
@ -240,10 +258,10 @@ test("session methods use the public HTTP contract", async () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (url.includes("/prompt")) return Response.json(admission)
|
if (url.includes("/prompt")) return Response.json(admission)
|
||||||
|
if (url.endsWith("/compact")) return Response.json(compactionAdmission)
|
||||||
if (url.includes("/context")) return Response.json({ data: [] })
|
if (url.includes("/context")) return Response.json({ data: [] })
|
||||||
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
|
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
|
||||||
if (url.endsWith("/api/session/active"))
|
if (url.endsWith("/api/session/active")) return Response.json({ data: { ses_test: { type: "running" } } })
|
||||||
return Response.json({ data: { ses_test: { type: "running" } } })
|
|
||||||
if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
|
if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
|
||||||
if (init?.method === "POST") return new Response(null, { status: 204 })
|
if (init?.method === "POST") return new Response(null, { status: 204 })
|
||||||
return Response.json({ data: [session.data], cursor: { next: "next" } })
|
return Response.json({ data: [session.data], cursor: { next: "next" } })
|
||||||
|
|
@ -364,6 +382,16 @@ const admission = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compactionAdmission = {
|
||||||
|
data: {
|
||||||
|
type: "compaction",
|
||||||
|
admittedSeq: 1,
|
||||||
|
id: "msg_compaction",
|
||||||
|
sessionID: "ses_test",
|
||||||
|
timeCreated: 1_717_171_717_000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const modelSwitchedMessage = {
|
const modelSwitchedMessage = {
|
||||||
id: "msg_model",
|
id: "msg_model",
|
||||||
type: "model-switched",
|
type: "model-switched",
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ const result =
|
||||||
|
|
||||||
`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption.
|
`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption.
|
||||||
|
|
||||||
Successful result values are JSON-safe data. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well.
|
Successful result values are JSON-safe data. An explicit `return` produces the program result; when it is omitted, the final executable top-level expression is returned as a model-friendly REPL convenience. Otherwise reaching the end produces `null`. Returned `undefined` and nested `undefined` values are normalized to `null` as well.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
|
@ -237,11 +237,11 @@ A host cannot define its own `$codemode` top-level namespace.
|
||||||
|
|
||||||
CodeMode executes a deliberately bounded JavaScript subset. It supports:
|
CodeMode executes a deliberately bounded JavaScript subset. It supports:
|
||||||
|
|
||||||
- Plain data literals, property access, assignment, and destructuring.
|
- Plain data literals, property access, assignment, destructuring, and sequence expressions (the comma operator, evaluated left to right with the final value returned).
|
||||||
- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`.
|
- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets, including assignment-form destructuring such as `for ([key, value] of entries)`), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`.
|
||||||
- Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring.
|
- Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring.
|
||||||
- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`.
|
- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`.
|
||||||
- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
|
- Common array, string, number, `Object`, `Math`, and `JSON` operations, including primitive-number `valueOf`, the standard non-finite `Number` constants, and host-backed `Math.random`. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
|
||||||
- `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones).
|
- `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones).
|
||||||
- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout.
|
- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout.
|
||||||
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
|
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
|
||||||
|
|
|
||||||
|
|
@ -155,12 +155,10 @@ current omissions to implement, not intentional product boundaries.
|
||||||
collection values, then extend it to bounded host streams when a stream boundary exists.
|
collection values, then extend it to bounded host streams when a stream boundary exists.
|
||||||
- [ ] Support callback-bearing standard-library variants that models commonly generate: the mapper argument to
|
- [ ] Support callback-bearing standard-library variants that models commonly generate: the mapper argument to
|
||||||
`Array.from(...)` and replacers for `JSON.stringify(...)`, including Effect-aware callbacks where needed.
|
`Array.from(...)` and replacers for `JSON.stringify(...)`, including Effect-aware callbacks where needed.
|
||||||
- [ ] Close basic `Object` parity gaps: let `Object.values`/`Object.entries` accept arrays, make `Object.assign` validate
|
- [ ] Add `Object.is` after runtime method and tool references have stable identity semantics.
|
||||||
and mutate its target, add `Object.is`, and let `Object.fromEntries` consume every supported iterable.
|
|
||||||
- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set
|
- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set
|
||||||
composition methods, and `Array.prototype.toSpliced`.
|
composition methods, and `Array.prototype.toSpliced`.
|
||||||
- [ ] Complete the deterministic `Math` surface beyond the current arithmetic, rounding, root, power, and logarithm
|
- [ ] Decide whether iterable `Math.sumPrecise` belongs in the runtime.
|
||||||
helpers. Decide separately whether nondeterministic `Math.random` belongs in the runtime.
|
|
||||||
- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter
|
- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter
|
||||||
defects are distinguishable without leaking private causes.
|
defects are distinguishable without leaking private causes.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ export type Binding = {
|
||||||
|
|
||||||
export type StatementResult =
|
export type StatementResult =
|
||||||
| { kind: "none" }
|
| { kind: "none" }
|
||||||
| { kind: "value"; value: unknown }
|
|
||||||
| { kind: "return"; value: unknown }
|
| { kind: "return"; value: unknown }
|
||||||
| { kind: "break" }
|
| { kind: "break" }
|
||||||
| { kind: "continue" }
|
| { kind: "continue" }
|
||||||
|
|
@ -45,6 +44,7 @@ export class CodeModeFunction {
|
||||||
readonly parameters: ReadonlyArray<AstNode>,
|
readonly parameters: ReadonlyArray<AstNode>,
|
||||||
readonly body: AstNode,
|
readonly body: AstNode,
|
||||||
readonly capturedScopes: ReadonlyArray<Map<string, Binding>>,
|
readonly capturedScopes: ReadonlyArray<Map<string, Binding>>,
|
||||||
|
readonly async: boolean,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,7 +153,8 @@ export const unsupportedSyntax = (kind: string, node: AstNode): InterpreterRunti
|
||||||
[supportedSyntaxMessage],
|
[supportedSyntaxMessage],
|
||||||
)
|
)
|
||||||
|
|
||||||
export const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null
|
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
typeof value === "object" && value !== null
|
||||||
|
|
||||||
export const asNode = (value: unknown, context: string): AstNode => {
|
export const asNode = (value: unknown, context: string): AstNode => {
|
||||||
if (!isRecord(value) || typeof value.type !== "string") {
|
if (!isRecord(value) || typeof value.type !== "string") {
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ import {
|
||||||
numberMethods,
|
numberMethods,
|
||||||
numberStatics,
|
numberStatics,
|
||||||
} from "../stdlib/number.js"
|
} from "../stdlib/number.js"
|
||||||
import { invokeObjectMethod } from "../stdlib/object.js"
|
import { invokeObjectMethod, objectMethodsPreservingIdentity } from "../stdlib/object.js"
|
||||||
import { promiseStatics, TOOL_CALL_CONCURRENCY } from "../stdlib/promise.js"
|
import { promiseStatics, TOOL_CALL_CONCURRENCY } from "../stdlib/promise.js"
|
||||||
import {
|
import {
|
||||||
escapeRegexHint,
|
escapeRegexHint,
|
||||||
|
|
@ -530,17 +530,29 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
|
||||||
if (args[0] instanceof SandboxURLSearchParams) {
|
if (args[0] instanceof SandboxURLSearchParams) {
|
||||||
return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
|
return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
|
||||||
}
|
}
|
||||||
const source = boundedData(args[0], "Array.from input")
|
const source = args[0]
|
||||||
|
if (source instanceof SandboxPromise) {
|
||||||
|
throw new InterpreterRuntimeError(
|
||||||
|
"Array.from received an un-awaited Promise; await it before creating the array.",
|
||||||
|
node,
|
||||||
|
"InvalidDataValue",
|
||||||
|
)
|
||||||
|
}
|
||||||
if (typeof source === "string") return Array.from(source)
|
if (typeof source === "string") return Array.from(source)
|
||||||
if (Array.isArray(source)) return [...source]
|
if (Array.isArray(source)) return [...source]
|
||||||
if (
|
if (
|
||||||
source !== null &&
|
source !== null &&
|
||||||
typeof source === "object" &&
|
typeof source === "object" &&
|
||||||
|
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
|
||||||
typeof (source as { length?: unknown }).length === "number"
|
typeof (source as { length?: unknown }).length === "number"
|
||||||
) {
|
) {
|
||||||
return Array.from(source as ArrayLike<unknown>)
|
return Array.from(source as ArrayLike<unknown>)
|
||||||
}
|
}
|
||||||
throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node)
|
throw new InterpreterRuntimeError(
|
||||||
|
"Array.from expects an array, string, Map, Set, or array-like value.",
|
||||||
|
node,
|
||||||
|
"InvalidDataValue",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
|
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
|
||||||
|
|
@ -606,26 +618,26 @@ class Interpreter<R> {
|
||||||
// ToolRuntime.make like invokeTool: the interpreter never holds the tree itself.
|
// ToolRuntime.make like invokeTool: the interpreter never holds the tree itself.
|
||||||
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
||||||
private readonly logs: Array<string>
|
private readonly logs: Array<string>
|
||||||
private lastValue: unknown
|
|
||||||
// Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap).
|
// Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap).
|
||||||
private readonly callPermits: Semaphore.Semaphore
|
private readonly callPermits: Semaphore.Semaphore
|
||||||
// Fiber-backed promises whose settlement no program construct has observed yet. Successful
|
// Fiber-backed promises whose settlement no program construct has observed yet. Successful
|
||||||
// program completion drains these (like a runtime waiting on in-flight work at exit) and
|
// program completion drains these (like a runtime waiting on in-flight work at exit) and
|
||||||
// surfaces a never-awaited failure as an unhandled-rejection diagnostic.
|
// surfaces a never-awaited failure as an unhandled-rejection diagnostic.
|
||||||
private readonly pendingSettlements = new Set<SandboxPromise>()
|
private readonly pendingSettlements: Set<SandboxPromise>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
||||||
toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
|
toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
|
||||||
logs: Array<string> = [],
|
logs: Array<string> = [],
|
||||||
|
shared?: { callPermits: Semaphore.Semaphore; pendingSettlements: Set<SandboxPromise> },
|
||||||
) {
|
) {
|
||||||
const globalScope = new Map<string, Binding>()
|
const globalScope = new Map<string, Binding>()
|
||||||
this.scopes = [globalScope]
|
this.scopes = [globalScope]
|
||||||
this.invokeTool = invokeTool
|
this.invokeTool = invokeTool
|
||||||
this.toolKeys = toolKeys
|
this.toolKeys = toolKeys
|
||||||
this.logs = logs
|
this.logs = logs
|
||||||
this.lastValue = undefined
|
this.callPermits = shared?.callPermits ?? Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY)
|
||||||
this.callPermits = Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY)
|
this.pendingSettlements = shared?.pendingSettlements ?? new Set<SandboxPromise>()
|
||||||
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
|
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
|
||||||
globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() })
|
globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() })
|
||||||
globalScope.set("undefined", { mutable: false, value: undefined })
|
globalScope.set("undefined", { mutable: false, value: undefined })
|
||||||
|
|
@ -669,13 +681,15 @@ class Interpreter<R> {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
self.hoistFunctions(program.body)
|
self.hoistFunctions(program.body)
|
||||||
let value: unknown = undefined
|
let value: unknown = undefined
|
||||||
let returned = false
|
for (const [index, statement] of program.body.entries()) {
|
||||||
for (const statement of program.body) {
|
if (index === program.body.length - 1 && statement.type === "ExpressionStatement") {
|
||||||
|
value = yield* self.evaluateExpression(getNode(statement, "expression"))
|
||||||
|
break
|
||||||
|
}
|
||||||
const result = yield* self.evaluateStatement(statement)
|
const result = yield* self.evaluateStatement(statement)
|
||||||
|
|
||||||
if (result.kind === "return") {
|
if (result.kind === "return") {
|
||||||
value = result.value
|
value = result.value
|
||||||
returned = true
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -683,11 +697,7 @@ class Interpreter<R> {
|
||||||
throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
|
throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!returned) value = self.lastValue
|
|
||||||
|
|
||||||
// The program body runs inside an implicit async function, so a returned promise
|
// The program body runs inside an implicit async function, so a returned promise
|
||||||
// resolves before crossing the data boundary - `return tools.ns.tool(...)` works
|
// resolves before crossing the data boundary - `return tools.ns.tool(...)` works
|
||||||
|
|
@ -705,15 +715,17 @@ class Interpreter<R> {
|
||||||
private drainPendingSettlements(): Effect.Effect<void, unknown, never> {
|
private drainPendingSettlements(): Effect.Effect<void, unknown, never> {
|
||||||
const self = this
|
const self = this
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
for (const promise of [...self.pendingSettlements]) {
|
while (self.pendingSettlements.size > 0) {
|
||||||
|
const promise = self.pendingSettlements.values().next().value
|
||||||
|
if (promise === undefined) break
|
||||||
const exit = yield* self.observePromise(promise)
|
const exit = yield* self.observePromise(promise)
|
||||||
if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue
|
if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue
|
||||||
const failure = normalizeError(Cause.squash(exit.cause))
|
const failure = normalizeError(Cause.squash(exit.cause))
|
||||||
throw new InterpreterRuntimeError(
|
throw new InterpreterRuntimeError(
|
||||||
`Unhandled rejection from an un-awaited tool call: ${failure.message}`,
|
`Unhandled rejection from an un-awaited promise: ${failure.message}`,
|
||||||
undefined,
|
undefined,
|
||||||
failure.kind,
|
failure.kind,
|
||||||
["Await tool calls - `const result = await tools.ns.tool(...)` - so failures can be caught and handled."],
|
["Await promises so failures can be caught and handled."],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -727,17 +739,15 @@ class Interpreter<R> {
|
||||||
path: ReadonlyArray<string>,
|
path: ReadonlyArray<string>,
|
||||||
args: Array<unknown>,
|
args: Array<unknown>,
|
||||||
): Effect.Effect<SandboxPromise, never, R> {
|
): Effect.Effect<SandboxPromise, never, R> {
|
||||||
const self = this
|
return this.createPromise(this.callPermits.withPermit(Effect.suspend(() => this.invokeTool(path, args))))
|
||||||
return Effect.map(
|
}
|
||||||
Effect.forkChild(this.callPermits.withPermit(Effect.suspend(() => self.invokeTool(path, args))), {
|
|
||||||
startImmediately: true,
|
private createPromise(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<SandboxPromise, never, R> {
|
||||||
}),
|
return Effect.map(Effect.forkChild(effect, { startImmediately: true }), (fiber) => {
|
||||||
(fiber) => {
|
const promise = new SandboxPromise(fiber)
|
||||||
const promise = new SandboxPromise(fiber)
|
this.pendingSettlements.add(promise)
|
||||||
self.pendingSettlements.add(promise)
|
return promise
|
||||||
return promise
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The promise's settlement as an Exit, marking it observed for unhandled-rejection tracking.
|
// The promise's settlement as an Exit, marking it observed for unhandled-rejection tracking.
|
||||||
|
|
@ -778,7 +788,7 @@ class Interpreter<R> {
|
||||||
private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case "ExpressionStatement":
|
case "ExpressionStatement":
|
||||||
return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value }))
|
return Effect.as(this.evaluateExpression(getNode(node, "expression")), { kind: "none" })
|
||||||
case "VariableDeclaration":
|
case "VariableDeclaration":
|
||||||
return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" }))
|
return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" }))
|
||||||
case "ReturnStatement": {
|
case "ReturnStatement": {
|
||||||
|
|
@ -831,11 +841,6 @@ class Interpreter<R> {
|
||||||
const statement = asNode(statementValue, "body")
|
const statement = asNode(statementValue, "body")
|
||||||
const result = yield* self.evaluateStatement(statement)
|
const result = yield* self.evaluateStatement(statement)
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.kind !== "none") {
|
if (result.kind !== "none") {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
@ -858,6 +863,7 @@ class Interpreter<R> {
|
||||||
getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)),
|
getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)),
|
||||||
getNode(node, "body"),
|
getNode(node, "body"),
|
||||||
this.scopes.slice(),
|
this.scopes.slice(),
|
||||||
|
node.async === true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -926,7 +932,6 @@ class Interpreter<R> {
|
||||||
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
|
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
|
||||||
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
|
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
|
||||||
if (result.kind === "return" || result.kind === "continue") return result
|
if (result.kind === "return" || result.kind === "continue") return result
|
||||||
if (result.kind === "value") self.lastValue = result.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { kind: "none" } satisfies StatementResult
|
return { kind: "none" } satisfies StatementResult
|
||||||
|
|
@ -954,9 +959,6 @@ class Interpreter<R> {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { kind: "none" } satisfies StatementResult
|
return { kind: "none" } satisfies StatementResult
|
||||||
|
|
@ -984,9 +986,6 @@ class Interpreter<R> {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
} while (yield* self.evaluateExpression(testNode))
|
} while (yield* self.evaluateExpression(testNode))
|
||||||
|
|
||||||
return { kind: "none" } satisfies StatementResult
|
return { kind: "none" } satisfies StatementResult
|
||||||
|
|
@ -1042,10 +1041,6 @@ class Interpreter<R> {
|
||||||
return { kind: "none" } satisfies StatementResult
|
return { kind: "none" } satisfies StatementResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iterationScope) {
|
if (iterationScope) {
|
||||||
const loopScope = self.currentScope()
|
const loopScope = self.currentScope()
|
||||||
for (const name of perIterationBindings) {
|
for (const name of perIterationBindings) {
|
||||||
|
|
@ -1085,7 +1080,7 @@ class Interpreter<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
|
let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
|
||||||
let assignmentName: string | undefined
|
let assignment: AstNode | undefined
|
||||||
|
|
||||||
if (left.type === "VariableDeclaration") {
|
if (left.type === "VariableDeclaration") {
|
||||||
const declarations = getArray(left, "declarations")
|
const declarations = getArray(left, "declarations")
|
||||||
|
|
@ -1095,8 +1090,13 @@ class Interpreter<R> {
|
||||||
|
|
||||||
const declarator = asNode(declarations[0], "declarations[0]")
|
const declarator = asNode(declarations[0], "declarations[0]")
|
||||||
declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
|
declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
|
||||||
} else if (left.type === "Identifier") {
|
} else if (
|
||||||
assignmentName = getString(left, "name")
|
left.type === "Identifier" ||
|
||||||
|
left.type === "MemberExpression" ||
|
||||||
|
left.type === "ArrayPattern" ||
|
||||||
|
left.type === "ObjectPattern"
|
||||||
|
) {
|
||||||
|
assignment = left
|
||||||
} else {
|
} else {
|
||||||
throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
|
throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
|
||||||
}
|
}
|
||||||
|
|
@ -1105,8 +1105,8 @@ class Interpreter<R> {
|
||||||
if (declaration) {
|
if (declaration) {
|
||||||
self.pushScope()
|
self.pushScope()
|
||||||
yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left)
|
yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left)
|
||||||
} else if (assignmentName) {
|
} else if (assignment) {
|
||||||
self.setIdentifierValue(assignmentName, value, left)
|
yield* self.assignPattern(assignment, value, left)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = yield* self.evaluateStatement(body).pipe(
|
const result = yield* self.evaluateStatement(body).pipe(
|
||||||
|
|
@ -1125,10 +1125,6 @@ class Interpreter<R> {
|
||||||
return { kind: "none" }
|
return { kind: "none" }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.kind === "continue") {
|
if (result.kind === "continue") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1218,10 +1214,6 @@ class Interpreter<R> {
|
||||||
return { kind: "none" }
|
return { kind: "none" }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.kind === "continue") {
|
if (result.kind === "continue") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1504,6 +1496,16 @@ class Interpreter<R> {
|
||||||
return this.evaluateUnaryExpression(node)
|
return this.evaluateUnaryExpression(node)
|
||||||
case "AssignmentExpression":
|
case "AssignmentExpression":
|
||||||
return this.evaluateAssignmentExpression(node)
|
return this.evaluateAssignmentExpression(node)
|
||||||
|
case "SequenceExpression": {
|
||||||
|
const self = this
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
let result: unknown
|
||||||
|
for (const expression of getArray(node, "expressions")) {
|
||||||
|
result = yield* self.evaluateExpression(asNode(expression, "expressions"))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
case "CallExpression":
|
case "CallExpression":
|
||||||
return this.evaluateCallExpression(node)
|
return this.evaluateCallExpression(node)
|
||||||
case "ArrowFunctionExpression":
|
case "ArrowFunctionExpression":
|
||||||
|
|
@ -2010,9 +2012,12 @@ class Interpreter<R> {
|
||||||
if (callable instanceof GlobalMethodReference) {
|
if (callable instanceof GlobalMethodReference) {
|
||||||
if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node)
|
if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node)
|
||||||
if (callable.namespace === "Object" && args[0] instanceof ToolReference) {
|
if (callable.namespace === "Object" && args[0] instanceof ToolReference) {
|
||||||
return self.invokeObjectMethodOnTools(callable.name, args[0] as ToolReference, node)
|
return self.invokeObjectMethodOnTools(callable.name, args[0], node)
|
||||||
}
|
}
|
||||||
if (callable.namespace === "Object" && callable.name === "assign") {
|
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
|
||||||
|
return invokeGlobalMethod(callable, args, node)
|
||||||
|
}
|
||||||
|
if (callable.namespace === "Array" && (callable.name === "from" || callable.name === "of")) {
|
||||||
return invokeGlobalMethod(callable, args, node)
|
return invokeGlobalMethod(callable, args, node)
|
||||||
}
|
}
|
||||||
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
|
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
|
||||||
|
|
@ -2033,8 +2038,8 @@ class Interpreter<R> {
|
||||||
|
|
||||||
// Object.* over a tool reference: `Object.keys(tools)` / `Object.keys(tools.ns)` enumerate
|
// Object.* over a tool reference: `Object.keys(tools)` / `Object.keys(tools.ns)` enumerate
|
||||||
// namespace/tool names from the host tool tree - the discovery idiom a model reaches for
|
// namespace/tool names from the host tool tree - the discovery idiom a model reaches for
|
||||||
// first. Every other Object helper cannot produce data from a tool reference, so it fails
|
// first. Other Object helpers fail with a pointer at the working idioms instead of a generic
|
||||||
// with a pointer at the working idioms instead of the generic plain-objects-only message.
|
// plain-data message.
|
||||||
private invokeObjectMethodOnTools(name: string, ref: ToolReference, node: AstNode): unknown {
|
private invokeObjectMethodOnTools(name: string, ref: ToolReference, node: AstNode): unknown {
|
||||||
if (name === "keys") {
|
if (name === "keys") {
|
||||||
return boundedData(this.enumerableKeys(ref)!, "Object.keys result")
|
return boundedData(this.enumerableKeys(ref)!, "Object.keys result")
|
||||||
|
|
@ -2226,14 +2231,36 @@ class Interpreter<R> {
|
||||||
switch (ref.name) {
|
switch (ref.name) {
|
||||||
case "all": {
|
case "all": {
|
||||||
// Mark every promise element observed up-front (Promise.all handles all of its
|
// Mark every promise element observed up-front (Promise.all handles all of its
|
||||||
// members' failures, as in JS), then join in index order; the first failure rejects
|
// members' failures, as in JS), race their settlements for fail-fast rejection, and
|
||||||
// the whole call while unrelated in-flight members keep running.
|
// preserve input order when they all fulfill. Rejected calls keep draining siblings.
|
||||||
const settles = items.map((item) =>
|
const observations = items.map((item, index) =>
|
||||||
item instanceof SandboxPromise ? this.settlePromise(item, node) : Effect.succeed(item),
|
item instanceof SandboxPromise
|
||||||
|
? Effect.map(this.observePromise(item), (exit) => ({ index, item, exit }))
|
||||||
|
: Effect.succeed({ index, item: undefined, exit: Exit.succeed(item) }),
|
||||||
)
|
)
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
|
const remaining = [...observations]
|
||||||
const values: Array<unknown> = []
|
const values: Array<unknown> = []
|
||||||
for (const settle of settles) values.push(yield* settle)
|
values.length = items.length
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
const winner = yield* Effect.raceAll(remaining)
|
||||||
|
const position = remaining.indexOf(observations[winner.index])
|
||||||
|
if (position >= 0) remaining.splice(position, 1)
|
||||||
|
if (Exit.isSuccess(winner.exit)) {
|
||||||
|
values[winner.index] = winner.exit.value
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
yield* self.createPromise(
|
||||||
|
Effect.asVoid(
|
||||||
|
Effect.forEach(
|
||||||
|
items,
|
||||||
|
(item) => (item instanceof SandboxPromise ? self.observePromise(item) : Effect.void),
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return yield* self.unwrapPromiseExit(winner.item, winner.exit, node)
|
||||||
|
}
|
||||||
return values
|
return values
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -2307,43 +2334,42 @@ class Interpreter<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
||||||
const self = this
|
const invocation = new Interpreter(this.invokeTool, this.toolKeys, this.logs, {
|
||||||
return Effect.suspend(() => {
|
callPermits: this.callPermits,
|
||||||
const savedScopes = self.scopes
|
pendingSettlements: this.pendingSettlements,
|
||||||
self.scopes = [...fn.capturedScopes, new Map<string, Binding>()]
|
|
||||||
const run = Effect.gen(function* () {
|
|
||||||
// Seed every parameter name into the scope as a TDZ slot first, so a default that
|
|
||||||
// references another parameter resolves to that (uninitialized) param rather than
|
|
||||||
// silently falling through to an outer binding of the same name - matching JS.
|
|
||||||
const paramScope = self.currentScope()
|
|
||||||
for (const parameter of fn.parameters) {
|
|
||||||
for (const name of collectPatternNames(parameter)) {
|
|
||||||
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const [index, parameter] of fn.parameters.entries()) {
|
|
||||||
if (parameter.type === "RestElement") {
|
|
||||||
yield* self.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
yield* self.declarePattern(parameter, args[index], true, parameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fn.body.type === "BlockStatement") {
|
|
||||||
const result = yield* self.evaluateStatement(fn.body)
|
|
||||||
return result.kind === "return" || result.kind === "value" ? result.value : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return yield* self.evaluateExpression(fn.body)
|
|
||||||
})
|
|
||||||
return run.pipe(
|
|
||||||
Effect.ensuring(
|
|
||||||
Effect.sync(() => {
|
|
||||||
self.scopes = savedScopes
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
invocation.scopes = [...fn.capturedScopes, new Map<string, Binding>()]
|
||||||
|
const run = Effect.gen(function* () {
|
||||||
|
// Seed every parameter name into the scope as a TDZ slot first, so a default that
|
||||||
|
// references another parameter resolves to that (uninitialized) param rather than
|
||||||
|
// silently falling through to an outer binding of the same name - matching JS.
|
||||||
|
const paramScope = invocation.currentScope()
|
||||||
|
for (const parameter of fn.parameters) {
|
||||||
|
for (const name of collectPatternNames(parameter)) {
|
||||||
|
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [index, parameter] of fn.parameters.entries()) {
|
||||||
|
if (parameter.type === "RestElement") {
|
||||||
|
yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
yield* invocation.declarePattern(parameter, args[index], true, parameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fn.body.type === "BlockStatement") {
|
||||||
|
const result = yield* invocation.evaluateStatement(fn.body)
|
||||||
|
return result.kind === "return" ? result.value : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return yield* invocation.evaluateExpression(fn.body)
|
||||||
|
})
|
||||||
|
if (!fn.async) return run
|
||||||
|
return this.createPromise(
|
||||||
|
Effect.flatMap(run, (value) =>
|
||||||
|
value instanceof SandboxPromise ? invocation.settlePromise(value) : Effect.succeed(value),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private invokeIntrinsic(
|
private invokeIntrinsic(
|
||||||
|
|
@ -2432,13 +2458,19 @@ class Interpreter<R> {
|
||||||
else value.replaceAll(pattern, collect)
|
else value.replaceAll(pattern, collect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const self = this
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const output: Array<string> = []
|
const output: Array<string> = []
|
||||||
let end = 0
|
let end = 0
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
|
const replacement = yield* apply(match.args)
|
||||||
|
const resolved =
|
||||||
|
args[1] instanceof CodeModeFunction && args[1].async && replacement instanceof SandboxPromise
|
||||||
|
? yield* self.settlePromise(replacement)
|
||||||
|
: replacement
|
||||||
output.push(
|
output.push(
|
||||||
value.slice(end, match.offset),
|
value.slice(end, match.offset),
|
||||||
coerceToString(boundedData(yield* apply(match.args), `String.${name} replacer result`)),
|
coerceToString(boundedData(resolved, `String.${name} replacer result`)),
|
||||||
)
|
)
|
||||||
end = match.offset + match.match.length
|
end = match.offset + match.match.length
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
|
export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
|
||||||
|
|
||||||
export const mathMethods = new Set([
|
export const mathMethods = new Set([
|
||||||
|
"random",
|
||||||
"max",
|
"max",
|
||||||
"min",
|
"min",
|
||||||
"abs",
|
"abs",
|
||||||
|
"acos",
|
||||||
|
"acosh",
|
||||||
|
"asin",
|
||||||
|
"asinh",
|
||||||
|
"atan",
|
||||||
|
"atan2",
|
||||||
|
"atanh",
|
||||||
"floor",
|
"floor",
|
||||||
"ceil",
|
"ceil",
|
||||||
"round",
|
"round",
|
||||||
|
|
@ -13,14 +21,27 @@ export const mathMethods = new Set([
|
||||||
"cbrt",
|
"cbrt",
|
||||||
"pow",
|
"pow",
|
||||||
"hypot",
|
"hypot",
|
||||||
|
"cos",
|
||||||
|
"cosh",
|
||||||
|
"sin",
|
||||||
|
"sinh",
|
||||||
|
"tan",
|
||||||
|
"tanh",
|
||||||
"log",
|
"log",
|
||||||
"log2",
|
"log2",
|
||||||
"log10",
|
"log10",
|
||||||
|
"log1p",
|
||||||
"exp",
|
"exp",
|
||||||
|
"expm1",
|
||||||
|
"f16round",
|
||||||
|
"fround",
|
||||||
|
"clz32",
|
||||||
|
"imul",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
|
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
|
||||||
if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
|
if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
|
||||||
|
if (name === "random") return Math.random()
|
||||||
const nums = args.map((arg) => {
|
const nums = args.map((arg) => {
|
||||||
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
|
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
|
||||||
return arg
|
return arg
|
||||||
|
|
@ -33,6 +54,20 @@ export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNo
|
||||||
return Math.min(...nums)
|
return Math.min(...nums)
|
||||||
case "abs":
|
case "abs":
|
||||||
return Math.abs(a)
|
return Math.abs(a)
|
||||||
|
case "acos":
|
||||||
|
return Math.acos(a)
|
||||||
|
case "acosh":
|
||||||
|
return Math.acosh(a)
|
||||||
|
case "asin":
|
||||||
|
return Math.asin(a)
|
||||||
|
case "asinh":
|
||||||
|
return Math.asinh(a)
|
||||||
|
case "atan":
|
||||||
|
return Math.atan(a)
|
||||||
|
case "atan2":
|
||||||
|
return Math.atan2(a, b)
|
||||||
|
case "atanh":
|
||||||
|
return Math.atanh(a)
|
||||||
case "floor":
|
case "floor":
|
||||||
return Math.floor(a)
|
return Math.floor(a)
|
||||||
case "ceil":
|
case "ceil":
|
||||||
|
|
@ -51,14 +86,38 @@ export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNo
|
||||||
return Math.pow(a, b)
|
return Math.pow(a, b)
|
||||||
case "hypot":
|
case "hypot":
|
||||||
return Math.hypot(...nums)
|
return Math.hypot(...nums)
|
||||||
|
case "cos":
|
||||||
|
return Math.cos(a)
|
||||||
|
case "cosh":
|
||||||
|
return Math.cosh(a)
|
||||||
|
case "sin":
|
||||||
|
return Math.sin(a)
|
||||||
|
case "sinh":
|
||||||
|
return Math.sinh(a)
|
||||||
|
case "tan":
|
||||||
|
return Math.tan(a)
|
||||||
|
case "tanh":
|
||||||
|
return Math.tanh(a)
|
||||||
case "log":
|
case "log":
|
||||||
return Math.log(a)
|
return Math.log(a)
|
||||||
case "log2":
|
case "log2":
|
||||||
return Math.log2(a)
|
return Math.log2(a)
|
||||||
case "log10":
|
case "log10":
|
||||||
return Math.log10(a)
|
return Math.log10(a)
|
||||||
|
case "log1p":
|
||||||
|
return Math.log1p(a)
|
||||||
case "exp":
|
case "exp":
|
||||||
return Math.exp(a)
|
return Math.exp(a)
|
||||||
|
case "expm1":
|
||||||
|
return Math.expm1(a)
|
||||||
|
case "f16round":
|
||||||
|
return Math.f16round(a)
|
||||||
|
case "fround":
|
||||||
|
return Math.fround(a)
|
||||||
|
case "clz32":
|
||||||
|
return Math.clz32(a)
|
||||||
|
case "imul":
|
||||||
|
return Math.imul(a, b)
|
||||||
}
|
}
|
||||||
throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
|
throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"])
|
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString", "valueOf"])
|
||||||
|
|
||||||
export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"])
|
export const numberConstants = new Set([
|
||||||
|
"MAX_SAFE_INTEGER",
|
||||||
|
"MIN_SAFE_INTEGER",
|
||||||
|
"MAX_VALUE",
|
||||||
|
"MIN_VALUE",
|
||||||
|
"EPSILON",
|
||||||
|
"NaN",
|
||||||
|
"POSITIVE_INFINITY",
|
||||||
|
"NEGATIVE_INFINITY",
|
||||||
|
])
|
||||||
|
|
||||||
export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"])
|
export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"])
|
||||||
|
|
||||||
|
|
@ -32,6 +41,9 @@ export const invokeNumberMethod = (value: number, name: string, args: Array<unkn
|
||||||
result = value.toString(radix)
|
result = value.toString(radix)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "valueOf":
|
||||||
|
result = value
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node)
|
throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,45 @@
|
||||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||||
import { isBlockedMember } from "../tool-runtime.js"
|
import { isBlockedMember } from "../tool-runtime.js"
|
||||||
import { isSandboxValue, SandboxMap, SandboxURLSearchParams } from "../values.js"
|
import { isSandboxValue, SandboxMap, SandboxPromise, SandboxSet, SandboxURLSearchParams } from "../values.js"
|
||||||
import { boundedData, coerceToString } from "./value.js"
|
import { boundedData, coerceToString } from "./value.js"
|
||||||
|
|
||||||
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"])
|
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"])
|
||||||
|
export const objectMethodsPreservingIdentity = new Set(["assign", "values", "entries", "fromEntries"])
|
||||||
|
|
||||||
export const invokeObjectMethod = (name: string, args: Array<unknown>, node: AstNode): unknown => {
|
export const invokeObjectMethod = (name: string, args: Array<unknown>, node: AstNode): unknown => {
|
||||||
if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node)
|
if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node)
|
||||||
const requireObject = (): Record<string, unknown> => {
|
const requireObject = (): Record<string, unknown> => {
|
||||||
const value = boundedData(args[0], `Object.${name} input`)
|
const input = args[0]
|
||||||
if (isSandboxValue(value)) return {}
|
if (Array.isArray(input)) return input as unknown as Record<string, unknown>
|
||||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
if (isSandboxValue(input)) return {}
|
||||||
throw new InterpreterRuntimeError(`Object.${name} expects a data object.`, node)
|
if (input instanceof SandboxPromise) {
|
||||||
|
throw new InterpreterRuntimeError(
|
||||||
|
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
|
||||||
|
node,
|
||||||
|
"InvalidDataValue",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return value as Record<string, unknown>
|
if (input === null || typeof input !== "object") {
|
||||||
|
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
|
||||||
|
}
|
||||||
|
const prototype = Object.getPrototypeOf(input)
|
||||||
|
if (prototype !== null && prototype !== Object.prototype) {
|
||||||
|
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
|
||||||
|
}
|
||||||
|
return input as Record<string, unknown>
|
||||||
}
|
}
|
||||||
const guardedSet = (out: Record<string, unknown>, key: string, item: unknown): void => {
|
const guardedSet = (out: Record<string, unknown>, key: string, item: unknown): void => {
|
||||||
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node)
|
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node)
|
||||||
out[key] = item
|
out[key] = item
|
||||||
}
|
}
|
||||||
|
const addEntry = (out: Record<string, unknown>, key: unknown, item: unknown): void => {
|
||||||
|
boundedData(key, "Object.fromEntries key")
|
||||||
|
boundedData(item, "Object.fromEntries value")
|
||||||
|
guardedSet(out, coerceToString(key), item)
|
||||||
|
}
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "keys": {
|
case "keys":
|
||||||
const value = boundedData(args[0], "Object.keys input")
|
return Object.keys(requireObject())
|
||||||
if (isSandboxValue(value)) return []
|
|
||||||
if (Array.isArray(value)) return Object.keys(value)
|
|
||||||
if (value === null || typeof value !== "object") {
|
|
||||||
throw new InterpreterRuntimeError("Object.keys expects a data object or array.", node)
|
|
||||||
}
|
|
||||||
return Object.keys(value)
|
|
||||||
}
|
|
||||||
case "values":
|
case "values":
|
||||||
return Object.values(requireObject())
|
return Object.values(requireObject())
|
||||||
case "entries":
|
case "entries":
|
||||||
|
|
@ -55,7 +66,7 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
|
||||||
case "fromEntries": {
|
case "fromEntries": {
|
||||||
if (args[0] instanceof SandboxMap) {
|
if (args[0] instanceof SandboxMap) {
|
||||||
const out: Record<string, unknown> = Object.create(null)
|
const out: Record<string, unknown> = Object.create(null)
|
||||||
for (const [key, item] of args[0].map.entries()) guardedSet(out, coerceToString(key), item)
|
for (const [key, item] of args[0].map.entries()) addEntry(out, key, item)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
if (args[0] instanceof SandboxURLSearchParams) {
|
if (args[0] instanceof SandboxURLSearchParams) {
|
||||||
|
|
@ -63,16 +74,18 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
|
||||||
for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value)
|
for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
const pairs = boundedData(args[0], "Object.fromEntries input")
|
const pairs = args[0] instanceof SandboxSet ? Array.from(args[0].set.values()) : args[0]
|
||||||
if (!Array.isArray(pairs)) {
|
if (!Array.isArray(pairs)) {
|
||||||
|
boundedData(args[0], "Object.fromEntries input")
|
||||||
throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node)
|
throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node)
|
||||||
}
|
}
|
||||||
const out: Record<string, unknown> = Object.create(null)
|
const out: Record<string, unknown> = Object.create(null)
|
||||||
for (const pair of pairs) {
|
for (const pair of pairs) {
|
||||||
if (!Array.isArray(pair)) {
|
const validated = boundedData(pair, "Object.fromEntries entry")
|
||||||
throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] pairs.", node)
|
if (validated === null || typeof validated !== "object" || isSandboxValue(validated))
|
||||||
}
|
throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node)
|
||||||
guardedSet(out, String(pair[0]), pair[1])
|
const entry = pair as Record<string, unknown>
|
||||||
|
addEntry(out, entry[0], entry[1])
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,16 @@ const copyBounded = (
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveSandboxValues))
|
const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveSandboxValues))
|
||||||
|
if (preserveSandboxValues) {
|
||||||
|
// Array metadata is not serialized, but intra-sandbox copies must retain it.
|
||||||
|
for (const [key, item] of Object.entries(value)) {
|
||||||
|
if (Object.hasOwn(copied, key)) continue
|
||||||
|
if (isBlockedMember(key)) {
|
||||||
|
throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`)
|
||||||
|
}
|
||||||
|
Reflect.set(copied, key, copyBounded(item, label, depth + 1, seen, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
seen.delete(value)
|
seen.delete(value)
|
||||||
return copied
|
return copied
|
||||||
}
|
}
|
||||||
|
|
@ -607,6 +617,7 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
|
||||||
"",
|
"",
|
||||||
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
|
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
|
||||||
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
|
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
|
||||||
|
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
|
||||||
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -658,6 +658,9 @@ describe("CodeMode public contract", () => {
|
||||||
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
|
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
|
||||||
expect(instructions).not.toContain("host globals")
|
expect(instructions).not.toContain("host globals")
|
||||||
expect(instructions).toContain("Use Code Mode tools for external operations")
|
expect(instructions).toContain("Use Code Mode tools for external operations")
|
||||||
|
expect(instructions).toContain(
|
||||||
|
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
|
||||||
|
)
|
||||||
expect(instructions).toContain(
|
expect(instructions).toContain(
|
||||||
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
||||||
)
|
)
|
||||||
|
|
@ -1081,6 +1084,24 @@ describe("CodeMode public contract", () => {
|
||||||
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
|
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("returns the final top-level expression when return is omitted", async () => {
|
||||||
|
const result = await Effect.runPromise(CodeMode.execute({ code: `1; 2` }))
|
||||||
|
|
||||||
|
expect(result).toStrictEqual({ ok: true, value: 2, toolCalls: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not implicitly return expressions nested in control flow", async () => {
|
||||||
|
const result = await Effect.runPromise(CodeMode.execute({ code: `if (true) { 2 }` }))
|
||||||
|
|
||||||
|
expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null when the final top-level statement is not an expression", async () => {
|
||||||
|
const result = await Effect.runPromise(CodeMode.execute({ code: `1; const value = 2` }))
|
||||||
|
|
||||||
|
expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
|
||||||
|
})
|
||||||
|
|
||||||
test("rejects invalid configuration and discovery limits", async () => {
|
test("rejects invalid configuration and discovery limits", async () => {
|
||||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
|
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
|
||||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
|
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
|
||||||
|
|
|
||||||
7398
packages/codemode/test/fixtures/opencode-v2-openapi.json
vendored
7398
packages/codemode/test/fixtures/opencode-v2-openapi.json
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -177,13 +177,13 @@ describe("OpenAPI.fromSpec", () => {
|
||||||
const spec = await opencodeSpec()
|
const spec = await opencodeSpec()
|
||||||
const result = OpenAPI.fromSpec({ spec, baseUrl })
|
const result = OpenAPI.fromSpec({ spec, baseUrl })
|
||||||
|
|
||||||
expect(result.skipped).toHaveLength(5)
|
expect(result.skipped).toHaveLength(4)
|
||||||
expect(result.skipped).toContainEqual({
|
expect(result.skipped).toContainEqual({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/api/pty/{ptyID}/connect",
|
path: "/api/pty/{ptyID}/connect",
|
||||||
reason: "WebSocket operations are not supported",
|
reason: "WebSocket operations are not supported",
|
||||||
})
|
})
|
||||||
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(3)
|
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(2)
|
||||||
expect(result.skipped).toContainEqual({
|
expect(result.skipped).toContainEqual({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/api/fs/read/*",
|
path: "/api/fs/read/*",
|
||||||
|
|
@ -210,11 +210,11 @@ describe("OpenAPI.fromSpec", () => {
|
||||||
if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated")
|
if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated")
|
||||||
expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }")
|
expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }")
|
||||||
expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined()
|
expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined()
|
||||||
expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined()
|
expect(Tool.isDefinition(toolAt(result.tools, "v2.pty.connect"))).toBe(false)
|
||||||
expect(toolAt(result.tools, "v2.session.log")).toBeUndefined()
|
expect(toolAt(result.tools, "v2.session.log")).toBeUndefined()
|
||||||
expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined()
|
expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined()
|
||||||
expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined()
|
expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined()
|
||||||
expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined()
|
expect(toolAt(result.tools, "v2.pty.connect.token")).not.toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves operation path sanitization and collision handling", () => {
|
test("preserves operation path sanitization and collision handling", () => {
|
||||||
|
|
|
||||||
|
|
@ -464,6 +464,54 @@ describe("H5: builtin coercion functions work as array callbacks", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("for...of assignment destructuring", () => {
|
||||||
|
test("assigns entry pairs into predeclared variables", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let key
|
||||||
|
let item
|
||||||
|
const out = []
|
||||||
|
for ([key, item] of Object.entries({ a: 1, b: 2 })) out.push(key + item)
|
||||||
|
return { key, item, out }
|
||||||
|
`),
|
||||||
|
).toEqual({ key: "b", item: 2, out: ["a1", "b2"] })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("assigns object patterns and defaults", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let id
|
||||||
|
let label
|
||||||
|
const labels = []
|
||||||
|
for ({ id, label = "unknown" } of [{ id: 1 }, { id: 2, label: "two" }]) labels.push(label)
|
||||||
|
return { id, label, labels }
|
||||||
|
`),
|
||||||
|
).toEqual({ id: 2, label: "two", labels: ["unknown", "two"] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("sequence expressions", () => {
|
||||||
|
test("evaluate left to right and return the final value", async () => {
|
||||||
|
expect(await value(`let x = 0; const result = (x += 1, x *= 3, x + 2); return { x, result }`)).toEqual({
|
||||||
|
x: 3,
|
||||||
|
result: 5,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("support comma-separated for-loop updates", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const pairs = []
|
||||||
|
for (let left = 0, right = 3; left < right; left++, right--) pairs.push([left, right])
|
||||||
|
return pairs
|
||||||
|
`),
|
||||||
|
).toEqual([
|
||||||
|
[0, 3],
|
||||||
|
[1, 2],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("destructuring assignment", () => {
|
describe("destructuring assignment", () => {
|
||||||
test("assigns object and array patterns to existing bindings", async () => {
|
test("assigns object and array patterns to existing bindings", async () => {
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,14 @@ const failingTool = Tool.make({
|
||||||
run: () => Effect.fail(toolError("Lookup refused")),
|
run: () => Effect.fail(toolError("Lookup refused")),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const completedTool = (trace: Trace) =>
|
||||||
|
Tool.make({
|
||||||
|
description: "Return the number of completed sleepy calls",
|
||||||
|
input: Schema.Struct({}),
|
||||||
|
output: Schema.Number,
|
||||||
|
run: () => Effect.succeed(trace.completed),
|
||||||
|
})
|
||||||
|
|
||||||
const run = (
|
const run = (
|
||||||
code: string,
|
code: string,
|
||||||
options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {},
|
options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {},
|
||||||
|
|
@ -55,7 +63,7 @@ const run = (
|
||||||
const trace = options.trace ?? makeTrace()
|
const trace = options.trace ?? makeTrace()
|
||||||
return Effect.runPromise(
|
return Effect.runPromise(
|
||||||
CodeMode.execute({
|
CodeMode.execute({
|
||||||
tools: { host: { sleepy: sleepyTool(trace), fail: failingTool } },
|
tools: { host: { sleepy: sleepyTool(trace), fail: failingTool, completed: completedTool(trace) } },
|
||||||
code,
|
code,
|
||||||
...(options.limits ? { limits: options.limits } : {}),
|
...(options.limits ? { limits: options.limits } : {}),
|
||||||
}),
|
}),
|
||||||
|
|
@ -75,6 +83,42 @@ const error = async (code: string, options: { trace?: Trace; limits?: CodeMode.E
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("first-class promise values", () => {
|
describe("first-class promise values", () => {
|
||||||
|
test("async functions return promises with isolated concurrent invocations", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const load = async (id) => {
|
||||||
|
const result = await tools.host.sleepy({ id, ms: 20 })
|
||||||
|
return [id, result]
|
||||||
|
}
|
||||||
|
const first = load(1)
|
||||||
|
const second = load(2)
|
||||||
|
return [first instanceof Promise, second instanceof Promise, await Promise.all([first, second])]
|
||||||
|
`),
|
||||||
|
).toEqual([
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
[
|
||||||
|
[1, 1],
|
||||||
|
[2, 2],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("async function errors reject instead of throwing at the call site", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const fail = async () => { throw new Error("boom") }
|
||||||
|
const promise = fail()
|
||||||
|
try {
|
||||||
|
await promise
|
||||||
|
return "no"
|
||||||
|
} catch (error) {
|
||||||
|
return error.message
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toBe("boom")
|
||||||
|
})
|
||||||
|
|
||||||
test("an un-awaited tool call starts eagerly, in call order, before any await", async () => {
|
test("an un-awaited tool call starts eagerly, in call order, before any await", async () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await value(
|
const result = await value(
|
||||||
|
|
@ -163,9 +207,33 @@ describe("first-class promise values", () => {
|
||||||
return "done"
|
return "done"
|
||||||
`)
|
`)
|
||||||
expect(diagnostic.kind).toBe("ToolFailure")
|
expect(diagnostic.kind).toBe("ToolFailure")
|
||||||
expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited tool call")
|
expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited promise")
|
||||||
|
expect(diagnostic.message).toContain("Lookup refused")
|
||||||
|
expect(diagnostic.suggestions?.join(" ")).toContain("Await promises")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("a never-awaited failing async function surfaces as an unhandled promise rejection", async () => {
|
||||||
|
const diagnostic = await error(`
|
||||||
|
const fail = async () => { throw new Error("boom") }
|
||||||
|
fail()
|
||||||
|
return "done"
|
||||||
|
`)
|
||||||
|
expect(diagnostic.kind).toBe("ExecutionFailure")
|
||||||
|
expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited promise")
|
||||||
|
expect(diagnostic.message).toContain("boom")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("drains promises started by an async function after an await", async () => {
|
||||||
|
const diagnostic = await error(`
|
||||||
|
const run = async () => {
|
||||||
|
await tools.host.sleepy({ id: 1 })
|
||||||
|
tools.host.fail({})
|
||||||
|
}
|
||||||
|
run()
|
||||||
|
return "done"
|
||||||
|
`)
|
||||||
|
expect(diagnostic.kind).toBe("ToolFailure")
|
||||||
expect(diagnostic.message).toContain("Lookup refused")
|
expect(diagnostic.message).toContain("Lookup refused")
|
||||||
expect(diagnostic.suggestions?.join(" ")).toContain("await tools.ns.tool(...)")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -177,6 +245,12 @@ describe("promises at data boundaries", () => {
|
||||||
expect(diagnostic.message).toContain("await tools.ns.tool(...)")
|
expect(diagnostic.message).toContain("await tools.ns.tool(...)")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("collection helpers do not let un-awaited promises cross the result boundary", async () => {
|
||||||
|
const diagnostic = await error(`return Array.from([Promise.resolve(1)])`)
|
||||||
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||||
|
expect(diagnostic.message).toContain("un-awaited Promise")
|
||||||
|
})
|
||||||
|
|
||||||
test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => {
|
test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => {
|
||||||
const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`)
|
const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`)
|
||||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||||
|
|
@ -232,6 +306,19 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
expect(trace.maxActive).toBeGreaterThan(1)
|
expect(trace.maxActive).toBeGreaterThan(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("runs async map callbacks concurrently", async () => {
|
||||||
|
const trace = makeTrace()
|
||||||
|
const result = await value(
|
||||||
|
`
|
||||||
|
const ids = [1, 2, 3, 4]
|
||||||
|
return await Promise.all(ids.map(async (id) => await tools.host.sleepy({ id, ms: 40 })))
|
||||||
|
`,
|
||||||
|
{ trace },
|
||||||
|
)
|
||||||
|
expect(result).toEqual([1, 2, 3, 4])
|
||||||
|
expect(trace.maxActive).toBeGreaterThan(1)
|
||||||
|
})
|
||||||
|
|
||||||
test("caps live tool-call concurrency at the fixed internal constant (8)", async () => {
|
test("caps live tool-call concurrency at the fixed internal constant (8)", async () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await value(
|
const result = await value(
|
||||||
|
|
@ -265,6 +352,28 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
).toBe("Lookup refused")
|
).toBe("Lookup refused")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("rejects before an earlier slow promise fulfills", async () => {
|
||||||
|
const trace = makeTrace()
|
||||||
|
expect(
|
||||||
|
await value(
|
||||||
|
`
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
tools.host.sleepy({ id: 1, ms: 100 }),
|
||||||
|
tools.host.fail({}),
|
||||||
|
])
|
||||||
|
return -1
|
||||||
|
} catch {
|
||||||
|
return await tools.host.completed({})
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ trace },
|
||||||
|
),
|
||||||
|
).toBe(0)
|
||||||
|
expect(trace.completed).toBe(1)
|
||||||
|
expect(trace.interrupted).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
test("a non-collection argument is a clear error", async () => {
|
test("a non-collection argument is a clear error", async () => {
|
||||||
const diagnostic = await error(`return await Promise.all(42)`)
|
const diagnostic = await error(`return await Promise.all(42)`)
|
||||||
expect(diagnostic.message).toContain("Promise.all expects an array")
|
expect(diagnostic.message).toContain("Promise.all expects an array")
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,28 @@ const error = async (code: string) => {
|
||||||
return result.error
|
return result.error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("Number and Math", () => {
|
||||||
|
test("Math.random returns a number in [0, 1)", async () => {
|
||||||
|
expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Number exposes native non-finite constants", async () => {
|
||||||
|
expect(
|
||||||
|
await value(
|
||||||
|
`return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`,
|
||||||
|
),
|
||||||
|
).toEqual([true, true, true])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Number valueOf returns its primitive receiver", async () => {
|
||||||
|
expect(await value(`return (42).valueOf()`)).toBe(42)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Number valueOf does not enable boxed numbers", async () => {
|
||||||
|
expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Date", () => {
|
describe("Date", () => {
|
||||||
test("Date.now() returns a number", async () => {
|
test("Date.now() returns a number", async () => {
|
||||||
expect(await value(`return typeof Date.now()`)).toBe("number")
|
expect(await value(`return typeof Date.now()`)).toBe("number")
|
||||||
|
|
@ -586,6 +608,85 @@ describe("Set", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("stdlib integration", () => {
|
describe("stdlib integration", () => {
|
||||||
|
test("Object values and entries accept arrays", async () => {
|
||||||
|
expect(await value(`return [Object.values(["a", "b"]), Object.entries(["a", "b"])]`)).toEqual([
|
||||||
|
["a", "b"],
|
||||||
|
[
|
||||||
|
["0", "a"],
|
||||||
|
["1", "b"],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
expect(await value(`const match = /a/.exec("ba"); return [Object.values(match), Object.entries(match)]`)).toEqual([
|
||||||
|
["a", 1],
|
||||||
|
[
|
||||||
|
["0", "a"],
|
||||||
|
["index", 1],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
expect(await value(`return Object.keys(Object.values({ match: /a/.exec("ba") })[0])`)).toEqual(["0", "index"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Object.fromEntries accepts every supported entry collection", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
return [
|
||||||
|
Object.fromEntries([["a", 1]]),
|
||||||
|
Object.fromEntries(new Map([["b", 2]])),
|
||||||
|
Object.fromEntries(new Set([["c", 3]])),
|
||||||
|
Object.fromEntries(new URLSearchParams("d=4")),
|
||||||
|
Object.fromEntries([{ 0: "e", 1: 5 }]),
|
||||||
|
Object.fromEntries(new Set([[{}, 6], [new Date(0), 7], [null, 8], [undefined, 9]])),
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
).toEqual([
|
||||||
|
{ a: 1 },
|
||||||
|
{ b: 2 },
|
||||||
|
{ c: 3 },
|
||||||
|
{ d: "4" },
|
||||||
|
{ e: 5 },
|
||||||
|
{ "[object Object]": 6, "1970-01-01T00:00:00.000Z": 7, null: 8, undefined: 9 },
|
||||||
|
])
|
||||||
|
expect(await value(`try { Object.fromEntries(new Set([Math.max])); return false } catch { return true }`)).toBe(
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
await value(
|
||||||
|
`try { Object.fromEntries(new Map([["fn", Math.max]])); return false } catch { return true }`,
|
||||||
|
),
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("deterministic Math methods match the host runtime", async () => {
|
||||||
|
const result = await value(`
|
||||||
|
return [
|
||||||
|
Math.acos(0.5), Math.acosh(2), Math.asin(0.5), Math.asinh(2), Math.atan(1), Math.atan2(1, 2), Math.atanh(0.5),
|
||||||
|
Math.cos(0.5), Math.cosh(0.5), Math.sin(0.5), Math.sinh(0.5), Math.tan(0.5), Math.tanh(0.5),
|
||||||
|
Math.log1p(0.5), Math.expm1(0.5), Math.f16round(1.337), Math.fround(1.337), Math.clz32(1), Math.imul(2, 3),
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
expect(result).toEqual([
|
||||||
|
Math.acos(0.5),
|
||||||
|
Math.acosh(2),
|
||||||
|
Math.asin(0.5),
|
||||||
|
Math.asinh(2),
|
||||||
|
Math.atan(1),
|
||||||
|
Math.atan2(1, 2),
|
||||||
|
Math.atanh(0.5),
|
||||||
|
Math.cos(0.5),
|
||||||
|
Math.cosh(0.5),
|
||||||
|
Math.sin(0.5),
|
||||||
|
Math.sinh(0.5),
|
||||||
|
Math.tan(0.5),
|
||||||
|
Math.tanh(0.5),
|
||||||
|
Math.log1p(0.5),
|
||||||
|
Math.expm1(0.5),
|
||||||
|
Math.f16round(1.337),
|
||||||
|
Math.fround(1.337),
|
||||||
|
Math.clz32(1),
|
||||||
|
Math.imul(2, 3),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
test("Object.assign mutates and returns its target", async () => {
|
test("Object.assign mutates and returns its target", async () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
|
|
@ -672,6 +773,43 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Object.values/entries preserve nested object identity", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const child = { selected: false }
|
||||||
|
const rows = { a: child }
|
||||||
|
Object.values(rows)[0].selected = true
|
||||||
|
return child.selected
|
||||||
|
`),
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const child = { selected: false }
|
||||||
|
const rows = { a: child }
|
||||||
|
Object.entries(rows)[0][1].selected = true
|
||||||
|
return child.selected
|
||||||
|
`),
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Object enumeration preserves promises and callable references", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const pending = Promise.resolve(1)
|
||||||
|
const source = { pending }
|
||||||
|
return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]]
|
||||||
|
`),
|
||||||
|
).toEqual([["pending"], true, 1, 1])
|
||||||
|
expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => {
|
||||||
|
const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`)
|
||||||
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||||
|
expect(diagnostic.message).toContain("await")
|
||||||
|
expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue")
|
||||||
|
})
|
||||||
|
|
||||||
test("Object.assign keeps Maps usable", async () => {
|
test("Object.assign keeps Maps usable", async () => {
|
||||||
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
|
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
|
||||||
1,
|
1,
|
||||||
|
|
@ -694,6 +832,53 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
|
||||||
expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
|
expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Array.from and Array.of preserve nested object identity", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const child = { selected: false }
|
||||||
|
Array.from([child])[0].selected = true
|
||||||
|
return child.selected
|
||||||
|
`),
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const child = { selected: false }
|
||||||
|
Array.of(child)[0].selected = true
|
||||||
|
return child.selected
|
||||||
|
`),
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Array.from and Array.of preserve promises and callable references", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const pending = Promise.resolve(1)
|
||||||
|
return [await Array.from([pending])[0], await Array.of(pending)[0]]
|
||||||
|
`),
|
||||||
|
).toEqual([1, 1])
|
||||||
|
expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Array.from preserves identity across supported collection shapes", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const child = { selected: false }
|
||||||
|
const fromArrayLike = Array.from({ 0: child, length: 1 })
|
||||||
|
const fromMap = Array.from(new Map([["child", child]]))
|
||||||
|
const fromSet = Array.from(new Set([child]))
|
||||||
|
fromArrayLike[0].selected = true
|
||||||
|
return [fromMap[0][1] === child, fromSet[0] === child, child.selected]
|
||||||
|
`),
|
||||||
|
).toEqual([true, true, true])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Array.from rejects invalid receivers and gives promises an await hint", async () => {
|
||||||
|
const diagnostic = await error(`return Array.from(Promise.resolve([1]))`)
|
||||||
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||||
|
expect(diagnostic.message).toContain("await")
|
||||||
|
expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue")
|
||||||
|
})
|
||||||
|
|
||||||
test("regexes stay callable through Object.values", async () => {
|
test("regexes stay callable through Object.values", async () => {
|
||||||
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
|
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "992b24b9-f3e9-41f5-87a5-4917d1423169",
|
"id": "b0355fd9-bf41-42e3-9dca-76107de27ecd",
|
||||||
"prevIds": [
|
"prevIds": ["95328a41-789d-44de-9643-6ac6ecd6b4ec"],
|
||||||
"96e9fe64-660f-4a73-9414-b38bb7eac290"
|
|
||||||
],
|
|
||||||
"ddl": [
|
"ddl": [
|
||||||
{
|
{
|
||||||
"name": "workspace",
|
"name": "workspace",
|
||||||
|
|
@ -1012,13 +1010,23 @@
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"default": null,
|
"default": null,
|
||||||
"generated": null,
|
"generated": null,
|
||||||
|
"name": "type",
|
||||||
|
"entityType": "columns",
|
||||||
|
"table": "session_input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": null,
|
||||||
|
"generated": null,
|
||||||
"name": "prompt",
|
"name": "prompt",
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session_input"
|
"table": "session_input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"notNull": true,
|
"notNull": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"default": null,
|
"default": null,
|
||||||
"generated": null,
|
"generated": null,
|
||||||
|
|
@ -1166,6 +1174,26 @@
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "session"
|
"table": "session"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": null,
|
||||||
|
"generated": null,
|
||||||
|
"name": "fork_session_id",
|
||||||
|
"entityType": "columns",
|
||||||
|
"table": "session"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": null,
|
||||||
|
"generated": null,
|
||||||
|
"name": "fork_message_id",
|
||||||
|
"entityType": "columns",
|
||||||
|
"table": "session"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
|
|
@ -1547,13 +1575,9 @@
|
||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1562,13 +1586,9 @@
|
||||||
"table": "workspace"
|
"table": "workspace"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["active_account_id"],
|
||||||
"active_account_id"
|
|
||||||
],
|
|
||||||
"tableTo": "account",
|
"tableTo": "account",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "SET NULL",
|
"onDelete": "SET NULL",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1577,13 +1597,9 @@
|
||||||
"table": "account_state"
|
"table": "account_state"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"tableTo": "event_sequence",
|
"tableTo": "event_sequence",
|
||||||
"columnsTo": [
|
"columnsTo": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1592,13 +1608,9 @@
|
||||||
"table": "event"
|
"table": "event"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1607,13 +1619,9 @@
|
||||||
"table": "permission"
|
"table": "permission"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1622,13 +1630,9 @@
|
||||||
"table": "project_directory"
|
"table": "project_directory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1637,13 +1641,9 @@
|
||||||
"table": "instruction_checkpoint"
|
"table": "instruction_checkpoint"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1652,13 +1652,9 @@
|
||||||
"table": "instruction_entry"
|
"table": "instruction_entry"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1667,13 +1663,9 @@
|
||||||
"table": "message"
|
"table": "message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["message_id"],
|
||||||
"message_id"
|
|
||||||
],
|
|
||||||
"tableTo": "message",
|
"tableTo": "message",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1682,13 +1674,9 @@
|
||||||
"table": "part"
|
"table": "part"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1697,13 +1685,9 @@
|
||||||
"table": "session_input"
|
"table": "session_input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1712,13 +1696,9 @@
|
||||||
"table": "session_message"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1727,13 +1707,9 @@
|
||||||
"table": "session"
|
"table": "session"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1742,13 +1718,9 @@
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
|
|
@ -1757,184 +1729,140 @@
|
||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["email", "url"],
|
||||||
"email",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "control_account_pk",
|
"name": "control_account_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "control_account"
|
"table": "control_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id", "directory"],
|
||||||
"project_id",
|
|
||||||
"directory"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_directory_pk",
|
"name": "project_directory_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "project_directory"
|
"table": "project_directory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id", "key"],
|
||||||
"session_id",
|
|
||||||
"key"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "instruction_entry_pk",
|
"name": "instruction_entry_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "instruction_entry"
|
"table": "instruction_entry"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id", "position"],
|
||||||
"session_id",
|
|
||||||
"position"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "todo_pk",
|
"name": "todo_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "workspace_pk",
|
"name": "workspace_pk",
|
||||||
"table": "workspace",
|
"table": "workspace",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["name"],
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "data_migration_pk",
|
"name": "data_migration_pk",
|
||||||
"table": "data_migration",
|
"table": "data_migration",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_state_pk",
|
"name": "account_state_pk",
|
||||||
"table": "account_state",
|
"table": "account_state",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_pk",
|
"name": "account_pk",
|
||||||
"table": "account",
|
"table": "account",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "credential_pk",
|
"name": "credential_pk",
|
||||||
"table": "credential",
|
"table": "credential",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_sequence_pk",
|
"name": "event_sequence_pk",
|
||||||
"table": "event_sequence",
|
"table": "event_sequence",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_pk",
|
"name": "event_pk",
|
||||||
"table": "event",
|
"table": "event",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "permission_pk",
|
"name": "permission_pk",
|
||||||
"table": "permission",
|
"table": "permission",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_pk",
|
"name": "project_pk",
|
||||||
"table": "project",
|
"table": "project",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "instruction_checkpoint_pk",
|
"name": "instruction_checkpoint_pk",
|
||||||
"table": "instruction_checkpoint",
|
"table": "instruction_checkpoint",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "message_pk",
|
"name": "message_pk",
|
||||||
"table": "message",
|
"table": "message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "part_pk",
|
"name": "part_pk",
|
||||||
"table": "part",
|
"table": "part",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_input_pk",
|
"name": "session_input_pk",
|
||||||
"table": "session_input",
|
"table": "session_input",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_message_pk",
|
"name": "session_message_pk",
|
||||||
"table": "session_message",
|
"table": "session_message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_pk",
|
"name": "session_pk",
|
||||||
"table": "session",
|
"table": "session",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_share_pk",
|
"name": "session_share_pk",
|
||||||
"table": "session_share",
|
"table": "session_share",
|
||||||
|
|
@ -2066,6 +1994,10 @@
|
||||||
"value": "promoted_seq",
|
"value": "promoted_seq",
|
||||||
"isExpression": false
|
"isExpression": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"value": "type",
|
||||||
|
"isExpression": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"value": "delivery",
|
"value": "delivery",
|
||||||
"isExpression": false
|
"isExpression": false
|
||||||
|
|
@ -2078,7 +2010,21 @@
|
||||||
"isUnique": false,
|
"isUnique": false,
|
||||||
"where": null,
|
"where": null,
|
||||||
"origin": "manual",
|
"origin": "manual",
|
||||||
"name": "session_input_session_pending_delivery_seq_idx",
|
"name": "session_input_session_pending_type_delivery_seq_idx",
|
||||||
|
"entityType": "indexes",
|
||||||
|
"table": "session_input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"value": "session_id",
|
||||||
|
"isExpression": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"where": "\"session_input\".\"type\" = 'compaction' and \"session_input\".\"promoted_seq\" is null",
|
||||||
|
"origin": "manual",
|
||||||
|
"name": "session_input_session_pending_compaction_idx",
|
||||||
"entityType": "indexes",
|
"entityType": "indexes",
|
||||||
"table": "session_input"
|
"table": "session_input"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import { State } from "./state"
|
||||||
|
|
||||||
export const ID = Agent.ID
|
export const ID = Agent.ID
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
|
export const Name = Agent.Name
|
||||||
|
export type Name = Agent.Name
|
||||||
export const defaultID = ID.make("build")
|
export const defaultID = ID.make("build")
|
||||||
|
|
||||||
export const Color = Agent.Color
|
export const Color = Agent.Color
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
JSONValue,
|
JSONValue,
|
||||||
LanguageModelV3,
|
LanguageModelV3,
|
||||||
LanguageModelV3CallOptions,
|
LanguageModelV3CallOptions,
|
||||||
|
LanguageModelV3FinishReason,
|
||||||
LanguageModelV3FunctionTool,
|
LanguageModelV3FunctionTool,
|
||||||
LanguageModelV3Message,
|
LanguageModelV3Message,
|
||||||
LanguageModelV3Prompt,
|
LanguageModelV3Prompt,
|
||||||
|
|
@ -304,6 +305,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
|
||||||
const route: AnyRoute = {
|
const route: AnyRoute = {
|
||||||
id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
|
id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
|
||||||
provider: ProviderID.make(info.providerID),
|
provider: ProviderID.make(info.providerID),
|
||||||
|
providerMetadataKey: optionKey,
|
||||||
protocol: "ai-sdk",
|
protocol: "ai-sdk",
|
||||||
endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
|
endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
|
||||||
auth: Auth.none,
|
auth: Auth.none,
|
||||||
|
|
@ -416,7 +418,7 @@ function assistantPart(part: ContentPart): AssistantContent {
|
||||||
case "media":
|
case "media":
|
||||||
return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
|
return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
|
||||||
case "reasoning":
|
case "reasoning":
|
||||||
return [{ type: "reasoning", text: part.text }]
|
return [{ type: "reasoning", text: part.text, providerOptions: providerOptions(part.providerMetadata) }]
|
||||||
case "tool-call":
|
case "tool-call":
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -425,6 +427,7 @@ function assistantPart(part: ContentPart): AssistantContent {
|
||||||
toolName: part.name,
|
toolName: part.name,
|
||||||
input: part.input,
|
input: part.input,
|
||||||
providerExecuted: part.providerExecuted,
|
providerExecuted: part.providerExecuted,
|
||||||
|
providerOptions: providerOptions(part.providerMetadata),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
case "tool-result":
|
case "tool-result":
|
||||||
|
|
@ -440,6 +443,7 @@ function toolResultPart(part: ContentPart): ToolResultContent[] {
|
||||||
toolCallId: part.id,
|
toolCallId: part.id,
|
||||||
toolName: part.name,
|
toolName: part.name,
|
||||||
output: toolOutput(part.result),
|
output: toolOutput(part.result),
|
||||||
|
providerOptions: providerOptions(part.providerMetadata),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -624,8 +628,8 @@ function usage(input: Extract<LanguageModelV3StreamPart, { type: "finish" }>["us
|
||||||
return Object.values(output).some((value) => value !== undefined) ? output : undefined
|
return Object.values(output).some((value) => value !== undefined) ? output : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishReason(value: unknown): FinishReason {
|
function finishReason(value: LanguageModelV3FinishReason): FinishReason {
|
||||||
return Schema.is(FinishReason)(value) ? value : "unknown"
|
return value.unified === "other" ? "unknown" : value.unified
|
||||||
}
|
}
|
||||||
|
|
||||||
function providerMetadata(value: unknown) {
|
function providerMetadata(value: unknown) {
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,11 @@ export class Timeout extends Schema.Class<Timeout>("ConfigV2.MCP.Timeout")({
|
||||||
startup: PositiveInt.pipe(Schema.optional).annotate({
|
startup: PositiveInt.pipe(Schema.optional).annotate({
|
||||||
description: "Maximum time in milliseconds to establish and initialize the MCP server.",
|
description: "Maximum time in milliseconds to establish and initialize the MCP server.",
|
||||||
}),
|
}),
|
||||||
request: PositiveInt.pipe(Schema.optional).annotate({
|
catalog: PositiveInt.pipe(Schema.optional).annotate({
|
||||||
description: "Maximum time in milliseconds to wait for MCP catalog/list requests after initialization.",
|
description: "Maximum time in milliseconds to wait for MCP discovery requests such as tools/list and prompts/list.",
|
||||||
|
}),
|
||||||
|
execution: PositiveInt.pipe(Schema.optional).annotate({
|
||||||
|
description: "Maximum time in milliseconds to wait for MCP tool and prompt execution.",
|
||||||
}),
|
}),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export * as ConfigProviderPlugin from "./provider"
|
export * as ConfigProviderPlugin from "./provider"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
import { Money } from "@opencode-ai/schema/money"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
|
@ -91,8 +92,8 @@ export const Plugin = define({
|
||||||
input: cost.input,
|
input: cost.input,
|
||||||
output: cost.output,
|
output: cost.output,
|
||||||
cache: {
|
cache: {
|
||||||
read: cost.cache?.read ?? 0,
|
read: cost.cache?.read ?? Money.USDPerMillionTokens.zero,
|
||||||
write: cost.cache?.write ?? 0,
|
write: cost.cache?.write ?? Money.USDPerMillionTokens.zero,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export * as ConfigProvider from "./provider"
|
export * as ConfigProvider from "./provider"
|
||||||
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
|
import { Money } from "@opencode-ai/schema/money"
|
||||||
import { ModelV2 } from "../model"
|
import { ModelV2 } from "../model"
|
||||||
|
|
||||||
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
|
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
|
||||||
|
|
@ -17,8 +18,8 @@ export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")(
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
|
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
|
||||||
read: Schema.Finite.pipe(Schema.optional),
|
read: Money.USDPerMillionTokens.pipe(Schema.optional),
|
||||||
write: Schema.Finite.pipe(Schema.optional),
|
write: Money.USDPerMillionTokens.pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
|
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
|
||||||
|
|
@ -26,8 +27,8 @@ class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
|
||||||
type: Schema.Literal("context"),
|
type: Schema.Literal("context"),
|
||||||
size: Schema.Int,
|
size: Schema.Int,
|
||||||
}).pipe(Schema.optional),
|
}).pipe(Schema.optional),
|
||||||
input: Schema.Finite,
|
input: Money.USDPerMillionTokens,
|
||||||
output: Schema.Finite,
|
output: Money.USDPerMillionTokens,
|
||||||
cache: Cache.pipe(Schema.optional),
|
cache: Cache.pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
|
|
|
||||||
4
packages/core/src/database/migration.gen.ts
generated
4
packages/core/src/database/migration.gen.ts
generated
|
|
@ -44,6 +44,10 @@ export const migrations = (
|
||||||
import("./migration/20260703090000_reset_v2_event_rename_sweep"),
|
import("./migration/20260703090000_reset_v2_event_rename_sweep"),
|
||||||
import("./migration/20260703181610_event_created_column"),
|
import("./migration/20260703181610_event_created_column"),
|
||||||
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
|
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
|
||||||
|
import("./migration/20260703200000_reset_v2_session_events"),
|
||||||
import("./migration/20260705180000_rename_instructions"),
|
import("./migration/20260705180000_rename_instructions"),
|
||||||
|
import("./migration/20260706223930_add-session-fork"),
|
||||||
|
import("./migration/20260707010146_durable_session_inbox"),
|
||||||
|
import("./migration/20260707120000_migrate_prelaunch_v2_state"),
|
||||||
])
|
])
|
||||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260703200000_reset_v2_session_events",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`DELETE FROM \`session_input\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`session_message\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260706223930_add-session-fork",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`ALTER TABLE \`session\` ADD \`fork_session_id\` text;`)
|
||||||
|
yield* tx.run(`ALTER TABLE \`session\` ADD \`fork_message_id\` text;`)
|
||||||
|
yield* tx.run(`
|
||||||
|
UPDATE \`session\`
|
||||||
|
SET
|
||||||
|
\`parent_id\` = NULL,
|
||||||
|
\`fork_session_id\` = (
|
||||||
|
SELECT json_extract(\`event\`.\`data\`, '$.parentID')
|
||||||
|
FROM \`event\`
|
||||||
|
WHERE \`event\`.\`aggregate_id\` = \`session\`.\`id\`
|
||||||
|
AND \`event\`.\`type\` = 'session.forked'
|
||||||
|
ORDER BY \`event\`.\`seq\`
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
\`fork_message_id\` = (
|
||||||
|
SELECT json_extract(\`event\`.\`data\`, '$.from')
|
||||||
|
FROM \`event\`
|
||||||
|
WHERE \`event\`.\`aggregate_id\` = \`session\`.\`id\`
|
||||||
|
AND \`event\`.\`type\` = 'session.forked'
|
||||||
|
ORDER BY \`event\`.\`seq\`
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM \`event\`
|
||||||
|
WHERE \`event\`.\`aggregate_id\` = \`session\`.\`id\`
|
||||||
|
AND \`event\`.\`type\` = 'session.forked'
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260707010146_durable_session_inbox",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`PRAGMA foreign_keys=OFF;`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`__new_session_input\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`session_id\` text NOT NULL,
|
||||||
|
\`type\` text NOT NULL,
|
||||||
|
\`prompt\` text,
|
||||||
|
\`delivery\` text,
|
||||||
|
\`admitted_seq\` integer NOT NULL,
|
||||||
|
\`promoted_seq\` integer,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(
|
||||||
|
`INSERT INTO \`__new_session_input\`(\`id\`, \`session_id\`, \`type\`, \`prompt\`, \`delivery\`, \`admitted_seq\`, \`promoted_seq\`, \`time_created\`) SELECT \`id\`, \`session_id\`, 'prompt', \`prompt\`, \`delivery\`, \`admitted_seq\`, \`promoted_seq\`, \`time_created\` FROM \`session_input\`;`,
|
||||||
|
)
|
||||||
|
yield* tx.run(`DROP TABLE \`session_input\`;`)
|
||||||
|
yield* tx.run(`ALTER TABLE \`__new_session_input\` RENAME TO \`session_input\`;`)
|
||||||
|
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||||
|
yield* tx.run(`DROP INDEX IF EXISTS \`session_input_session_pending_delivery_seq_idx\`;`)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE INDEX \`session_input_session_pending_type_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`type\`,\`delivery\`,\`admitted_seq\`);`,
|
||||||
|
)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE UNIQUE INDEX \`session_input_session_pending_compaction_idx\` ON \`session_input\` (\`session_id\`) WHERE "session_input"."type" = 'compaction' and "session_input"."promoted_seq" is null;`,
|
||||||
|
)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`,
|
||||||
|
)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
import { sql } from "drizzle-orm"
|
||||||
|
import { Effect, Schema } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString)
|
||||||
|
const isObject = Schema.is(Schema.Record(Schema.String, Schema.Unknown))
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260707120000_migrate_prelaunch_v2_state",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(
|
||||||
|
sql`DELETE FROM session_message WHERE type = 'compaction' AND json_extract(data, '$.status') = 'queued'`,
|
||||||
|
)
|
||||||
|
const messages = yield* tx.all<{ id: string; type: string; data: string }>(
|
||||||
|
sql`SELECT id, type, data FROM session_message WHERE type IN ('skill', 'shell', 'assistant', 'compaction', 'synthetic')`,
|
||||||
|
)
|
||||||
|
for (const row of messages) {
|
||||||
|
const data = object(decodeJson(row.data))
|
||||||
|
yield* tx.run(
|
||||||
|
sql`UPDATE session_message SET data = ${JSON.stringify(messageData(row.type, data))} WHERE id = ${row.id}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* tx.run(sql`DELETE FROM event WHERE type = 'session.compaction.delta.1'`)
|
||||||
|
const events = yield* tx.all<{ id: string; aggregateID: string; seq: number; type: string; data: string }>(sql`
|
||||||
|
SELECT id, aggregate_id as aggregateID, seq, type, data
|
||||||
|
FROM event
|
||||||
|
WHERE type IN (
|
||||||
|
'session.skill.activated.1',
|
||||||
|
'session.skill.activated.2',
|
||||||
|
'session.compaction.started.1',
|
||||||
|
'session.compaction.started.2',
|
||||||
|
'session.compaction.ended.1',
|
||||||
|
'session.compaction.failed.1',
|
||||||
|
'session.compaction.failed.2',
|
||||||
|
'session.revert.staged.1',
|
||||||
|
'session.revert.staged.2'
|
||||||
|
)
|
||||||
|
ORDER BY aggregate_id, seq
|
||||||
|
`)
|
||||||
|
const compactionReasons = new Map<string, "auto" | "manual">()
|
||||||
|
for (const row of events) {
|
||||||
|
const data = object(decodeJson(row.data))
|
||||||
|
if (row.type.startsWith("session.compaction.ended.")) {
|
||||||
|
compactionReasons.delete(row.aggregateID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const event = eventData(row.type, data, compactionReasons.get(row.aggregateID))
|
||||||
|
if (row.type.startsWith("session.compaction.started."))
|
||||||
|
compactionReasons.set(row.aggregateID, event.data.reason === "auto" ? "auto" : "manual")
|
||||||
|
if (row.type.startsWith("session.compaction.failed.")) compactionReasons.delete(row.aggregateID)
|
||||||
|
yield* tx.run(
|
||||||
|
sql`UPDATE event SET type = ${event.type}, data = ${JSON.stringify(event.data)} WHERE id = ${row.id}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
|
|
||||||
|
function messageData(type: string, data: Record<string, unknown>) {
|
||||||
|
if (type === "skill")
|
||||||
|
return defined({
|
||||||
|
metadata: data.metadata,
|
||||||
|
time: data.time,
|
||||||
|
skill: data.skill ?? data.id ?? data.name,
|
||||||
|
name: data.name,
|
||||||
|
text: data.text,
|
||||||
|
})
|
||||||
|
if (type === "shell") {
|
||||||
|
const shell = object(data.shell)
|
||||||
|
return defined({
|
||||||
|
metadata: data.metadata,
|
||||||
|
time: data.time,
|
||||||
|
shellID: data.shellID ?? shell.id,
|
||||||
|
command: data.command ?? shell.command,
|
||||||
|
status: data.status ?? shell.status,
|
||||||
|
exit: data.exit ?? shell.exit,
|
||||||
|
output: data.output,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (type === "assistant")
|
||||||
|
return defined({
|
||||||
|
metadata: data.metadata,
|
||||||
|
time: data.time,
|
||||||
|
agent: data.agent,
|
||||||
|
model: data.model,
|
||||||
|
content: Array.isArray(data.content) ? data.content.map(assistantContent) : data.content,
|
||||||
|
snapshot: data.snapshot,
|
||||||
|
finish: data.finish,
|
||||||
|
cost: data.cost,
|
||||||
|
tokens: data.tokens,
|
||||||
|
error: data.error,
|
||||||
|
retry: data.retry,
|
||||||
|
})
|
||||||
|
if (type === "compaction") {
|
||||||
|
if (data.status === "failed")
|
||||||
|
return defined({
|
||||||
|
metadata: data.metadata,
|
||||||
|
time: data.time,
|
||||||
|
status: data.status,
|
||||||
|
reason: data.reason,
|
||||||
|
error: data.error ?? genericCompactionError,
|
||||||
|
})
|
||||||
|
return defined({
|
||||||
|
metadata: data.metadata,
|
||||||
|
time: data.time,
|
||||||
|
status: data.status,
|
||||||
|
reason: data.reason,
|
||||||
|
summary: data.summary,
|
||||||
|
recent: data.recent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (type === "synthetic")
|
||||||
|
return defined({ metadata: data.metadata, time: data.time, text: data.text, description: data.description })
|
||||||
|
const { sessionID: _, ...current } = data
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
function assistantContent(value: unknown) {
|
||||||
|
const content = object(value)
|
||||||
|
if (content.type === "text") return defined({ type: content.type, text: content.text })
|
||||||
|
if (content.type === "reasoning")
|
||||||
|
return defined({ type: content.type, text: content.text, state: content.state, time: content.time })
|
||||||
|
if (content.type !== "tool") return content
|
||||||
|
return defined({
|
||||||
|
type: content.type,
|
||||||
|
id: content.id,
|
||||||
|
name: content.name,
|
||||||
|
executed: content.executed,
|
||||||
|
providerState: content.providerState,
|
||||||
|
providerResultState: content.providerResultState,
|
||||||
|
state: toolState(content.state),
|
||||||
|
time: content.time,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolState(value: unknown) {
|
||||||
|
const state = object(value)
|
||||||
|
if (state.status === "pending" || state.status === "streaming")
|
||||||
|
return defined({ status: "streaming", input: state.input })
|
||||||
|
if (state.status === "running")
|
||||||
|
return defined({ status: state.status, input: state.input, structured: state.structured, content: state.content })
|
||||||
|
if (state.status === "completed")
|
||||||
|
return defined({
|
||||||
|
status: state.status,
|
||||||
|
input: state.input,
|
||||||
|
structured: state.structured,
|
||||||
|
content: state.content,
|
||||||
|
result: state.result,
|
||||||
|
})
|
||||||
|
if (state.status === "error")
|
||||||
|
return defined({
|
||||||
|
status: state.status,
|
||||||
|
input: state.input,
|
||||||
|
structured: state.structured,
|
||||||
|
content: state.content,
|
||||||
|
error: state.error,
|
||||||
|
result: state.result,
|
||||||
|
})
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventData(type: string, data: Record<string, unknown>, compactionReason?: "auto" | "manual") {
|
||||||
|
if (type.startsWith("session.skill.activated."))
|
||||||
|
return {
|
||||||
|
type: "session.skill.activated.1",
|
||||||
|
data: defined({ sessionID: data.sessionID, id: data.id ?? data.name, name: data.name, text: data.text }),
|
||||||
|
}
|
||||||
|
if (type.startsWith("session.compaction.started."))
|
||||||
|
return {
|
||||||
|
type: "session.compaction.started.1",
|
||||||
|
data: defined({
|
||||||
|
sessionID: data.sessionID,
|
||||||
|
reason: data.reason,
|
||||||
|
recent: data.recent ?? "",
|
||||||
|
inputID: data.inputID,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
if (type.startsWith("session.compaction.failed."))
|
||||||
|
return {
|
||||||
|
type: "session.compaction.failed.1",
|
||||||
|
data: defined({
|
||||||
|
sessionID: data.sessionID,
|
||||||
|
reason: data.reason ?? compactionReason ?? "manual",
|
||||||
|
error: data.error ?? genericCompactionError,
|
||||||
|
inputID: data.inputID,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
const revert = object(data.revert)
|
||||||
|
return {
|
||||||
|
type: "session.revert.staged.1",
|
||||||
|
data: defined({
|
||||||
|
sessionID: data.sessionID,
|
||||||
|
revert: defined({
|
||||||
|
messageID: revert.messageID,
|
||||||
|
partID: revert.partID,
|
||||||
|
snapshot: revert.snapshot,
|
||||||
|
files: Array.isArray(revert.files)
|
||||||
|
? revert.files.map((value) => {
|
||||||
|
const file = object(value)
|
||||||
|
return defined({
|
||||||
|
file: file.file ?? file.path,
|
||||||
|
patch: file.patch,
|
||||||
|
additions: file.additions,
|
||||||
|
deletions: file.deletions,
|
||||||
|
status: file.status,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const genericCompactionError = {
|
||||||
|
type: "compaction.failed",
|
||||||
|
message: "Compaction failed before recording an error",
|
||||||
|
}
|
||||||
|
|
||||||
|
function object(value: unknown): Record<string, unknown> {
|
||||||
|
return isObject(value) ? value : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function defined(value: Record<string, unknown>) {
|
||||||
|
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== undefined))
|
||||||
|
}
|
||||||
|
|
@ -170,8 +170,9 @@ export default {
|
||||||
CREATE TABLE \`session_input\` (
|
CREATE TABLE \`session_input\` (
|
||||||
\`id\` text PRIMARY KEY,
|
\`id\` text PRIMARY KEY,
|
||||||
\`session_id\` text NOT NULL,
|
\`session_id\` text NOT NULL,
|
||||||
\`prompt\` text NOT NULL,
|
\`type\` text NOT NULL,
|
||||||
\`delivery\` text NOT NULL,
|
\`prompt\` text,
|
||||||
|
\`delivery\` text,
|
||||||
\`admitted_seq\` integer NOT NULL,
|
\`admitted_seq\` integer NOT NULL,
|
||||||
\`promoted_seq\` integer,
|
\`promoted_seq\` integer,
|
||||||
\`time_created\` integer NOT NULL,
|
\`time_created\` integer NOT NULL,
|
||||||
|
|
@ -196,6 +197,8 @@ export default {
|
||||||
\`project_id\` text NOT NULL,
|
\`project_id\` text NOT NULL,
|
||||||
\`workspace_id\` text,
|
\`workspace_id\` text,
|
||||||
\`parent_id\` text,
|
\`parent_id\` text,
|
||||||
|
\`fork_session_id\` text,
|
||||||
|
\`fork_message_id\` text,
|
||||||
\`slug\` text NOT NULL,
|
\`slug\` text NOT NULL,
|
||||||
\`directory\` text NOT NULL,
|
\`directory\` text NOT NULL,
|
||||||
\`path\` text,
|
\`path\` text,
|
||||||
|
|
@ -259,7 +262,10 @@ export default {
|
||||||
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||||
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
||||||
yield* tx.run(
|
yield* tx.run(
|
||||||
`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`,
|
`CREATE INDEX \`session_input_session_pending_type_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`type\`,\`delivery\`,\`admitted_seq\`);`,
|
||||||
|
)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE UNIQUE INDEX \`session_input_session_pending_compaction_idx\` ON \`session_input\` (\`session_id\`) WHERE "session_input"."type" = 'compaction' and "session_input"."promoted_seq" is null;`,
|
||||||
)
|
)
|
||||||
yield* tx.run(
|
yield* tx.run(
|
||||||
`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`,
|
`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`,
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,6 @@ export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.no
|
||||||
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
|
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
|
||||||
// TODO: Add snapshots / undo after V2 snapshot design exists.
|
// TODO: Add snapshots / undo after V2 snapshot design exists.
|
||||||
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
|
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
|
||||||
// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits.
|
// TODO: Design multi-file transactions / rollback if patch needs atomic edits.
|
||||||
// Until then, edits are sequential and report partial application.
|
// Until then, edits are sequential and report partial application.
|
||||||
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
|
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * as File from "./file"
|
export * as File from "./file"
|
||||||
|
|
||||||
import { Revert } from "@opencode-ai/schema/revert"
|
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||||
|
|
||||||
export const Diff = Revert.FileDiff
|
export const Diff = FileDiff.Info
|
||||||
export type Diff = typeof Diff.Type
|
export type Diff = typeof Diff.Type
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const layer = Layer.effect(
|
||||||
.flatMap((item) => item.info.watcher?.ignore ?? [])
|
.flatMap((item) => item.info.watcher?.ignore ?? [])
|
||||||
const home = path.resolve(location.directory) === path.resolve(os.homedir())
|
const home = path.resolve(location.directory) === path.resolve(os.homedir())
|
||||||
|
|
||||||
if (!home) {
|
if (!home && location.vcs) {
|
||||||
yield* watcher
|
yield* watcher
|
||||||
.subscribe({
|
.subscribe({
|
||||||
path: location.directory,
|
path: location.directory,
|
||||||
|
|
|
||||||
|
|
@ -606,7 +606,7 @@ const layer = Layer.effect(
|
||||||
file,
|
file,
|
||||||
])).text
|
])).text
|
||||||
return {
|
return {
|
||||||
path: file,
|
file,
|
||||||
status,
|
status,
|
||||||
additions: binary ? 0 : Number(stats[0] ?? 0),
|
additions: binary ? 0 : Number(stats[0] ?? 0),
|
||||||
deletions: binary ? 0 : Number(stats[1] ?? 0),
|
deletions: binary ? 0 : Number(stats[1] ?? 0),
|
||||||
|
|
|
||||||
|
|
@ -5,29 +5,22 @@ import { Catalog } from "./catalog"
|
||||||
import { CommandV2 } from "./command"
|
import { CommandV2 } from "./command"
|
||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
import { LayerNode } from "./effect/layer-node"
|
import { LayerNode } from "./effect/layer-node"
|
||||||
import { makeLocationNode, Node } from "./effect/app-node"
|
import { Node } from "./effect/app-node"
|
||||||
import { httpClient } from "./effect/app-node-platform"
|
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { FileMutation } from "./file-mutation"
|
import { FileMutation } from "./file-mutation"
|
||||||
import { FileSystem } from "./filesystem"
|
import { FileSystem } from "./filesystem"
|
||||||
import { FileSystemSearch } from "./filesystem/search"
|
import { FileSystemSearch } from "./filesystem/search"
|
||||||
import { FSUtil } from "./fs-util"
|
|
||||||
import { Generate } from "./generate"
|
import { Generate } from "./generate"
|
||||||
import { Form } from "./form"
|
import { Form } from "./form"
|
||||||
import { Global } from "./global"
|
|
||||||
import { LocationWatcher } from "./filesystem/location-watcher"
|
|
||||||
import { Image } from "./image"
|
import { Image } from "./image"
|
||||||
|
import { LocationWatcher } from "./filesystem/location-watcher"
|
||||||
import { Integration } from "./integration"
|
import { Integration } from "./integration"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { LocationMutation } from "./location-mutation"
|
import { LocationMutation } from "./location-mutation"
|
||||||
import { LocationServiceMap } from "./location-service-map"
|
import { LocationServiceMap } from "./location-service-map"
|
||||||
import { MCP } from "./mcp/index"
|
import { MCP } from "./mcp/index"
|
||||||
import { ModelsDev } from "./models-dev"
|
|
||||||
import { Npm } from "./npm"
|
|
||||||
import { PermissionV2 } from "./permission"
|
import { PermissionV2 } from "./permission"
|
||||||
import { PluginV2 } from "./plugin"
|
import { PluginV2 } from "./plugin"
|
||||||
import { PluginRuntime } from "./plugin/runtime"
|
|
||||||
import { SdkPlugins } from "./plugin/sdk"
|
|
||||||
import { PluginSupervisor } from "./plugin/supervisor"
|
import { PluginSupervisor } from "./plugin/supervisor"
|
||||||
import { ProjectCopy } from "./project/copy"
|
import { ProjectCopy } from "./project/copy"
|
||||||
import { Pty } from "./pty"
|
import { Pty } from "./pty"
|
||||||
|
|
@ -35,7 +28,6 @@ import { QuestionV2 } from "./question"
|
||||||
import { Shell } from "./shell"
|
import { Shell } from "./shell"
|
||||||
import { Reference } from "./reference"
|
import { Reference } from "./reference"
|
||||||
import { ReferenceGuidance } from "./reference/guidance"
|
import { ReferenceGuidance } from "./reference/guidance"
|
||||||
import { Ripgrep } from "./ripgrep"
|
|
||||||
import { SessionRunnerLLM } from "./session/runner/llm"
|
import { SessionRunnerLLM } from "./session/runner/llm"
|
||||||
import { SessionRunnerModel } from "./session/runner/model"
|
import { SessionRunnerModel } from "./session/runner/model"
|
||||||
import { SessionCompaction } from "./session/compaction"
|
import { SessionCompaction } from "./session/compaction"
|
||||||
|
|
@ -51,49 +43,11 @@ import { SessionInstructions } from "./session/instructions"
|
||||||
import { McpTool } from "./tool/mcp"
|
import { McpTool } from "./tool/mcp"
|
||||||
import { ReadToolFileSystem } from "./tool/read-filesystem"
|
import { ReadToolFileSystem } from "./tool/read-filesystem"
|
||||||
import { ToolRegistry } from "./tool/registry"
|
import { ToolRegistry } from "./tool/registry"
|
||||||
import { WebSearchTool } from "./tool/websearch"
|
|
||||||
import { ToolOutputStore } from "./tool-output-store"
|
import { ToolOutputStore } from "./tool-output-store"
|
||||||
import { Vcs } from "./vcs"
|
import { Vcs } from "./vcs"
|
||||||
|
|
||||||
export { LocationServiceMap } from "./location-service-map"
|
export { LocationServiceMap } from "./location-service-map"
|
||||||
|
|
||||||
const pluginSupervisorNode = makeLocationNode({
|
|
||||||
service: PluginSupervisor.Service,
|
|
||||||
layer: PluginSupervisor.layer,
|
|
||||||
deps: [
|
|
||||||
PluginV2.node,
|
|
||||||
SdkPlugins.node,
|
|
||||||
AgentV2.node,
|
|
||||||
Catalog.node,
|
|
||||||
CommandV2.node,
|
|
||||||
Config.node,
|
|
||||||
EventV2.node,
|
|
||||||
FileMutation.node,
|
|
||||||
FileSystem.node,
|
|
||||||
FSUtil.node,
|
|
||||||
Global.node,
|
|
||||||
httpClient,
|
|
||||||
Image.node,
|
|
||||||
Integration.node,
|
|
||||||
Location.node,
|
|
||||||
LocationMutation.node,
|
|
||||||
ModelsDev.node,
|
|
||||||
Npm.node,
|
|
||||||
PermissionV2.node,
|
|
||||||
PluginRuntime.node,
|
|
||||||
Form.node,
|
|
||||||
ReadToolFileSystem.node,
|
|
||||||
Reference.node,
|
|
||||||
Ripgrep.node,
|
|
||||||
SessionInstructions.node,
|
|
||||||
SessionTodo.node,
|
|
||||||
Shell.node,
|
|
||||||
SkillV2.node,
|
|
||||||
ToolRegistry.toolsNode,
|
|
||||||
WebSearchTool.configNode,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const locationServiceNodes = [
|
const locationServiceNodes = [
|
||||||
Location.node,
|
Location.node,
|
||||||
Config.node,
|
Config.node,
|
||||||
|
|
@ -104,7 +58,7 @@ const locationServiceNodes = [
|
||||||
Catalog.node,
|
Catalog.node,
|
||||||
AISDK.node,
|
AISDK.node,
|
||||||
PluginV2.node,
|
PluginV2.node,
|
||||||
pluginSupervisorNode,
|
PluginSupervisor.node,
|
||||||
ProjectCopy.node,
|
ProjectCopy.node,
|
||||||
ProjectCopy.refreshNode,
|
ProjectCopy.refreshNode,
|
||||||
FileSystemSearch.node,
|
FileSystemSearch.node,
|
||||||
|
|
@ -150,31 +104,44 @@ export type LocationError = LayerNode.Error<typeof locationServices>
|
||||||
export function buildLocationServiceMap(
|
export function buildLocationServiceMap(
|
||||||
replacements: LayerNode.Replacements = [],
|
replacements: LayerNode.Replacements = [],
|
||||||
): Layer.Layer<LocationServiceMap.Service> {
|
): Layer.Layer<LocationServiceMap.Service> {
|
||||||
|
// Structural Equal is own-key-set sensitive, so `{ directory }` (schema-decoded
|
||||||
|
// payloads omit optional keys) and `{ directory, workspaceID: undefined }` are
|
||||||
|
// different RcMap keys. The RcMap caches by the raw key before the build
|
||||||
|
// callback runs, so canonicalize at the map boundary to the key-present shape.
|
||||||
|
const canonical = (ref: Location.Ref) => Location.Ref.make({ directory: ref.directory, workspaceID: ref.workspaceID })
|
||||||
return Layer.effect(
|
return Layer.effect(
|
||||||
LocationServiceMap.Service,
|
LocationServiceMap.Service,
|
||||||
LayerMap.make(
|
Effect.map(
|
||||||
(ref: Location.Ref) => {
|
LayerMap.make(
|
||||||
const startedAt = performance.now()
|
(ref: Location.Ref) => {
|
||||||
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
const startedAt = performance.now()
|
||||||
// Apply replacements during hoist, not afterward: replacements can
|
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
||||||
// introduce new tagged dependencies (Location.boundNode depends on
|
// Apply replacements during hoist, not afterward: replacements can
|
||||||
// Project), and the hoist walk is the only pass that can still slice
|
// introduce new tagged dependencies (Location.boundNode depends on
|
||||||
// those back out.
|
// Project), and the hoist walk is the only pass that can still slice
|
||||||
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
|
// those back out.
|
||||||
|
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
|
||||||
|
|
||||||
return LayerNode.compile(location.node).pipe(
|
return LayerNode.compile(location.node).pipe(
|
||||||
Layer.fresh,
|
Layer.fresh,
|
||||||
Layer.tap(() =>
|
Layer.tap(() =>
|
||||||
Effect.logInfo("location services booted", {
|
Effect.logInfo("location services booted", {
|
||||||
directory: ref.directory,
|
directory: ref.directory,
|
||||||
workspaceID: ref.workspaceID,
|
workspaceID: ref.workspaceID,
|
||||||
durationMs: Math.round(performance.now() - startedAt),
|
durationMs: Math.round(performance.now() - startedAt),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Layer.provide(LayerNode.compile(location.hoisted)),
|
Layer.provide(LayerNode.compile(location.hoisted)),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ idleTimeToLive: "60 minutes" },
|
{ idleTimeToLive: "60 minutes" },
|
||||||
|
),
|
||||||
|
(inner) => ({
|
||||||
|
...inner,
|
||||||
|
get: (ref: Location.Ref) => inner.get(canonical(ref)),
|
||||||
|
contextEffect: (ref: Location.Ref) => inner.contextEffect(canonical(ref)),
|
||||||
|
invalidate: (ref: Location.Ref) => inner.invalidate(canonical(ref)),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ export * as MCPClient from "./client"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { execFile } from "node:child_process"
|
import { execFile } from "node:child_process"
|
||||||
import { pathToFileURL } from "node:url"
|
import { pathToFileURL } from "node:url"
|
||||||
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||||
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
ListToolsResultSchema,
|
ListToolsResultSchema,
|
||||||
PromptListChangedNotificationSchema,
|
PromptListChangedNotificationSchema,
|
||||||
PromptSchema,
|
PromptSchema,
|
||||||
|
ResourceListChangedNotificationSchema,
|
||||||
type LoggingMessageNotification,
|
type LoggingMessageNotification,
|
||||||
LoggingMessageNotificationSchema,
|
LoggingMessageNotificationSchema,
|
||||||
ToolListChangedNotificationSchema,
|
ToolListChangedNotificationSchema,
|
||||||
|
|
@ -31,7 +32,8 @@ import { ConfigMCP } from "../config/mcp"
|
||||||
import { InstallationVersion } from "../installation/version"
|
import { InstallationVersion } from "../installation/version"
|
||||||
|
|
||||||
const DEFAULT_STARTUP_TIMEOUT = 30_000
|
const DEFAULT_STARTUP_TIMEOUT = 30_000
|
||||||
const DEFAULT_REQUEST_TIMEOUT = 30_000
|
const DEFAULT_CATALOG_TIMEOUT = 30_000
|
||||||
|
const DEFAULT_EXECUTION_TIMEOUT = 12 * 60 * 60 * 1_000 // 12 hours
|
||||||
|
|
||||||
type Transport = StdioClientTransport | StreamableHTTPClientTransport
|
type Transport = StdioClientTransport | StreamableHTTPClientTransport
|
||||||
|
|
||||||
|
|
@ -67,11 +69,13 @@ export interface ToolDefinition {
|
||||||
export interface PromptDefinition {
|
export interface PromptDefinition {
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly description: string | undefined
|
readonly description: string | undefined
|
||||||
readonly arguments: ReadonlyArray<{
|
readonly arguments:
|
||||||
readonly name: string
|
| ReadonlyArray<{
|
||||||
readonly description: string | undefined
|
readonly name: string
|
||||||
readonly required: boolean | undefined
|
readonly description: string | undefined
|
||||||
}> | undefined
|
readonly required: boolean | undefined
|
||||||
|
}>
|
||||||
|
| undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PromptMessage {
|
export interface PromptMessage {
|
||||||
|
|
@ -83,6 +87,28 @@ export interface PromptResult {
|
||||||
readonly messages: ReadonlyArray<PromptMessage>
|
readonly messages: ReadonlyArray<PromptMessage>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResourceDefinition {
|
||||||
|
readonly name: string
|
||||||
|
readonly uri: string
|
||||||
|
readonly description: string | undefined
|
||||||
|
readonly mimeType: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceTemplateDefinition {
|
||||||
|
readonly name: string
|
||||||
|
readonly uriTemplate: string
|
||||||
|
readonly description: string | undefined
|
||||||
|
readonly mimeType: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResourceContentPart =
|
||||||
|
| { readonly type: "text"; readonly uri: string; readonly text: string; readonly mimeType: string | undefined }
|
||||||
|
| { readonly type: "blob"; readonly uri: string; readonly blob: string; readonly mimeType: string | undefined }
|
||||||
|
|
||||||
|
export interface ReadResourceResult {
|
||||||
|
readonly contents: ReadonlyArray<ResourceContentPart>
|
||||||
|
}
|
||||||
|
|
||||||
export type CallToolContent =
|
export type CallToolContent =
|
||||||
| { readonly type: "text"; readonly text: string }
|
| { readonly type: "text"; readonly text: string }
|
||||||
| { readonly type: "media"; readonly data: string; readonly mimeType: string }
|
| { readonly type: "media"; readonly data: string; readonly mimeType: string }
|
||||||
|
|
@ -123,6 +149,12 @@ export interface Connection {
|
||||||
readonly tools: () => Effect.Effect<ToolDefinition[], Error>
|
readonly tools: () => Effect.Effect<ToolDefinition[], Error>
|
||||||
/** Lists the server's prompts; returns [] when the server doesn't advertise prompt support, fails on a transport error. */
|
/** Lists the server's prompts; returns [] when the server doesn't advertise prompt support, fails on a transport error. */
|
||||||
readonly prompts: () => Effect.Effect<PromptDefinition[], Error>
|
readonly prompts: () => Effect.Effect<PromptDefinition[], Error>
|
||||||
|
/** Lists the server's resources; returns [] when the server doesn't advertise resource support. */
|
||||||
|
readonly resources: () => Effect.Effect<ResourceDefinition[], Error>
|
||||||
|
/** Lists the server's resource templates; returns [] when the server doesn't advertise resource support. */
|
||||||
|
readonly resourceTemplates: () => Effect.Effect<ResourceTemplateDefinition[], Error>
|
||||||
|
/** Reads one resource; returns undefined when the server doesn't advertise resource support. */
|
||||||
|
readonly readResource: (input: { readonly uri: string }) => Effect.Effect<ReadResourceResult | undefined, Error>
|
||||||
/** Invokes a prompt on the server. Interruption aborts the in-flight request. */
|
/** Invokes a prompt on the server. Interruption aborts the in-flight request. */
|
||||||
readonly prompt: (input: {
|
readonly prompt: (input: {
|
||||||
readonly name: string
|
readonly name: string
|
||||||
|
|
@ -140,6 +172,8 @@ export interface Connection {
|
||||||
readonly onToolsChanged: (callback: () => void) => void
|
readonly onToolsChanged: (callback: () => void) => void
|
||||||
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
|
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
|
||||||
readonly onPromptsChanged: (callback: () => void) => void
|
readonly onPromptsChanged: (callback: () => void) => void
|
||||||
|
/** Registers a callback fired when the server announces its resource catalog changed. */
|
||||||
|
readonly onResourcesChanged: (callback: () => void) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
|
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
|
||||||
|
|
@ -167,7 +201,8 @@ export const connect = Effect.fnUntraced(function* (
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!URL.canParse(config.url)) return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
|
if (!URL.canParse(config.url))
|
||||||
|
return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
|
||||||
return new StreamableHTTPClientTransport(new URL(config.url), {
|
return new StreamableHTTPClientTransport(new URL(config.url), {
|
||||||
requestInit: config.headers ? { headers: config.headers } : undefined,
|
requestInit: config.headers ? { headers: config.headers } : undefined,
|
||||||
authProvider,
|
authProvider,
|
||||||
|
|
@ -201,12 +236,10 @@ export const connect = Effect.fnUntraced(function* (
|
||||||
}).pipe(Effect.exit)
|
}).pipe(Effect.exit)
|
||||||
if (Exit.isSuccess(exit)) {
|
if (Exit.isSuccess(exit)) {
|
||||||
yield* Effect.addFinalizer(() =>
|
yield* Effect.addFinalizer(() =>
|
||||||
cleanupStdioDescendants(transport).pipe(
|
cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => client.close())), Effect.ignore),
|
||||||
Effect.andThen(Effect.promise(() => client.close())),
|
|
||||||
Effect.ignore,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
const requestTimeout = config.timeout?.request ?? DEFAULT_REQUEST_TIMEOUT
|
const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT
|
||||||
|
const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT
|
||||||
return {
|
return {
|
||||||
instructions: client.getInstructions()?.trim() || undefined,
|
instructions: client.getInstructions()?.trim() || undefined,
|
||||||
tools: () =>
|
tools: () =>
|
||||||
|
|
@ -218,11 +251,11 @@ export const connect = Effect.fnUntraced(function* (
|
||||||
async (cursor) => {
|
async (cursor) => {
|
||||||
const params = cursor === undefined ? undefined : { cursor }
|
const params = cursor === undefined ? undefined : { cursor }
|
||||||
try {
|
try {
|
||||||
return await client.listTools(params, { timeout: requestTimeout })
|
return await client.listTools(params, { timeout: catalogTimeout })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof Error) || !isOutputSchemaError(error)) throw error
|
if (!(error instanceof Error) || !isOutputSchemaError(error)) throw error
|
||||||
return client.request({ method: "tools/list", params }, TolerantListToolsResult, {
|
return client.request({ method: "tools/list", params }, TolerantListToolsResult, {
|
||||||
timeout: requestTimeout,
|
timeout: catalogTimeout,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -248,14 +281,16 @@ export const connect = Effect.fnUntraced(function* (
|
||||||
async (cursor) => {
|
async (cursor) => {
|
||||||
const params = cursor === undefined ? undefined : { cursor }
|
const params = cursor === undefined ? undefined : { cursor }
|
||||||
return client.request({ method: "prompts/list", params }, TolerantListPromptsResult, {
|
return client.request({ method: "prompts/list", params }, TolerantListPromptsResult, {
|
||||||
timeout: requestTimeout,
|
timeout: catalogTimeout,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
(result) => result.prompts,
|
(result) => result.prompts,
|
||||||
),
|
),
|
||||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.tapError((error) => Effect.logWarning("failed to list MCP prompts", { server, error: error.message })),
|
Effect.tapError((error) =>
|
||||||
|
Effect.logWarning("failed to list MCP prompts", { server, error: error.message }),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return prompts.map((prompt) => ({
|
return prompts.map((prompt) => ({
|
||||||
name: prompt.name,
|
name: prompt.name,
|
||||||
|
|
@ -267,13 +302,81 @@ export const connect = Effect.fnUntraced(function* (
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
}),
|
}),
|
||||||
|
resources: () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
if (!client.getServerCapabilities()?.resources) return []
|
||||||
|
const resources = yield* Effect.tryPromise({
|
||||||
|
try: () =>
|
||||||
|
paginate(
|
||||||
|
(cursor) =>
|
||||||
|
client.listResources(cursor === undefined ? undefined : { cursor }, { timeout: catalogTimeout }),
|
||||||
|
(result) => result.resources,
|
||||||
|
),
|
||||||
|
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||||
|
}).pipe(
|
||||||
|
Effect.tapError((error) =>
|
||||||
|
Effect.logWarning("failed to list MCP resources", { server, error: error.message }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return resources.map((resource) => ({
|
||||||
|
name: resource.name,
|
||||||
|
uri: resource.uri,
|
||||||
|
description: resource.description,
|
||||||
|
mimeType: resource.mimeType,
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
resourceTemplates: () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
if (!client.getServerCapabilities()?.resources) return []
|
||||||
|
const templates = yield* Effect.tryPromise({
|
||||||
|
try: () =>
|
||||||
|
paginate(
|
||||||
|
(cursor) =>
|
||||||
|
client.listResourceTemplates(cursor === undefined ? undefined : { cursor }, {
|
||||||
|
timeout: catalogTimeout,
|
||||||
|
}),
|
||||||
|
(result) => result.resourceTemplates,
|
||||||
|
),
|
||||||
|
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||||
|
}).pipe(
|
||||||
|
Effect.tapError((error) =>
|
||||||
|
Effect.logWarning("failed to list MCP resource templates", { server, error: error.message }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return templates.map((template) => ({
|
||||||
|
name: template.name,
|
||||||
|
uriTemplate: template.uriTemplate,
|
||||||
|
description: template.description,
|
||||||
|
mimeType: template.mimeType,
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
readResource: (input) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
if (!client.getServerCapabilities()?.resources) return undefined
|
||||||
|
const result = yield* Effect.tryPromise({
|
||||||
|
try: (signal) => client.readResource({ uri: input.uri }, { signal, timeout: executionTimeout }),
|
||||||
|
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||||
|
}).pipe(
|
||||||
|
Effect.tapError((error) =>
|
||||||
|
Effect.logWarning("failed to read MCP resource", { server, uri: input.uri, error: error.message }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
contents: result.contents.map(
|
||||||
|
(part): ResourceContentPart =>
|
||||||
|
"text" in part
|
||||||
|
? { type: "text", uri: part.uri, text: part.text, mimeType: part.mimeType }
|
||||||
|
: { type: "blob", uri: part.uri, blob: part.blob, mimeType: part.mimeType },
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}),
|
||||||
prompt: (input) =>
|
prompt: (input) =>
|
||||||
Effect.tryPromise({
|
Effect.tryPromise({
|
||||||
try: (signal) =>
|
try: (signal) =>
|
||||||
client.request(
|
client.request(
|
||||||
{ method: "prompts/get", params: { name: input.name, arguments: input.args ?? {} } },
|
{ method: "prompts/get", params: { name: input.name, arguments: input.args ?? {} } },
|
||||||
GetPromptResultSchema,
|
GetPromptResultSchema,
|
||||||
{ signal },
|
{ signal, timeout: executionTimeout },
|
||||||
),
|
),
|
||||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||||
}).pipe(
|
}).pipe(
|
||||||
|
|
@ -287,8 +390,8 @@ export const connect = Effect.fnUntraced(function* (
|
||||||
client.callTool(
|
client.callTool(
|
||||||
{ name: input.name, arguments: input.args ?? {} },
|
{ name: input.name, arguments: input.args ?? {} },
|
||||||
CallToolResultSchema,
|
CallToolResultSchema,
|
||||||
// Keep progress tokens available without imposing a client timeout on tool execution.
|
// Keep progress tokens available while enforcing a hard wall-clock execution timeout.
|
||||||
{ signal, resetTimeoutOnProgress: true, onprogress: () => {} },
|
{ signal, timeout: executionTimeout, onprogress: () => {} },
|
||||||
),
|
),
|
||||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||||
}).pipe(
|
}).pipe(
|
||||||
|
|
@ -326,13 +429,14 @@ export const connect = Effect.fnUntraced(function* (
|
||||||
if (!client.getServerCapabilities()?.prompts?.listChanged) return
|
if (!client.getServerCapabilities()?.prompts?.listChanged) return
|
||||||
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
|
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
|
||||||
},
|
},
|
||||||
|
onResourcesChanged: (callback) => {
|
||||||
|
if (!client.getServerCapabilities()?.resources?.listChanged) return
|
||||||
|
client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => callback())
|
||||||
|
},
|
||||||
} satisfies Connection
|
} satisfies Connection
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* cleanupStdioDescendants(transport).pipe(
|
yield* cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => transport.close())), Effect.ignore)
|
||||||
Effect.andThen(Effect.promise(() => transport.close())),
|
|
||||||
Effect.ignore,
|
|
||||||
)
|
|
||||||
const error = Cause.squash(exit.cause)
|
const error = Cause.squash(exit.cause)
|
||||||
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
|
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
|
||||||
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
|
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
|
||||||
|
|
|
||||||
|
|
@ -83,48 +83,16 @@ export class PromptResult extends Schema.Class<PromptResult>("MCP.PromptResult")
|
||||||
messages: Schema.Array(PromptMessage),
|
messages: Schema.Array(PromptMessage),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export class Resource extends Schema.Class<Resource>("MCP.Resource")({
|
export const Resource = Mcp.Resource
|
||||||
server: ServerName,
|
export type Resource = Mcp.Resource
|
||||||
name: Schema.String,
|
export const ResourceTemplate = Mcp.ResourceTemplate
|
||||||
uri: Schema.String,
|
export type ResourceTemplate = Mcp.ResourceTemplate
|
||||||
description: Schema.String.pipe(Schema.optional),
|
export const ResourceCatalog = Mcp.ResourceCatalog
|
||||||
mimeType: Schema.String.pipe(Schema.optional),
|
export type ResourceCatalog = Mcp.ResourceCatalog
|
||||||
}) {}
|
export const ResourceContentPart = Mcp.ResourceContentPart
|
||||||
|
export type ResourceContentPart = Mcp.ResourceContentPart
|
||||||
export class ResourceTemplate extends Schema.Class<ResourceTemplate>("MCP.ResourceTemplate")({
|
export const ResourceContent = Mcp.ResourceContent
|
||||||
server: ServerName,
|
export type ResourceContent = Mcp.ResourceContent
|
||||||
name: Schema.String,
|
|
||||||
uriTemplate: Schema.String,
|
|
||||||
description: Schema.String.pipe(Schema.optional),
|
|
||||||
mimeType: Schema.String.pipe(Schema.optional),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class ResourceCatalog extends Schema.Class<ResourceCatalog>("MCP.ResourceCatalog")({
|
|
||||||
resources: Schema.Array(Resource),
|
|
||||||
templates: Schema.Array(ResourceTemplate),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export const ResourceContentPart = Schema.Union([
|
|
||||||
Schema.Struct({
|
|
||||||
type: Schema.Literal("text"),
|
|
||||||
uri: Schema.String,
|
|
||||||
text: Schema.String,
|
|
||||||
mimeType: Schema.String.pipe(Schema.optional),
|
|
||||||
}),
|
|
||||||
Schema.Struct({
|
|
||||||
type: Schema.Literal("blob"),
|
|
||||||
uri: Schema.String,
|
|
||||||
blob: Schema.String,
|
|
||||||
mimeType: Schema.String.pipe(Schema.optional),
|
|
||||||
}),
|
|
||||||
]).pipe(Schema.toTaggedUnion("type"))
|
|
||||||
export type ResourceContentPart = typeof ResourceContentPart.Type
|
|
||||||
|
|
||||||
export class ResourceContent extends Schema.Class<ResourceContent>("MCP.ResourceContent")({
|
|
||||||
server: ServerName,
|
|
||||||
uri: Schema.String,
|
|
||||||
contents: Schema.Array(ResourceContentPart),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
|
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
|
||||||
server: ServerName,
|
server: ServerName,
|
||||||
|
|
@ -415,6 +383,24 @@ export const layer = Layer.effect(
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const toResource = (server: ServerName, def: MCPClient.ResourceDefinition) =>
|
||||||
|
Resource.make({
|
||||||
|
server,
|
||||||
|
name: def.name,
|
||||||
|
uri: def.uri,
|
||||||
|
description: def.description,
|
||||||
|
mimeType: def.mimeType,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toResourceTemplate = (server: ServerName, def: MCPClient.ResourceTemplateDefinition) =>
|
||||||
|
ResourceTemplate.make({
|
||||||
|
server,
|
||||||
|
name: def.name,
|
||||||
|
uriTemplate: def.uriTemplate,
|
||||||
|
description: def.description,
|
||||||
|
mimeType: def.mimeType,
|
||||||
|
})
|
||||||
|
|
||||||
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
|
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
|
||||||
connection.tools().pipe(
|
connection.tools().pipe(
|
||||||
Effect.map((defs) => {
|
Effect.map((defs) => {
|
||||||
|
|
@ -443,6 +429,7 @@ export const layer = Layer.effect(
|
||||||
entry.prompts = undefined
|
entry.prompts = undefined
|
||||||
entry.status = { status: "failed", error: "Connection closed" }
|
entry.status = { status: "failed", error: "Connection closed" }
|
||||||
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
|
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
|
||||||
|
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
|
||||||
fork(events.publish(Command.Event.Updated, {}).pipe(Effect.ignore))
|
fork(events.publish(Command.Event.Updated, {}).pipe(Effect.ignore))
|
||||||
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
|
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
|
||||||
})
|
})
|
||||||
|
|
@ -458,6 +445,10 @@ export const layer = Layer.effect(
|
||||||
connection.onPromptsChanged(() => {
|
connection.onPromptsChanged(() => {
|
||||||
fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore))
|
fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore))
|
||||||
})
|
})
|
||||||
|
connection.onResourcesChanged(() => {
|
||||||
|
if (entry.client !== connection) return
|
||||||
|
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
|
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
|
||||||
|
|
@ -501,6 +492,7 @@ export const layer = Layer.effect(
|
||||||
// after the initial registration sweep and emits no list-changed notification would otherwise
|
// after the initial registration sweep and emits no list-changed notification would otherwise
|
||||||
// stay invisible to the model.
|
// stay invisible to the model.
|
||||||
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
|
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
|
||||||
|
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
|
||||||
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
|
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
|
||||||
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
|
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
|
||||||
return
|
return
|
||||||
|
|
@ -557,11 +549,6 @@ export const layer = Layer.effect(
|
||||||
concurrency: "unbounded",
|
concurrency: "unbounded",
|
||||||
discard: true,
|
discard: true,
|
||||||
})
|
})
|
||||||
const gate = Effect.fnUntraced(function* (server: ServerName | string) {
|
|
||||||
const target = yield* requireServer(server)
|
|
||||||
yield* Deferred.await(target.entry.startup)
|
|
||||||
})
|
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
servers: Effect.fn("MCP.servers")(function* () {
|
servers: Effect.fn("MCP.servers")(function* () {
|
||||||
const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b))
|
const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b))
|
||||||
|
|
@ -637,11 +624,54 @@ export const layer = Layer.effect(
|
||||||
}),
|
}),
|
||||||
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
|
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
|
||||||
yield* whenAllReady
|
yield* whenAllReady
|
||||||
return new ResourceCatalog({ resources: [], templates: [] })
|
const catalogs = yield* Effect.forEach(
|
||||||
|
Array.from(runtime),
|
||||||
|
([name, entry]) => {
|
||||||
|
if (!entry.client) return Effect.succeed({ resources: [], templates: [] })
|
||||||
|
return Effect.all(
|
||||||
|
{
|
||||||
|
resources: entry.client.resources().pipe(Effect.catch(() => Effect.succeed([]))),
|
||||||
|
templates: entry.client.resourceTemplates().pipe(Effect.catch(() => Effect.succeed([]))),
|
||||||
|
},
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
).pipe(
|
||||||
|
Effect.map((catalog) => ({
|
||||||
|
resources: catalog.resources.map((def) => toResource(name, def)),
|
||||||
|
templates: catalog.templates.map((def) => toResourceTemplate(name, def)),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
)
|
||||||
|
return ResourceCatalog.make({
|
||||||
|
resources: catalogs
|
||||||
|
.flatMap((catalog) => catalog.resources)
|
||||||
|
.toSorted(
|
||||||
|
(a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name) || a.uri.localeCompare(b.uri),
|
||||||
|
),
|
||||||
|
templates: catalogs
|
||||||
|
.flatMap((catalog) => catalog.templates)
|
||||||
|
.toSorted(
|
||||||
|
(a, b) =>
|
||||||
|
a.server.localeCompare(b.server) ||
|
||||||
|
a.name.localeCompare(b.name) ||
|
||||||
|
a.uriTemplate.localeCompare(b.uriTemplate),
|
||||||
|
),
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
readResource: Effect.fn("MCP.readResource")(function* (input) {
|
readResource: Effect.fn("MCP.readResource")(function* (input) {
|
||||||
yield* gate(input.server)
|
const target = yield* requireServer(input.server)
|
||||||
return undefined
|
yield* Deferred.await(target.entry.startup)
|
||||||
|
if (!target.entry.client) return undefined
|
||||||
|
const result = yield* target.entry.client
|
||||||
|
.readResource({ uri: input.uri })
|
||||||
|
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||||
|
if (!result) return undefined
|
||||||
|
return ResourceContent.make({
|
||||||
|
server: target.name,
|
||||||
|
uri: input.uri,
|
||||||
|
contents: result.contents,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import path from "path"
|
||||||
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
|
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
|
||||||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||||
import { ModelsDev } from "@opencode-ai/schema/models-dev"
|
import { ModelsDev } from "@opencode-ai/schema/models-dev"
|
||||||
|
import { Money } from "@opencode-ai/schema/money"
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
import { Flag } from "./flag/flag"
|
import { Flag } from "./flag/flag"
|
||||||
import { Flock } from "./util/flock"
|
import { Flock } from "./util/flock"
|
||||||
|
|
@ -18,10 +19,10 @@ export type CatalogModelStatus = typeof CatalogModelStatus.Type
|
||||||
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
|
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
|
||||||
|
|
||||||
const CostTier = Schema.Struct({
|
const CostTier = Schema.Struct({
|
||||||
input: Schema.Finite,
|
input: Money.USDPerMillionTokens,
|
||||||
output: Schema.Finite,
|
output: Money.USDPerMillionTokens,
|
||||||
cache_read: Schema.optional(Schema.Finite),
|
cache_read: Schema.optional(Money.USDPerMillionTokens),
|
||||||
cache_write: Schema.optional(Schema.Finite),
|
cache_write: Schema.optional(Money.USDPerMillionTokens),
|
||||||
tier: Schema.Struct({
|
tier: Schema.Struct({
|
||||||
type: Schema.Literal("context"),
|
type: Schema.Literal("context"),
|
||||||
size: Schema.Finite,
|
size: Schema.Finite,
|
||||||
|
|
@ -29,17 +30,17 @@ const CostTier = Schema.Struct({
|
||||||
})
|
})
|
||||||
|
|
||||||
const Cost = Schema.Struct({
|
const Cost = Schema.Struct({
|
||||||
input: Schema.Finite,
|
input: Money.USDPerMillionTokens,
|
||||||
output: Schema.Finite,
|
output: Money.USDPerMillionTokens,
|
||||||
cache_read: Schema.optional(Schema.Finite),
|
cache_read: Schema.optional(Money.USDPerMillionTokens),
|
||||||
cache_write: Schema.optional(Schema.Finite),
|
cache_write: Schema.optional(Money.USDPerMillionTokens),
|
||||||
tiers: Schema.optional(Schema.Array(CostTier)),
|
tiers: Schema.optional(Schema.Array(CostTier)),
|
||||||
context_over_200k: Schema.optional(
|
context_over_200k: Schema.optional(
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
input: Schema.Finite,
|
input: Money.USDPerMillionTokens,
|
||||||
output: Schema.Finite,
|
output: Money.USDPerMillionTokens,
|
||||||
cache_read: Schema.optional(Schema.Finite),
|
cache_read: Schema.optional(Money.USDPerMillionTokens),
|
||||||
cache_write: Schema.optional(Schema.Finite),
|
cache_write: Schema.optional(Money.USDPerMillionTokens),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,13 @@ export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("P
|
||||||
|
|
||||||
export class BlockedError extends Schema.TaggedErrorClass<BlockedError>()("PermissionV2.BlockedError", {
|
export class BlockedError extends Schema.TaggedErrorClass<BlockedError>()("PermissionV2.BlockedError", {
|
||||||
rules: Permission.Ruleset,
|
rules: Permission.Ruleset,
|
||||||
}) {}
|
permission: Schema.String,
|
||||||
|
resources: Schema.Array(Schema.String),
|
||||||
|
}) {
|
||||||
|
override get message() {
|
||||||
|
return `Permission denied: ${this.permission}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", {
|
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", {
|
||||||
requestID: ID,
|
requestID: ID,
|
||||||
|
|
@ -201,6 +207,8 @@ const layer = Layer.effect(
|
||||||
if (result.effect === "deny") {
|
if (result.effect === "deny") {
|
||||||
return yield* new BlockedError({
|
return yield* new BlockedError({
|
||||||
rules: relevant(input, result.rules),
|
rules: relevant(input, result.rules),
|
||||||
|
permission: input.action,
|
||||||
|
resources: input.resources,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (result.effect === "allow") return
|
if (result.effect === "allow") return
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * as PluginV2 from "./plugin"
|
export * as PluginV2 from "./plugin"
|
||||||
|
|
||||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Event, ID, type Info } from "@opencode-ai/schema/plugin"
|
import { Event, ID, type Info } from "@opencode-ai/schema/plugin"
|
||||||
import { makeLocationNode } from "./effect/app-node"
|
import { makeLocationNode } from "./effect/app-node"
|
||||||
import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
|
import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
|
||||||
|
|
@ -18,6 +18,7 @@ import { SkillV2 } from "./skill"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
import { ToolRegistry } from "./tool/registry"
|
import { ToolRegistry } from "./tool/registry"
|
||||||
import { ToolHooks } from "./tool/hooks"
|
import { ToolHooks } from "./tool/hooks"
|
||||||
|
import { PluginHooks } from "./plugin/hooks"
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly activate: (plugins: readonly { readonly plugin: Plugin; readonly version?: string }[]) => Effect.Effect<void>
|
readonly activate: (plugins: readonly { readonly plugin: Plugin; readonly version?: string }[]) => Effect.Effect<void>
|
||||||
|
|
@ -57,12 +58,13 @@ const layer = Layer.effect(
|
||||||
generation.length === definitions.length &&
|
generation.length === definitions.length &&
|
||||||
generation.every(
|
generation.every(
|
||||||
(plugin, index) => plugin.id === definitions[index]?.id && plugin.version === definitions[index]?.version,
|
(plugin, index) => plugin.id === definitions[index]?.id && plugin.version === definitions[index]?.version,
|
||||||
)
|
) &&
|
||||||
|
definitions.every((definition) => active.has(definition.id))
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
generation = undefined
|
generation = undefined
|
||||||
const exit = yield* State.batch(
|
yield* State.batch(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const scopes = Array.from(active.values()).toReversed()
|
const scopes = Array.from(active.values()).toReversed()
|
||||||
active.clear()
|
active.clear()
|
||||||
|
|
@ -81,13 +83,17 @@ const layer = Layer.effect(
|
||||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
||||||
Effect.exit,
|
Effect.exit,
|
||||||
)
|
)
|
||||||
if (Exit.isFailure(loaded)) return loaded
|
if (Exit.isFailure(loaded)) {
|
||||||
|
yield* Effect.logWarning("failed to load plugin", {
|
||||||
|
"plugin.id": definition.id,
|
||||||
|
cause: loaded.cause,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
active.set(definition.id, child)
|
active.set(definition.id, child)
|
||||||
}
|
}
|
||||||
return Exit.void
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (Exit.isFailure(exit)) return yield* exit
|
|
||||||
generation = definitions.map((definition) => ({
|
generation = definitions.map((definition) => ({
|
||||||
id: definition.id,
|
id: definition.id,
|
||||||
...(definition.version === undefined ? {} : { version: definition.version }),
|
...(definition.version === undefined ? {} : { version: definition.version }),
|
||||||
|
|
@ -131,6 +137,7 @@ export const node = makeLocationNode({
|
||||||
SkillV2.node,
|
SkillV2.node,
|
||||||
ToolRegistry.toolsNode,
|
ToolRegistry.toolsNode,
|
||||||
ToolHooks.node,
|
ToolHooks.node,
|
||||||
|
PluginHooks.node,
|
||||||
PluginRuntime.node,
|
PluginRuntime.node,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ export const Plugin = define({
|
||||||
|
|
||||||
yield* ctx.agent.transform((draft) => {
|
yield* ctx.agent.transform((draft) => {
|
||||||
draft.update(AgentV2.defaultID, (item) => {
|
draft.update(AgentV2.defaultID, (item) => {
|
||||||
|
item.name = AgentV2.Name.make("Build")
|
||||||
item.description = "The default agent. Executes tools based on configured permissions."
|
item.description = "The default agent. Executes tools based on configured permissions."
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.permissions.push(
|
item.permissions.push(
|
||||||
|
|
@ -136,6 +137,7 @@ export const Plugin = define({
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("plan"), (item) => {
|
draft.update(AgentV2.ID.make("plan"), (item) => {
|
||||||
|
item.name = AgentV2.Name.make("Plan")
|
||||||
item.description = "Plan mode. Disallows all edit tools."
|
item.description = "Plan mode. Disallows all edit tools."
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.permissions.push(
|
item.permissions.push(
|
||||||
|
|
@ -155,6 +157,7 @@ export const Plugin = define({
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("general"), (item) => {
|
draft.update(AgentV2.ID.make("general"), (item) => {
|
||||||
|
item.name = AgentV2.Name.make("General")
|
||||||
item.description =
|
item.description =
|
||||||
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
|
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
|
||||||
item.mode = "subagent"
|
item.mode = "subagent"
|
||||||
|
|
@ -167,6 +170,7 @@ export const Plugin = define({
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("explore"), (item) => {
|
draft.update(AgentV2.ID.make("explore"), (item) => {
|
||||||
|
item.name = AgentV2.Name.make("Explore")
|
||||||
item.description =
|
item.description =
|
||||||
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
|
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
|
||||||
item.system = PROMPT_EXPLORE
|
item.system = PROMPT_EXPLORE
|
||||||
|
|
@ -189,6 +193,7 @@ export const Plugin = define({
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("compaction"), (item) => {
|
draft.update(AgentV2.ID.make("compaction"), (item) => {
|
||||||
|
item.name = AgentV2.Name.make("Compaction")
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.hidden = true
|
item.hidden = true
|
||||||
item.system = PROMPT_COMPACTION
|
item.system = PROMPT_COMPACTION
|
||||||
|
|
@ -196,6 +201,7 @@ export const Plugin = define({
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("title"), (item) => {
|
draft.update(AgentV2.ID.make("title"), (item) => {
|
||||||
|
item.name = AgentV2.Name.make("Title")
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.hidden = true
|
item.hidden = true
|
||||||
item.system = PROMPT_TITLE
|
item.system = PROMPT_TITLE
|
||||||
|
|
@ -203,6 +209,7 @@ export const Plugin = define({
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(AgentV2.ID.make("summary"), (item) => {
|
draft.update(AgentV2.ID.make("summary"), (item) => {
|
||||||
|
item.name = AgentV2.Name.make("Summary")
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.hidden = true
|
item.hidden = true
|
||||||
item.system = PROMPT_SUMMARY
|
item.system = PROMPT_SUMMARY
|
||||||
|
|
|
||||||
67
packages/core/src/plugin/hooks.ts
Normal file
67
packages/core/src/plugin/hooks.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
export * as PluginHooks from "./hooks"
|
||||||
|
|
||||||
|
import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
|
||||||
|
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
|
||||||
|
import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
|
||||||
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
|
import { State } from "../state"
|
||||||
|
|
||||||
|
export interface Domains {
|
||||||
|
readonly aisdk: AISDKHooks
|
||||||
|
readonly session: SessionHooks
|
||||||
|
readonly tool: ToolHooks
|
||||||
|
}
|
||||||
|
|
||||||
|
type Callback<Event> = (event: Event) => Effect.Effect<void>
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly register: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
|
||||||
|
domain: Domain,
|
||||||
|
name: Name,
|
||||||
|
callback: Callback<Domains[Domain][Name]>,
|
||||||
|
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||||
|
readonly trigger: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
|
||||||
|
domain: Domain,
|
||||||
|
name: Name,
|
||||||
|
event: Domains[Domain][Name],
|
||||||
|
) => Effect.Effect<Domains[Domain][Name]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginHooks") {}
|
||||||
|
|
||||||
|
const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const callbacks = new Map<string, Function[]>()
|
||||||
|
const key = (domain: keyof Domains, name: PropertyKey) => `${domain}.${String(name)}`
|
||||||
|
|
||||||
|
const register: Interface["register"] = Effect.fn("PluginHooks.register")(function* (domain, name, callback) {
|
||||||
|
const scope = yield* Scope.Scope
|
||||||
|
const id = key(domain, name)
|
||||||
|
let active = true
|
||||||
|
callbacks.set(id, [...(callbacks.get(id) ?? []), callback])
|
||||||
|
const dispose = Effect.sync(() => {
|
||||||
|
if (!active) return
|
||||||
|
active = false
|
||||||
|
const next = (callbacks.get(id) ?? []).filter((item) => item !== callback)
|
||||||
|
if (next.length === 0) callbacks.delete(id)
|
||||||
|
else callbacks.set(id, next)
|
||||||
|
})
|
||||||
|
yield* Scope.addFinalizer(scope, dispose)
|
||||||
|
return { dispose }
|
||||||
|
})
|
||||||
|
|
||||||
|
const trigger: Interface["trigger"] = Effect.fn("PluginHooks.trigger")(function* (domain, name, event) {
|
||||||
|
for (const callback of callbacks.get(key(domain, name)) ?? []) {
|
||||||
|
const result: Effect.Effect<void> = Reflect.apply(callback, undefined, [event])
|
||||||
|
yield* result
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
return Service.of({ register, trigger })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * as PluginHost from "./host"
|
export * as PluginHost from "./host"
|
||||||
|
|
||||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { EventManifest } from "@opencode-ai/schema/event-manifest"
|
import { EventManifest } from "@opencode-ai/schema/event-manifest"
|
||||||
import { Effect, Schema, Stream } from "effect"
|
import { Effect, Schema, Stream } from "effect"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
|
|
@ -22,6 +22,7 @@ import { Tool } from "../tool/tool"
|
||||||
import { Tools } from "../tool/tools"
|
import { Tools } from "../tool/tools"
|
||||||
import { ToolHooks } from "../tool/hooks"
|
import { ToolHooks } from "../tool/hooks"
|
||||||
import { WorkspaceV2 } from "../workspace"
|
import { WorkspaceV2 } from "../workspace"
|
||||||
|
import { PluginHooks } from "./hooks"
|
||||||
|
|
||||||
const mutable = <T>(value: T) => value as DeepMutable<T>
|
const mutable = <T>(value: T) => value as DeepMutable<T>
|
||||||
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
|
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
|
||||||
|
|
@ -36,6 +37,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||||
const skill = yield* SkillV2.Service
|
const skill = yield* SkillV2.Service
|
||||||
const tools = yield* Tools.Service
|
const tools = yield* Tools.Service
|
||||||
const toolHooks = yield* ToolHooks.Service
|
const toolHooks = yield* ToolHooks.Service
|
||||||
|
const hooks = yield* PluginHooks.Service
|
||||||
const runtime = yield* PluginRuntime.Service
|
const runtime = yield* PluginRuntime.Service
|
||||||
const locationInfo = () =>
|
const locationInfo = () =>
|
||||||
new Location.Info({
|
new Location.Info({
|
||||||
|
|
@ -43,7 +45,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||||
workspaceID: location.workspaceID,
|
workspaceID: location.workspaceID,
|
||||||
project: location.project,
|
project: location.project,
|
||||||
})
|
})
|
||||||
const locationRef = (input?: Parameters<PluginContext["agent"]["list"]>[0]) =>
|
const locationRef = (input?: Parameters<Plugin.Context["agent"]["list"]>[0]) =>
|
||||||
input?.location === undefined
|
input?.location === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: Location.Ref.make({
|
: Location.Ref.make({
|
||||||
|
|
@ -79,32 +81,32 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
aisdk: {
|
aisdk: {
|
||||||
sdk: (callback) =>
|
hook: (name, callback) => {
|
||||||
aisdk.hook.sdk((event) => {
|
if (name === "sdk") {
|
||||||
|
return aisdk.hook.sdk((event) => {
|
||||||
|
const output = {
|
||||||
|
model: mutable(event.model),
|
||||||
|
package: event.package,
|
||||||
|
options: event.options,
|
||||||
|
sdk: event.sdk,
|
||||||
|
}
|
||||||
|
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||||
|
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return aisdk.hook.language((event) => {
|
||||||
const output = {
|
const output = {
|
||||||
model: mutable(event.model),
|
model: mutable(event.model),
|
||||||
package: event.package,
|
|
||||||
options: event.options,
|
options: event.options,
|
||||||
sdk: event.sdk,
|
sdk: event.sdk,
|
||||||
}
|
|
||||||
const result = callback(output)
|
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
|
||||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
language: (callback) =>
|
|
||||||
aisdk.hook.language((event) => {
|
|
||||||
const output = {
|
|
||||||
model: mutable(event.model),
|
|
||||||
sdk: event.sdk,
|
|
||||||
options: event.options,
|
|
||||||
language: event.language,
|
language: event.language,
|
||||||
}
|
}
|
||||||
const result = callback(output)
|
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
|
||||||
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
||||||
)
|
)
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
catalog: {
|
catalog: {
|
||||||
provider: {
|
provider: {
|
||||||
|
|
@ -164,25 +166,29 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||||
integration: {
|
integration: {
|
||||||
list: () => response(integration.list()),
|
list: () => response(integration.list()),
|
||||||
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
|
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
|
||||||
connectKey: (input) =>
|
connect: {
|
||||||
integration.connection.key({
|
key: (input) =>
|
||||||
integrationID: Integration.ID.make(input.integrationID),
|
integration.connection.key({
|
||||||
key: input.key,
|
|
||||||
label: input.label,
|
|
||||||
}),
|
|
||||||
connectOauth: (input) =>
|
|
||||||
response(
|
|
||||||
integration.connection.oauth({
|
|
||||||
integrationID: Integration.ID.make(input.integrationID),
|
integrationID: Integration.ID.make(input.integrationID),
|
||||||
methodID: Integration.MethodID.make(input.methodID),
|
key: input.key,
|
||||||
inputs: input.inputs,
|
|
||||||
label: input.label,
|
label: input.label,
|
||||||
}),
|
}),
|
||||||
),
|
oauth: (input) =>
|
||||||
attemptStatus: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
|
response(
|
||||||
attemptComplete: (input) =>
|
integration.connection.oauth({
|
||||||
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
|
integrationID: Integration.ID.make(input.integrationID),
|
||||||
attemptCancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
|
methodID: Integration.MethodID.make(input.methodID),
|
||||||
|
inputs: input.inputs,
|
||||||
|
label: input.label,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
attempt: {
|
||||||
|
status: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
|
||||||
|
complete: (input) =>
|
||||||
|
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
|
||||||
|
cancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
|
||||||
|
},
|
||||||
reload: integration.reload,
|
reload: integration.reload,
|
||||||
connection: {
|
connection: {
|
||||||
active: (id) => integration.connection.active(Integration.ID.make(id)),
|
active: (id) => integration.connection.active(Integration.ID.make(id)),
|
||||||
|
|
@ -317,11 +323,12 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||||
registrations,
|
registrations,
|
||||||
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
|
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
|
||||||
{ discard: true },
|
{ discard: true },
|
||||||
)
|
).pipe(Effect.orDie)
|
||||||
|
return { dispose: Effect.void }
|
||||||
}),
|
}),
|
||||||
execute: {
|
hook: (name, callback) => {
|
||||||
before: (callback) =>
|
if (name === "execute.before") {
|
||||||
toolHooks.hook.before((event) => {
|
return toolHooks.hook.before((event) => {
|
||||||
const output = {
|
const output = {
|
||||||
tool: event.tool,
|
tool: event.tool,
|
||||||
sessionID: event.sessionID,
|
sessionID: event.sessionID,
|
||||||
|
|
@ -330,38 +337,37 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||||
toolCallID: event.toolCallID,
|
toolCallID: event.toolCallID,
|
||||||
input: event.input,
|
input: event.input,
|
||||||
}
|
}
|
||||||
const result = callback(output)
|
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
|
||||||
Effect.tap(() => Effect.sync(() => (event.input = output.input))),
|
Effect.tap(() => Effect.sync(() => (event.input = output.input))),
|
||||||
)
|
)
|
||||||
}),
|
})
|
||||||
after: (callback) =>
|
}
|
||||||
toolHooks.hook.after((event) => {
|
return toolHooks.hook.after((event) => {
|
||||||
const output = {
|
const output = {
|
||||||
tool: event.tool,
|
tool: event.tool,
|
||||||
sessionID: event.sessionID,
|
sessionID: event.sessionID,
|
||||||
agent: event.agent,
|
agent: event.agent,
|
||||||
assistantMessageID: event.assistantMessageID,
|
assistantMessageID: event.assistantMessageID,
|
||||||
toolCallID: event.toolCallID,
|
toolCallID: event.toolCallID,
|
||||||
input: event.input,
|
input: event.input,
|
||||||
result: event.result,
|
result: event.result,
|
||||||
output: event.output,
|
output: event.output,
|
||||||
outputPaths: event.outputPaths,
|
outputPaths: event.outputPaths,
|
||||||
}
|
}
|
||||||
const result = callback(output)
|
return Reflect.apply(callback, undefined, [output]).pipe(
|
||||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
Effect.tap(() =>
|
||||||
Effect.tap(() =>
|
Effect.sync(() => {
|
||||||
Effect.sync(() => {
|
event.result = output.result
|
||||||
event.result = output.result
|
event.output = output.output
|
||||||
event.output = output.output
|
event.outputPaths = output.outputPaths
|
||||||
event.outputPaths = output.outputPaths
|
}),
|
||||||
}),
|
),
|
||||||
),
|
)
|
||||||
)
|
})
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
|
hook: (name, callback) => hooks.register("session", name, callback),
|
||||||
create: (input) =>
|
create: (input) =>
|
||||||
runtime.session.create({
|
runtime.session.create({
|
||||||
id: input?.id,
|
id: input?.id,
|
||||||
|
|
@ -375,5 +381,5 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||||
command: runtime.session.command,
|
command: runtime.session.command,
|
||||||
interrupt: (input) => runtime.session.interrupt(input.sessionID),
|
interrupt: (input) => runtime.session.interrupt(input.sessionID),
|
||||||
},
|
},
|
||||||
} satisfies PluginContext
|
} satisfies Plugin.Context
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * as PluginInternal from "./internal"
|
export * as PluginInternal from "./internal"
|
||||||
|
|
||||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Context, Effect, Scope } from "effect"
|
import { Context, Effect, Scope } from "effect"
|
||||||
import { HttpClient } from "effect/unstable/http"
|
import { HttpClient } from "effect/unstable/http"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
|
|
@ -31,7 +31,7 @@ import { SessionInstructions } from "../session/instructions"
|
||||||
import { SessionTodo } from "../session/todo"
|
import { SessionTodo } from "../session/todo"
|
||||||
import { Shell } from "../shell"
|
import { Shell } from "../shell"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
import { ApplyPatchTool } from "../tool/apply-patch"
|
import { PatchTool } from "../tool/patch"
|
||||||
import { EditTool } from "../tool/edit"
|
import { EditTool } from "../tool/edit"
|
||||||
import { GlobTool } from "../tool/glob"
|
import { GlobTool } from "../tool/glob"
|
||||||
import { GrepTool } from "../tool/grep"
|
import { GrepTool } from "../tool/grep"
|
||||||
|
|
@ -127,7 +127,7 @@ const pre = [
|
||||||
SkillPlugin.Plugin,
|
SkillPlugin.Plugin,
|
||||||
ModelsDevPlugin,
|
ModelsDevPlugin,
|
||||||
...ProviderPlugins,
|
...ProviderPlugins,
|
||||||
ApplyPatchTool.Plugin,
|
PatchTool.Plugin,
|
||||||
EditTool.Plugin,
|
EditTool.Plugin,
|
||||||
GlobTool.Plugin,
|
GlobTool.Plugin,
|
||||||
GrepTool.Plugin,
|
GrepTool.Plugin,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
import { Money } from "@opencode-ai/schema/money"
|
||||||
|
import type { ModelInfo } from "@opencode-ai/sdk/v2/types"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { ModelV2 } from "../model"
|
import { ModelV2 } from "../model"
|
||||||
|
|
@ -11,13 +12,13 @@ function released(date: string) {
|
||||||
return Number.isFinite(time) ? time : 0
|
return Number.isFinite(time) ? time : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
|
function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
|
||||||
const base = {
|
const base = {
|
||||||
input: input?.input ?? 0,
|
input: input?.input ?? Money.USDPerMillionTokens.zero,
|
||||||
output: input?.output ?? 0,
|
output: input?.output ?? Money.USDPerMillionTokens.zero,
|
||||||
cache: {
|
cache: {
|
||||||
read: input?.cache_read ?? 0,
|
read: input?.cache_read ?? Money.USDPerMillionTokens.zero,
|
||||||
write: input?.cache_write ?? 0,
|
write: input?.cache_write ?? Money.USDPerMillionTokens.zero,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
|
@ -27,8 +28,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
|
||||||
input: item.input,
|
input: item.input,
|
||||||
output: item.output,
|
output: item.output,
|
||||||
cache: {
|
cache: {
|
||||||
read: item.cache_read ?? 0,
|
read: item.cache_read ?? Money.USDPerMillionTokens.zero,
|
||||||
write: item.cache_write ?? 0,
|
write: item.cache_write ?? Money.USDPerMillionTokens.zero,
|
||||||
},
|
},
|
||||||
})) ?? []),
|
})) ?? []),
|
||||||
...(input?.context_over_200k
|
...(input?.context_over_200k
|
||||||
|
|
@ -41,8 +42,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
|
||||||
input: input.context_over_200k.input,
|
input: input.context_over_200k.input,
|
||||||
output: input.context_over_200k.output,
|
output: input.context_over_200k.output,
|
||||||
cache: {
|
cache: {
|
||||||
read: input.context_over_200k.cache_read ?? 0,
|
read: input.context_over_200k.cache_read ?? Money.USDPerMillionTokens.zero,
|
||||||
write: input.context_over_200k.cache_write ?? 0,
|
write: input.context_over_200k.cache_write ?? Money.USDPerMillionTokens.zero,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -50,13 +51,13 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) {
|
function mergeCost(base: ModelInfo["cost"], override: ModelsDev.Model["cost"] | undefined) {
|
||||||
if (!override) return base
|
if (!override) return base
|
||||||
const next = cost(override)
|
const next = cost(override)
|
||||||
const [baseDefault, ...baseTiers] = base
|
const [baseDefault, ...baseTiers] = base
|
||||||
const [nextDefault, ...nextTiers] = next
|
const [nextDefault, ...nextTiers] = next
|
||||||
const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
|
const tierKey = (item: ModelInfo["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
|
||||||
const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({
|
const merge = (left: ModelInfo["cost"][number], right: ModelInfo["cost"][number]) => ({
|
||||||
...left,
|
...left,
|
||||||
...right,
|
...right,
|
||||||
tier: right.tier ?? left.tier,
|
tier: right.tier ?? left.tier,
|
||||||
|
|
@ -67,12 +68,25 @@ function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"]
|
||||||
const current = tiers.get(tierKey(item))
|
const current = tiers.get(tierKey(item))
|
||||||
tiers.set(tierKey(item), current ? merge(current, item) : item)
|
tiers.set(tierKey(item), current ? merge(current, item) : item)
|
||||||
}
|
}
|
||||||
return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
|
return [
|
||||||
|
merge(
|
||||||
|
baseDefault ?? {
|
||||||
|
input: Money.USDPerMillionTokens.zero,
|
||||||
|
output: Money.USDPerMillionTokens.zero,
|
||||||
|
cache: {
|
||||||
|
read: Money.USDPerMillionTokens.zero,
|
||||||
|
write: Money.USDPerMillionTokens.zero,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nextDefault,
|
||||||
|
),
|
||||||
|
...tiers.values(),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
||||||
|
|
||||||
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelV2Info["variants"]> {
|
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelInfo["variants"]> {
|
||||||
const npm = model.provider?.npm ?? provider.npm
|
const npm = model.provider?.npm ?? provider.npm
|
||||||
const options = model.reasoning_options ?? []
|
const options = model.reasoning_options ?? []
|
||||||
const effort = options.find((option) => option.type === "effort")
|
const effort = options.find((option) => option.type === "effort")
|
||||||
|
|
@ -117,7 +131,7 @@ function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.
|
||||||
function budgetVariants(
|
function budgetVariants(
|
||||||
npm: string | undefined,
|
npm: string | undefined,
|
||||||
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
|
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
|
||||||
): NonNullable<ModelV2Info["variants"]> {
|
): NonNullable<ModelInfo["variants"]> {
|
||||||
const max = option.max
|
const max = option.max
|
||||||
const high =
|
const high =
|
||||||
option.max === undefined
|
option.max === undefined
|
||||||
|
|
@ -146,7 +160,7 @@ function modeName(model: ModelsDev.Model, mode: string) {
|
||||||
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
|
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeVariants(model: ModelV2Info, next: NonNullable<ModelV2Info["variants"]>) {
|
function mergeVariants(model: ModelInfo, next: NonNullable<ModelInfo["variants"]>) {
|
||||||
const variants = model.variants ?? []
|
const variants = model.variants ?? []
|
||||||
const existing = new Map(variants.map((variant) => [variant.id, variant]))
|
const existing = new Map(variants.map((variant) => [variant.id, variant]))
|
||||||
const nextIDs = new Set(next.map((variant) => variant.id))
|
const nextIDs = new Set(next.map((variant) => variant.id))
|
||||||
|
|
@ -157,13 +171,13 @@ function mergeVariants(model: ModelV2Info, next: NonNullable<ModelV2Info["varian
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyModel(
|
function applyModel(
|
||||||
draft: ModelV2Info,
|
draft: ModelInfo,
|
||||||
model: ModelsDev.Model,
|
model: ModelsDev.Model,
|
||||||
input: {
|
input: {
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
readonly cost?: ModelV2Info["cost"]
|
readonly cost?: ModelInfo["cost"]
|
||||||
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
|
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
|
||||||
readonly variants?: NonNullable<ModelV2Info["variants"]>
|
readonly variants?: NonNullable<ModelInfo["variants"]>
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
draft.name = input.name ?? model.name
|
draft.name = input.name ?? model.name
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
export * as PluginPromise from "./promise"
|
export * as PluginPromise from "./promise"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||||
import type { Plugin, PluginContext } from "@opencode-ai/plugin/v2/promise"
|
|
||||||
import { Effect, Scope, Stream } from "effect"
|
import { Effect, Scope, Stream } from "effect"
|
||||||
|
|
||||||
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
||||||
type Registration = { readonly dispose: () => Promise<void> }
|
type Registration = { readonly dispose: () => Promise<void> }
|
||||||
|
type PromisePlugin = import("@opencode-ai/plugin/v2/plugin").Plugin
|
||||||
|
type PromisePluginContext = import("@opencode-ai/plugin/v2/plugin").Context
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
||||||
|
|
@ -16,8 +17,8 @@ type Registration = { readonly dispose: () => Promise<void> }
|
||||||
* preserves boot-time batching, so Promise-plugin transforms still coalesce
|
* preserves boot-time batching, so Promise-plugin transforms still coalesce
|
||||||
* into one reload per domain.
|
* into one reload per domain.
|
||||||
*/
|
*/
|
||||||
export function fromPromise(plugin: Plugin) {
|
export function fromPromise(plugin: PromisePlugin) {
|
||||||
return define({
|
return Plugin.define({
|
||||||
id: plugin.id,
|
id: plugin.id,
|
||||||
effect: (host) =>
|
effect: (host) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -43,7 +44,7 @@ export function fromPromise(plugin: Plugin) {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const context2: PluginContext = {
|
const context2: PromisePluginContext = {
|
||||||
options: host.options,
|
options: host.options,
|
||||||
agent: {
|
agent: {
|
||||||
list: (input) => run(host.agent.list(input)),
|
list: (input) => run(host.agent.list(input)),
|
||||||
|
|
@ -51,10 +52,8 @@ export function fromPromise(plugin: Plugin) {
|
||||||
reload: () => run(host.agent.reload()),
|
reload: () => run(host.agent.reload()),
|
||||||
},
|
},
|
||||||
aisdk: {
|
aisdk: {
|
||||||
sdk: (callback) =>
|
hook: (name, callback) =>
|
||||||
register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
register(host.aisdk.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||||
language: (callback) =>
|
|
||||||
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
|
||||||
},
|
},
|
||||||
catalog: {
|
catalog: {
|
||||||
provider: {
|
provider: {
|
||||||
|
|
@ -79,11 +78,15 @@ export function fromPromise(plugin: Plugin) {
|
||||||
integration: {
|
integration: {
|
||||||
list: (input) => run(host.integration.list(input)),
|
list: (input) => run(host.integration.list(input)),
|
||||||
get: (input) => run(host.integration.get(input)),
|
get: (input) => run(host.integration.get(input)),
|
||||||
connectKey: (input) => run(host.integration.connectKey(input)),
|
connect: {
|
||||||
connectOauth: (input) => run(host.integration.connectOauth(input)),
|
key: (input) => run(host.integration.connect.key(input)),
|
||||||
attemptStatus: (input) => run(host.integration.attemptStatus(input)),
|
oauth: (input) => run(host.integration.connect.oauth(input)),
|
||||||
attemptComplete: (input) => run(host.integration.attemptComplete(input)),
|
},
|
||||||
attemptCancel: (input) => run(host.integration.attemptCancel(input)),
|
attempt: {
|
||||||
|
status: (input) => run(host.integration.attempt.status(input)),
|
||||||
|
complete: (input) => run(host.integration.attempt.complete(input)),
|
||||||
|
cancel: (input) => run(host.integration.attempt.cancel(input)),
|
||||||
|
},
|
||||||
transform: transform(host.integration),
|
transform: transform(host.integration),
|
||||||
reload: () => run(host.integration.reload()),
|
reload: () => run(host.integration.reload()),
|
||||||
connection: {
|
connection: {
|
||||||
|
|
@ -104,12 +107,19 @@ export function fromPromise(plugin: Plugin) {
|
||||||
transform: transform(host.skill),
|
transform: transform(host.skill),
|
||||||
reload: () => run(host.skill.reload()),
|
reload: () => run(host.skill.reload()),
|
||||||
},
|
},
|
||||||
|
tool: {
|
||||||
|
transform: transform(host.tool),
|
||||||
|
hook: (name, callback) =>
|
||||||
|
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||||
|
},
|
||||||
session: {
|
session: {
|
||||||
create: (input) => run(host.session.create(input)),
|
create: (input) => run(host.session.create(input)),
|
||||||
get: (input) => run(host.session.get(input)),
|
get: (input) => run(host.session.get(input)),
|
||||||
prompt: (input) => run(host.session.prompt(input)),
|
prompt: (input) => run(host.session.prompt(input)),
|
||||||
command: (input) => run(host.session.command(input)),
|
command: (input) => run(host.session.command(input)),
|
||||||
interrupt: (input) => run(host.session.interrupt(input)),
|
interrupt: (input) => run(host.session.interrupt(input)),
|
||||||
|
hook: (name, callback) =>
|
||||||
|
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
export const AlibabaPlugin = define({
|
export const AlibabaPlugin = define({
|
||||||
id: "opencode.provider.alibaba",
|
id: "opencode.provider.alibaba",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.hook(
|
||||||
|
"sdk",
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.package !== "@ai-sdk/alibaba") return
|
if (evt.package !== "@ai-sdk/alibaba") return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ export const AmazonBedrockPlugin = define({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.hook(
|
||||||
|
"sdk",
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
|
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
|
||||||
const options = { ...evt.options }
|
const options = { ...evt.options }
|
||||||
|
|
@ -108,7 +109,8 @@ export const AmazonBedrockPlugin = define({
|
||||||
evt.sdk = mod.createAmazonBedrock(options)
|
evt.sdk = mod.createAmazonBedrock(options)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.hook(
|
||||||
|
"language",
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue