mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 19:09:49 -04:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81d81e6334 | |||
| 459af775e8 | |||
| a771859362 | |||
| 31d01d404a | |||
| 814e83ffec | |||
| 4c3e65c877 | |||
| 98ea5b6e7e | |||
| 3f8c659056 | |||
| 3910a6e527 | |||
| 24892559ae | |||
| cd93533b1f | |||
| 0590452456 | |||
| 93940a1859 | |||
| 1e439b8226 | |||
| 8b2f8355b2 | |||
| aed03078f8 | |||
| c50d65b4d6 |
@@ -187,6 +187,8 @@ export async function handler(
|
||||
// Try another provider => stop retrying if using fallback provider
|
||||
if (
|
||||
res.status !== 200 &&
|
||||
// ie. 400 error is usually provider error like malformed request
|
||||
res.status !== 400 &&
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
@@ -226,7 +228,7 @@ export async function handler(
|
||||
logger.debug("STATUS: " + res.status + " " + res.statusText)
|
||||
|
||||
// Handle non-streaming response
|
||||
if (!isStream || res.status === 429) {
|
||||
if (!isStream || [400, 404, 429].includes(res.status)) {
|
||||
const json = await res.json()
|
||||
await rateLimiter?.track()
|
||||
if (json.usage) {
|
||||
@@ -238,6 +240,9 @@ export async function handler(
|
||||
await reload(billingSource, authInfo, costInfo)
|
||||
json.cost = calculateOccurredCost(billingSource, costInfo)
|
||||
}
|
||||
if (res.status === 400) {
|
||||
logger.metric({ "error.response": JSON.stringify(json) })
|
||||
}
|
||||
if (json.error?.message) {
|
||||
json.error.message = `Error from provider${providerInfo.displayName ? ` (${providerInfo.displayName})` : ""}: ${json.error.message}`
|
||||
}
|
||||
@@ -393,7 +398,7 @@ export async function handler(
|
||||
type: "error",
|
||||
error: {
|
||||
type: "error",
|
||||
message: error.message,
|
||||
message: "Internal server error",
|
||||
},
|
||||
}),
|
||||
{ status: 500 },
|
||||
|
||||
@@ -147,6 +147,17 @@ import `z` do so only for local `ZodOverride` bridges or for `z.ZodType`
|
||||
type annotations — the `export const <Info|Spec>` values are all Effect
|
||||
Schema at source.
|
||||
|
||||
A file is considered "done" when:
|
||||
|
||||
- its exported schema values (`Info`, `Input`, `Event`, `Definition`, etc.)
|
||||
are authored as Effect Schema
|
||||
- any remaining zod is either a derived compat bridge (via `zod()` /
|
||||
`zodObject()`), a `z.ZodType` type annotation, or a documented
|
||||
`ZodOverride` escape hatch — never a hand-written parallel source of truth
|
||||
|
||||
Files that meet this bar but still carry a compat bridge are checked off
|
||||
with an inline note describing the bridge and what unblocks its removal.
|
||||
|
||||
- [x] skills, formatter, console-state, mcp, lsp, permission (leaves), model-id, command, plugin, provider
|
||||
- [x] server, layout
|
||||
- [x] keybinds
|
||||
@@ -243,8 +254,8 @@ Working rule for this cluster:
|
||||
5. Errors and event payloads last
|
||||
- `NamedError.create(...)` shapes can stay temporarily if converting them to
|
||||
`Schema.TaggedErrorClass` would force unrelated churn
|
||||
- `SyncEvent.define(...)` and `BusEvent.define(...)` payloads can keep using
|
||||
derived `.zod` until the sync/bus layers are migrated
|
||||
- `SyncEvent.define(...)` and `BusEvent.define(...)` payloads can use
|
||||
derived `.zod` at remaining zod-based HTTP/OpenAPI boundaries
|
||||
|
||||
Possible later tightening after the Schema-first migration is stable:
|
||||
|
||||
@@ -263,9 +274,9 @@ Possible later tightening after the Schema-first migration is stable:
|
||||
|
||||
### Provider domain
|
||||
|
||||
- [ ] `src/provider/auth.ts`
|
||||
- [ ] `src/provider/models.ts`
|
||||
- [ ] `src/provider/provider.ts`
|
||||
- [x] `src/provider/auth.ts`
|
||||
- [x] `src/provider/models.ts`
|
||||
- [x] `src/provider/provider.ts`
|
||||
|
||||
### Tool schemas
|
||||
|
||||
@@ -273,25 +284,25 @@ Each tool declares its parameters via a zod schema. Tools are consumed by
|
||||
both the in-process runtime and the AI SDK's tool-calling layer, so the
|
||||
emitted JSON Schema must stay byte-identical.
|
||||
|
||||
- [ ] `src/tool/apply_patch.ts`
|
||||
- [ ] `src/tool/bash.ts`
|
||||
- [ ] `src/tool/codesearch.ts`
|
||||
- [ ] `src/tool/edit.ts`
|
||||
- [ ] `src/tool/glob.ts`
|
||||
- [ ] `src/tool/grep.ts`
|
||||
- [ ] `src/tool/invalid.ts`
|
||||
- [ ] `src/tool/lsp.ts`
|
||||
- [ ] `src/tool/plan.ts`
|
||||
- [ ] `src/tool/question.ts`
|
||||
- [ ] `src/tool/read.ts`
|
||||
- [ ] `src/tool/registry.ts`
|
||||
- [ ] `src/tool/skill.ts`
|
||||
- [ ] `src/tool/task.ts`
|
||||
- [ ] `src/tool/todo.ts`
|
||||
- [ ] `src/tool/tool.ts`
|
||||
- [ ] `src/tool/webfetch.ts`
|
||||
- [ ] `src/tool/websearch.ts`
|
||||
- [ ] `src/tool/write.ts`
|
||||
- [x] `src/tool/apply_patch.ts`
|
||||
- [x] `src/tool/bash.ts`
|
||||
- [x] `src/tool/codesearch.ts`
|
||||
- [x] `src/tool/edit.ts`
|
||||
- [x] `src/tool/glob.ts`
|
||||
- [x] `src/tool/grep.ts`
|
||||
- [x] `src/tool/invalid.ts`
|
||||
- [x] `src/tool/lsp.ts`
|
||||
- [x] `src/tool/plan.ts`
|
||||
- [x] `src/tool/question.ts`
|
||||
- [x] `src/tool/read.ts`
|
||||
- [x] `src/tool/registry.ts`
|
||||
- [x] `src/tool/skill.ts`
|
||||
- [x] `src/tool/task.ts`
|
||||
- [x] `src/tool/todo.ts`
|
||||
- [x] `src/tool/tool.ts`
|
||||
- [x] `src/tool/webfetch.ts`
|
||||
- [x] `src/tool/websearch.ts`
|
||||
- [x] `src/tool/write.ts`
|
||||
|
||||
### HTTP route boundaries
|
||||
|
||||
@@ -302,8 +313,8 @@ which means touching them is largely mechanical once the domain side is
|
||||
done.
|
||||
|
||||
- [ ] `src/server/error.ts`
|
||||
- [ ] `src/server/event.ts`
|
||||
- [ ] `src/server/projectors.ts`
|
||||
- [x] `src/server/event.ts`
|
||||
- [x] `src/server/projectors.ts`
|
||||
- [ ] `src/server/routes/control/index.ts`
|
||||
- [ ] `src/server/routes/control/workspace.ts`
|
||||
- [ ] `src/server/routes/global.ts`
|
||||
@@ -335,7 +346,7 @@ piecewise.
|
||||
|
||||
- [ ] `src/acp/agent.ts`
|
||||
- [ ] `src/agent/agent.ts`
|
||||
- [ ] `src/bus/bus-event.ts`
|
||||
- [x] `src/bus/bus-event.ts`
|
||||
- [ ] `src/bus/index.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui-migrate.ts`
|
||||
- [ ] `src/cli/cmd/tui/config/tui-schema.ts`
|
||||
@@ -343,9 +354,9 @@ piecewise.
|
||||
- [ ] `src/cli/cmd/tui/event.ts`
|
||||
- [ ] `src/cli/ui.ts`
|
||||
- [ ] `src/command/index.ts`
|
||||
- [ ] `src/control-plane/adaptors/worktree.ts`
|
||||
- [ ] `src/control-plane/types.ts`
|
||||
- [ ] `src/control-plane/workspace.ts`
|
||||
- [x] `src/control-plane/adaptors/worktree.ts`
|
||||
- [x] `src/control-plane/types.ts`
|
||||
- [x] `src/control-plane/workspace.ts`
|
||||
- [ ] `src/file/index.ts`
|
||||
- [ ] `src/file/ripgrep.ts`
|
||||
- [ ] `src/file/watcher.ts`
|
||||
@@ -365,7 +376,7 @@ piecewise.
|
||||
- [ ] `src/snapshot/index.ts`
|
||||
- [ ] `src/storage/db.ts`
|
||||
- [ ] `src/storage/storage.ts`
|
||||
- [ ] `src/sync/index.ts`
|
||||
- [x] `src/sync/index.ts` — public API (`SyncEvent.define`) is Schema-first; `payloads()` still derives zod for the remaining HTTP/OpenAPI boundary
|
||||
- [ ] `src/util/fn.ts`
|
||||
- [ ] `src/util/log.ts`
|
||||
- [ ] `src/util/update-schema.ts`
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import z from "zod"
|
||||
import type { ZodType } from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
|
||||
export type Definition = ReturnType<typeof define>
|
||||
export type Definition<Type extends string = string, Properties extends Schema.Top = Schema.Top> = {
|
||||
type: Type
|
||||
properties: Properties
|
||||
}
|
||||
|
||||
const registry = new Map<string, Definition>()
|
||||
|
||||
export function define<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
|
||||
const result = {
|
||||
type,
|
||||
properties,
|
||||
}
|
||||
export function define<Type extends string, Properties extends Schema.Top>(
|
||||
type: Type,
|
||||
properties: Properties,
|
||||
): Definition<Type, Properties> {
|
||||
const result = { type, properties }
|
||||
registry.set(type, result)
|
||||
return result
|
||||
}
|
||||
@@ -21,7 +25,7 @@ export function payloads() {
|
||||
return z
|
||||
.object({
|
||||
type: z.literal(type),
|
||||
properties: def.properties,
|
||||
properties: zodObject(def.properties),
|
||||
})
|
||||
.meta({
|
||||
ref: `Event.${def.type}`,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
|
||||
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema } from "effect"
|
||||
import { EffectBridge } from "@/effect"
|
||||
import { Log } from "../util"
|
||||
import { BusEvent } from "./bus-event"
|
||||
@@ -9,16 +8,18 @@ import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
const log = Log.create({ service: "bus" })
|
||||
|
||||
type BusProperties<D extends BusEvent.Definition<string, Schema.Top>> = Schema.Schema.Type<D["properties"]>
|
||||
|
||||
export const InstanceDisposed = BusEvent.define(
|
||||
"server.instance.disposed",
|
||||
z.object({
|
||||
directory: z.string(),
|
||||
Schema.Struct({
|
||||
directory: Schema.String,
|
||||
}),
|
||||
)
|
||||
|
||||
type Payload<D extends BusEvent.Definition = BusEvent.Definition> = {
|
||||
type: D["type"]
|
||||
properties: z.infer<D["properties"]>
|
||||
properties: BusProperties<D>
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -27,10 +28,7 @@ type State = {
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly publish: <D extends BusEvent.Definition>(
|
||||
def: D,
|
||||
properties: z.output<D["properties"]>,
|
||||
) => Effect.Effect<void>
|
||||
readonly publish: <D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) => Effect.Effect<void>
|
||||
readonly subscribe: <D extends BusEvent.Definition>(def: D) => Stream.Stream<Payload<D>>
|
||||
readonly subscribeAll: () => Stream.Stream<Payload>
|
||||
readonly subscribeCallback: <D extends BusEvent.Definition>(
|
||||
@@ -79,7 +77,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
}
|
||||
|
||||
function publish<D extends BusEvent.Definition>(def: D, properties: z.output<D["properties"]>) {
|
||||
function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
|
||||
return Effect.gen(function* () {
|
||||
const s = yield* InstanceState.get(state)
|
||||
const payload: Payload = { type: def.type, properties }
|
||||
@@ -175,14 +173,11 @@ const { runPromise, runSync } = makeRuntime(Service, layer)
|
||||
|
||||
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
|
||||
// Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw.
|
||||
export async function publish<D extends BusEvent.Definition>(def: D, properties: z.output<D["properties"]>) {
|
||||
export async function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
|
||||
return runPromise((svc) => svc.publish(def, properties))
|
||||
}
|
||||
|
||||
export function subscribe<D extends BusEvent.Definition>(
|
||||
def: D,
|
||||
callback: (event: { type: D["type"]; properties: z.infer<D["properties"]> }) => unknown,
|
||||
) {
|
||||
export function subscribe<D extends BusEvent.Definition>(def: D, callback: (event: Payload<D>) => unknown) {
|
||||
return runSync((svc) => svc.subscribeCallback(def, callback))
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { DialogProvider as DialogProviderList } from "@tui/component/dialog-prov
|
||||
import { ErrorComponent } from "@tui/component/error-component"
|
||||
import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
|
||||
import { ProjectProvider } from "@tui/context/project"
|
||||
import { EditorContextProvider } from "@tui/context/editor"
|
||||
import { useEvent } from "@tui/context/event"
|
||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||
import { StartupLoading } from "@tui/component/startup-loading"
|
||||
@@ -177,7 +178,9 @@ export function tui(input: {
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<App onSnapshot={input.onSnapshot} />
|
||||
<EditorContextProvider>
|
||||
<App onSnapshot={input.onSnapshot} />
|
||||
</EditorContextProvider>
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { pathToFileURL } from "bun"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import path from "path"
|
||||
import { firstBy } from "remeda"
|
||||
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useEditorContext } from "@tui/context/editor"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
@@ -77,6 +79,7 @@ export function Autocomplete(props: {
|
||||
agentStyleId: number
|
||||
promptPartTypeId: () => number
|
||||
}) {
|
||||
const editor = useEditorContext()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const command = useCommandDialog()
|
||||
@@ -221,6 +224,70 @@ export function Autocomplete(props: {
|
||||
}
|
||||
}
|
||||
|
||||
function createFilePart(item: string, lineRange?: { startLine: number; endLine?: number }) {
|
||||
const baseDir = (sync.path.directory || process.cwd()).replace(/\/+$/, "")
|
||||
const fullPath = path.isAbsolute(item) ? item : path.join(baseDir, item)
|
||||
const urlObj = pathToFileURL(fullPath)
|
||||
const filename =
|
||||
lineRange && !item.endsWith("/")
|
||||
? `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
||||
: item
|
||||
|
||||
if (lineRange && !item.endsWith("/")) {
|
||||
urlObj.searchParams.set("start", String(lineRange.startLine))
|
||||
if (lineRange.endLine !== undefined) {
|
||||
urlObj.searchParams.set("end", String(lineRange.endLine))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filename,
|
||||
url: urlObj.href,
|
||||
part: {
|
||||
type: "file" as const,
|
||||
mime: "text/plain",
|
||||
filename,
|
||||
url: urlObj.href,
|
||||
source: {
|
||||
type: "file" as const,
|
||||
text: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
value: "",
|
||||
},
|
||||
path: item,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMentionPath(filePath: string) {
|
||||
const baseDir = sync.path.directory || process.cwd()
|
||||
const absolute = path.resolve(filePath)
|
||||
const relative = path.relative(baseDir, absolute)
|
||||
|
||||
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
||||
return relative.split(path.sep).join("/")
|
||||
}
|
||||
|
||||
return absolute.split(path.sep).join("/")
|
||||
}
|
||||
|
||||
function insertFileMention(input: { filePath: string; lineStart: number; lineEnd: number }) {
|
||||
const item = normalizeMentionPath(input.filePath)
|
||||
const lineRange = {
|
||||
startLine: input.lineStart,
|
||||
endLine: input.lineEnd > input.lineStart ? input.lineEnd : undefined,
|
||||
}
|
||||
const { filename, part } = createFilePart(item, lineRange)
|
||||
const index = store.visible === "@" ? store.index : props.input().cursorOffset
|
||||
|
||||
command.keybinds(true)
|
||||
setStore("visible", false)
|
||||
setStore("index", index)
|
||||
insertPart(filename, part)
|
||||
}
|
||||
|
||||
const [files] = createResource(
|
||||
() => search(),
|
||||
async (query) => {
|
||||
@@ -250,18 +317,7 @@ export function Autocomplete(props: {
|
||||
const width = props.anchor().width - 4
|
||||
options.push(
|
||||
...sortedFiles.map((item): AutocompleteOption => {
|
||||
const baseDir = (sync.path.directory || process.cwd()).replace(/\/+$/, "")
|
||||
const fullPath = `${baseDir}/${item}`
|
||||
const urlObj = pathToFileURL(fullPath)
|
||||
let filename = item
|
||||
if (lineRange && !item.endsWith("/")) {
|
||||
filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
||||
urlObj.searchParams.set("start", String(lineRange.startLine))
|
||||
if (lineRange.endLine !== undefined) {
|
||||
urlObj.searchParams.set("end", String(lineRange.endLine))
|
||||
}
|
||||
}
|
||||
const url = urlObj.href
|
||||
const { filename, url, part } = createFilePart(item, lineRange)
|
||||
|
||||
const isDir = item.endsWith("/")
|
||||
return {
|
||||
@@ -270,21 +326,7 @@ export function Autocomplete(props: {
|
||||
isDirectory: isDir,
|
||||
path: item,
|
||||
onSelect: () => {
|
||||
insertPart(filename, {
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
filename,
|
||||
url,
|
||||
source: {
|
||||
type: "file",
|
||||
text: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
value: "",
|
||||
},
|
||||
path: item,
|
||||
},
|
||||
})
|
||||
insertPart(filename, part)
|
||||
},
|
||||
}
|
||||
}),
|
||||
@@ -501,6 +543,14 @@ export function Autocomplete(props: {
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribeMention = editor.onMention((mention) => {
|
||||
insertFileMention(mention)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
unsubscribeMention()
|
||||
})
|
||||
|
||||
props.ref({
|
||||
get visible() {
|
||||
return store.visible
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useRoute } from "@tui/context/route"
|
||||
import { useProject } from "@tui/context/project"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useEvent } from "@tui/context/event"
|
||||
import { useEditorContext } from "@tui/context/editor"
|
||||
import { MessageID, PartID } from "@/session/schema"
|
||||
import { createStore, produce, unwrap } from "solid-js/store"
|
||||
import { useKeybind } from "@tui/context/keybind"
|
||||
@@ -21,7 +22,7 @@ import { usePromptStash } from "./stash"
|
||||
import { DialogStash } from "../dialog-stash"
|
||||
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
||||
import { useCommandDialog } from "../dialog-command"
|
||||
import { useRenderer, type JSX } from "@opentui/solid"
|
||||
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import * as Editor from "@tui/util/editor"
|
||||
import { useExit } from "../../context/exit"
|
||||
import * as Clipboard from "../../util/clipboard"
|
||||
@@ -94,6 +95,7 @@ export function Prompt(props: PromptProps) {
|
||||
const local = useLocal()
|
||||
const args = useArgs()
|
||||
const sdk = useSDK()
|
||||
const editor = useEditorContext()
|
||||
const route = useRoute()
|
||||
const project = useProject()
|
||||
const sync = useSync()
|
||||
@@ -104,11 +106,34 @@ export function Prompt(props: PromptProps) {
|
||||
const stash = usePromptStash()
|
||||
const command = useCommandDialog()
|
||||
const renderer = useRenderer()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme, syntax } = useTheme()
|
||||
const kv = useKV()
|
||||
const animationsEnabled = createMemo(() => kv.get("animations_enabled", true))
|
||||
const list = createMemo(() => props.placeholders?.normal ?? [])
|
||||
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
||||
const editorPath = createMemo(() => editor.selection()?.filePath)
|
||||
const editorSelectionLabel = createMemo(() => {
|
||||
const selection = editor.selection()?.selection
|
||||
if (!selection) return
|
||||
if (selection.start.line === selection.end.line && selection.start.character === selection.end.character) return
|
||||
if (selection.start.line === selection.end.line) return `#${selection.start.line}`
|
||||
return `#${selection.start.line}-${selection.end.line}`
|
||||
})
|
||||
const editorFileLabel = createMemo(() => {
|
||||
const value = editorPath()
|
||||
if (!value) return
|
||||
const filename = path.basename(value)
|
||||
const file = /^index\.[^./]+$/.test(filename)
|
||||
? [path.basename(path.dirname(value)), filename].filter(Boolean).join("/")
|
||||
: filename
|
||||
return `${file.split(path.sep).join("/")}${editorSelectionLabel() ?? ""}`
|
||||
})
|
||||
const editorFileLabelDisplay = createMemo(() => {
|
||||
const file = editorFileLabel()
|
||||
if (!file) return
|
||||
return Locale.truncateMiddle(file, Math.max(12, Math.min(48, Math.floor(dimensions().width / 3))))
|
||||
})
|
||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
const hasRightContent = createMemo(() => Boolean(props.right))
|
||||
@@ -721,6 +746,27 @@ export function Prompt(props: PromptProps) {
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
const variant = local.model.variant.current()
|
||||
const editorSelection = editor.selection()
|
||||
const editorParts = editorSelection
|
||||
? [
|
||||
{
|
||||
id: PartID.ascending(),
|
||||
type: "text" as const,
|
||||
text: (() => {
|
||||
const start = editorSelection.selection.start
|
||||
const end = editorSelection.selection.end
|
||||
if (start.line === end.line && start.character === end.character) {
|
||||
return `Note: The user opened the file "${editorSelection.filePath}".`
|
||||
}
|
||||
if (start.line === end.line) {
|
||||
return `Note: The user selected line ${start.line} from "${editorSelection.filePath}": ${editorSelection.text}`
|
||||
}
|
||||
return `Note: The user selected lines ${start.line} to ${end.line} from "${editorSelection.filePath}": ${editorSelection.text}`
|
||||
})(),
|
||||
synthetic: true,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
if (store.mode === "shell") {
|
||||
void sdk.client.session.shell({
|
||||
@@ -773,6 +819,7 @@ export function Prompt(props: PromptProps) {
|
||||
model: selectedModel,
|
||||
variant,
|
||||
parts: [
|
||||
...editorParts,
|
||||
{
|
||||
id: PartID.ascending(),
|
||||
type: "text",
|
||||
@@ -1332,6 +1379,7 @@ export function Prompt(props: PromptProps) {
|
||||
</Show>
|
||||
<Show when={status().type !== "retry"}>
|
||||
<box gap={2} flexDirection="row">
|
||||
<Show when={editorFileLabelDisplay()}>{(file) => <text fg={theme.secondary}>{file()}</text>}</Show>
|
||||
<Switch>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<Switch>
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import z from "zod"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
const MCP_PROTOCOL_VERSION = "2025-11-25"
|
||||
|
||||
const JsonRpcMessageSchema = z.object({
|
||||
id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
method: z.string().optional(),
|
||||
params: z.unknown().optional(),
|
||||
result: z.unknown().optional(),
|
||||
error: z
|
||||
.object({
|
||||
code: z.number().optional(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const PositionSchema = z.object({
|
||||
line: z.number(),
|
||||
character: z.number(),
|
||||
})
|
||||
|
||||
const EditorSelectionSchema = z.object({
|
||||
text: z.string(),
|
||||
filePath: z.string(),
|
||||
selection: z.object({
|
||||
start: PositionSchema,
|
||||
end: PositionSchema,
|
||||
}),
|
||||
})
|
||||
|
||||
const EditorMentionSchema = z.object({
|
||||
filePath: z.string(),
|
||||
lineStart: z.number(),
|
||||
lineEnd: z.number(),
|
||||
})
|
||||
|
||||
const EditorServerInfoSchema = z.object({
|
||||
protocolVersion: z.string().optional(),
|
||||
serverInfo: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
type JsonRpcMessage = z.infer<typeof JsonRpcMessageSchema>
|
||||
export type EditorSelection = z.infer<typeof EditorSelectionSchema>
|
||||
export type EditorMention = z.infer<typeof EditorMentionSchema>
|
||||
type EditorServerInfo = z.infer<typeof EditorServerInfoSchema>
|
||||
|
||||
type EditorConnection = {
|
||||
url: string
|
||||
authToken?: string
|
||||
source: string
|
||||
}
|
||||
|
||||
type EditorLockFile = {
|
||||
port: number
|
||||
authToken?: string
|
||||
transport?: string
|
||||
workspaceFolders: string[]
|
||||
mtimeMs: number
|
||||
}
|
||||
|
||||
export const { use: useEditorContext, provider: EditorContextProvider } = createSimpleContext({
|
||||
name: "EditorContext",
|
||||
init: () => {
|
||||
const mentionListeners = new Set<(mention: EditorMention) => void>()
|
||||
const [store, setStore] = createStore<{
|
||||
status: "disabled" | "connecting" | "connected"
|
||||
selection: EditorSelection | undefined
|
||||
server: EditorServerInfo | undefined
|
||||
}>({
|
||||
status: "disabled",
|
||||
selection: undefined,
|
||||
server: undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
let socket: WebSocket | undefined
|
||||
let closed = false
|
||||
let reconnect: ReturnType<typeof setTimeout> | undefined
|
||||
let attempt = 0
|
||||
let requestID = 0
|
||||
const pending = new Map<number, string>()
|
||||
|
||||
const send = (payload: JsonRpcMessage) => {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) return
|
||||
socket.send(JSON.stringify({ jsonrpc: "2.0", ...payload }))
|
||||
}
|
||||
|
||||
const request = (method: string, params?: unknown) => {
|
||||
requestID += 1
|
||||
pending.set(requestID, method)
|
||||
send({ id: requestID, method, params })
|
||||
}
|
||||
|
||||
const scheduleReconnect = (delay: number) => {
|
||||
if (closed) return
|
||||
if (reconnect) clearTimeout(reconnect)
|
||||
reconnect = setTimeout(connect, delay)
|
||||
}
|
||||
|
||||
const connect = () => {
|
||||
if (closed) return
|
||||
|
||||
const connection = resolveEditorConnection()
|
||||
if (!connection) {
|
||||
setStore("status", "disabled")
|
||||
scheduleReconnect(1000)
|
||||
return
|
||||
}
|
||||
|
||||
setStore("status", "connecting")
|
||||
const current = openEditorSocket(connection)
|
||||
socket = current
|
||||
|
||||
current.addEventListener("open", () => {
|
||||
if (socket !== current) {
|
||||
current.close()
|
||||
return
|
||||
}
|
||||
|
||||
attempt = 0
|
||||
setStore("status", "connected")
|
||||
request("initialize", {
|
||||
protocolVersion: MCP_PROTOCOL_VERSION,
|
||||
capabilities: {},
|
||||
clientInfo: { name: "opencode", version: "0.0.0" },
|
||||
})
|
||||
})
|
||||
|
||||
current.addEventListener("message", (event) => {
|
||||
const message = parseMessage(event.data)
|
||||
if (!message) return
|
||||
|
||||
const selection =
|
||||
message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined
|
||||
if (selection?.success) {
|
||||
setStore("selection", selection.data)
|
||||
return
|
||||
}
|
||||
|
||||
const mention = message.method === "at_mentioned" ? EditorMentionSchema.safeParse(message.params) : undefined
|
||||
if (mention?.success) {
|
||||
mentionListeners.forEach((listener) => listener(mention.data))
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof message.id !== "number") return
|
||||
|
||||
const method = pending.get(message.id)
|
||||
if (!method) return
|
||||
|
||||
pending.delete(message.id)
|
||||
if (message.error) return
|
||||
|
||||
const initialize = method === "initialize" ? EditorServerInfoSchema.safeParse(message.result) : undefined
|
||||
if (initialize?.success) {
|
||||
setStore("server", initialize.data)
|
||||
send({ method: "notifications/initialized" })
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
current.addEventListener("close", () => {
|
||||
if (socket !== current) return
|
||||
|
||||
socket = undefined
|
||||
pending.clear()
|
||||
if (closed) return
|
||||
|
||||
setStore("status", "connecting")
|
||||
attempt += 1
|
||||
const delay = Math.min(1000 * 2 ** (attempt - 1), 30000)
|
||||
scheduleReconnect(delay)
|
||||
})
|
||||
}
|
||||
|
||||
scheduleReconnect(0)
|
||||
|
||||
onCleanup(() => {
|
||||
closed = true
|
||||
if (reconnect) clearTimeout(reconnect)
|
||||
socket?.close()
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
enabled() {
|
||||
return Boolean(resolveEditorConnection())
|
||||
},
|
||||
connected() {
|
||||
return store.status === "connected"
|
||||
},
|
||||
selection() {
|
||||
return store.selection
|
||||
},
|
||||
onMention(listener: (mention: EditorMention) => void) {
|
||||
mentionListeners.add(listener)
|
||||
return () => mentionListeners.delete(listener)
|
||||
},
|
||||
server() {
|
||||
return store.server
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function parsePort(value: string | undefined) {
|
||||
if (!value) return
|
||||
|
||||
const parsed = Number.parseInt(value, 10)
|
||||
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) return
|
||||
return parsed
|
||||
}
|
||||
|
||||
function resolveEditorConnection(): EditorConnection | undefined {
|
||||
const lock = resolveEditorLockFile()
|
||||
if (lock) {
|
||||
return {
|
||||
url: `ws://127.0.0.1:${lock.port}`,
|
||||
authToken: lock.authToken,
|
||||
source: `lock:${lock.port}`,
|
||||
}
|
||||
}
|
||||
|
||||
const port = parsePort(process.env.CLAUDE_CODE_SSE_PORT || process.env.OPENCODE_EDITOR_SSE_PORT)
|
||||
if (!port) return
|
||||
return {
|
||||
url: `ws://127.0.0.1:${port}`,
|
||||
source: `env:${port}`,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveEditorLockFile() {
|
||||
const directory = path.join(os.homedir(), ".claude", "ide")
|
||||
let entries: string[]
|
||||
|
||||
try {
|
||||
entries = readdirSync(directory)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const cwd = process.cwd()
|
||||
const locks = entries
|
||||
.filter((entry) => entry.endsWith(".lock"))
|
||||
.map((entry) => readEditorLockFile(path.join(directory, entry)))
|
||||
.filter((entry): entry is EditorLockFile => Boolean(entry))
|
||||
.sort((left, right) => scoreEditorLock(right, cwd) - scoreEditorLock(left, cwd))
|
||||
|
||||
return locks[0]
|
||||
}
|
||||
|
||||
function readEditorLockFile(filePath: string): EditorLockFile | undefined {
|
||||
const port = parsePort(path.basename(filePath, ".lock"))
|
||||
if (!port) return
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(filePath, "utf-8")) as unknown
|
||||
if (!isRecord(parsed)) return
|
||||
if (parsed.transport !== undefined && parsed.transport !== "ws") return
|
||||
|
||||
return {
|
||||
port,
|
||||
authToken: typeof parsed.authToken === "string" ? parsed.authToken : undefined,
|
||||
transport: typeof parsed.transport === "string" ? parsed.transport : undefined,
|
||||
workspaceFolders: Array.isArray(parsed.workspaceFolders)
|
||||
? parsed.workspaceFolders.filter((value): value is string => typeof value === "string")
|
||||
: [],
|
||||
mtimeMs: statSync(filePath).mtimeMs,
|
||||
}
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function scoreEditorLock(lock: EditorLockFile, cwd: string) {
|
||||
const workspaceMatch = lock.workspaceFolders.some((folder) => pathContains(folder, cwd)) ? 1 : 0
|
||||
return workspaceMatch * 1_000_000_000_000 + lock.mtimeMs
|
||||
}
|
||||
|
||||
function pathContains(parent: string, child: string) {
|
||||
const relative = path.relative(path.resolve(parent), path.resolve(child))
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
|
||||
}
|
||||
|
||||
function openEditorSocket(connection: EditorConnection) {
|
||||
if (!connection.authToken) return new WebSocket(connection.url)
|
||||
|
||||
return new WebSocket(connection.url, {
|
||||
headers: {
|
||||
"x-claude-code-ide-authorization": connection.authToken,
|
||||
},
|
||||
} as any)
|
||||
}
|
||||
|
||||
function parseMessage(value: unknown) {
|
||||
if (typeof value !== "string") return
|
||||
|
||||
try {
|
||||
return JsonRpcMessageSchema.parse(JSON.parse(value))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const TuiEvent = {
|
||||
PromptAppend: BusEvent.define("tui.prompt.append", z.object({ text: z.string() })),
|
||||
PromptAppend: BusEvent.define("tui.prompt.append", Schema.Struct({ text: Schema.String })),
|
||||
CommandExecute: BusEvent.define(
|
||||
"tui.command.execute",
|
||||
z.object({
|
||||
command: z.union([
|
||||
z.enum([
|
||||
Schema.Struct({
|
||||
command: Schema.Union([
|
||||
Schema.Literals([
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
@@ -26,23 +26,23 @@ export const TuiEvent = {
|
||||
"prompt.submit",
|
||||
"agent.cycle",
|
||||
]),
|
||||
z.string(),
|
||||
Schema.String,
|
||||
]),
|
||||
}),
|
||||
),
|
||||
ToastShow: BusEvent.define(
|
||||
"tui.toast.show",
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
message: z.string(),
|
||||
variant: z.enum(["info", "success", "warning", "error"]),
|
||||
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
|
||||
Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
message: Schema.String,
|
||||
variant: Schema.Literals(["info", "success", "warning", "error"]),
|
||||
duration: Schema.optional(Schema.Number).annotate({ description: "Duration in milliseconds" }),
|
||||
}),
|
||||
),
|
||||
SessionSelect: BusEvent.define(
|
||||
"tui.session.select",
|
||||
z.object({
|
||||
sessionID: SessionID.zod.describe("Session ID to navigate to"),
|
||||
Schema.Struct({
|
||||
sessionID: SessionID.annotate({ description: "Session ID to navigate to" }),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useTheme } from "@tui/context/theme"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { SplitBorder } from "../component/border"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { type TuiEvent } from "../event"
|
||||
|
||||
export type ToastOptions = z.infer<typeof TuiEvent.ToastShow.properties>
|
||||
export type ToastOptions = Schema.Schema.Type<typeof TuiEvent.ToastShow.properties>
|
||||
|
||||
export function Toast() {
|
||||
const toast = useToast()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"
|
||||
import { EffectBridge } from "@/effect"
|
||||
import type { InstanceContext } from "@/project/instance"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { Config } from "../config"
|
||||
import { MCP } from "../mcp"
|
||||
@@ -18,11 +18,11 @@ type State = {
|
||||
export const Event = {
|
||||
Executed: BusEvent.define(
|
||||
"command.executed",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
sessionID: SessionID.zod,
|
||||
arguments: z.string(),
|
||||
messageID: MessageID.zod,
|
||||
Schema.Struct({
|
||||
name: Schema.String,
|
||||
sessionID: SessionID,
|
||||
arguments: Schema.String,
|
||||
messageID: MessageID,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { Bus } from "@/bus"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { PositiveInt } from "@/util/schema"
|
||||
import { Log } from "../util"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
@@ -15,8 +16,6 @@ import { ConfigPermission } from "./permission"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
|
||||
const Color = Schema.Union([
|
||||
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "e
|
||||
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
|
||||
import { ConfigAgent } from "./agent"
|
||||
import { ConfigCommand } from "./command"
|
||||
import { ConfigFormatter } from "./formatter"
|
||||
@@ -88,9 +88,6 @@ export type Layout = ConfigLayout.Layout
|
||||
const AgentRef = Schema.Any.annotate({ [ZodOverride]: ConfigAgent.Info })
|
||||
const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level })
|
||||
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
|
||||
|
||||
// The Effect Schema is the canonical source of truth. The `.zod` compatibility
|
||||
// surface is derived so existing Hono validators keep working without a parallel
|
||||
// Zod definition.
|
||||
@@ -252,26 +249,9 @@ export const Info = Schema.Struct({
|
||||
})),
|
||||
)
|
||||
|
||||
// Schema.Struct produces readonly types by default, but the service code
|
||||
// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
|
||||
// readonly recursively so callers get the same mutable shape zod inferred.
|
||||
//
|
||||
// `Types.DeepMutable` from effect-smol would be a drop-in, but its fallback
|
||||
// branch `{ -readonly [K in keyof T]: ... }` collapses `unknown` to `{}`
|
||||
// (since `keyof unknown = never`), which widens `Record<string, unknown>`
|
||||
// fields like `ConfigPlugin.Options`. The local version gates on
|
||||
// `extends object` so `unknown` passes through.
|
||||
//
|
||||
// Tuple branch preserves `ConfigPlugin.Spec`'s `readonly [string, Options]`
|
||||
// shape (otherwise the general array branch widens it to an array).
|
||||
type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T extends readonly (infer U)[]
|
||||
? DeepMutable<U>[]
|
||||
: T extends object
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T
|
||||
|
||||
// Uses the shared `DeepMutable` from `@/util/schema`. See the definition
|
||||
// there for why the local variant is needed over `Types.DeepMutable` from
|
||||
// effect-smol (the upstream version collapses `unknown` to `{}`).
|
||||
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
|
||||
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
|
||||
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export * as ConfigPermission from "./permission"
|
||||
import { Schema, SchemaGetter } from "effect"
|
||||
import z from "zod"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
export const Action = Schema.Literals(["ask", "allow", "deny"])
|
||||
@@ -18,17 +20,9 @@ export const Rule = Schema.Union([Action, Object])
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Rule = Schema.Schema.Type<typeof Rule>
|
||||
|
||||
// Known permission keys get explicit types — most are full Rule (either a
|
||||
// single Action or a per-pattern object), but a handful of tools take no
|
||||
// sub-target patterns and are Action-only. Unknown keys fall through the
|
||||
// Record rest signature as Rule.
|
||||
//
|
||||
// StructWithRest canonicalises key order on decode (known first, then rest),
|
||||
// which used to require the `__originalKeys` preprocess hack because
|
||||
// `Permission.fromConfig` depended on the user's insertion order. That
|
||||
// dependency is gone — `fromConfig` now sorts top-level keys so wildcard
|
||||
// permissions come before specifics, making the final precedence
|
||||
// order-independent.
|
||||
// Known permission keys get explicit types in the Effect schema for generated
|
||||
// docs/types. Runtime config parsing uses `InfoZod` below so user key order is
|
||||
// preserved for permission precedence.
|
||||
const InputObject = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
read: Schema.optional(Rule),
|
||||
@@ -60,6 +54,18 @@ const InputSchema = Schema.Union([Action, InputObject])
|
||||
const normalizeInput = (input: Schema.Schema.Type<typeof InputSchema>): Schema.Schema.Type<typeof InputObject> =>
|
||||
typeof input === "string" ? { "*": input } : input
|
||||
|
||||
const ACTION_ONLY = new Set(["todowrite", "question", "webfetch", "websearch", "codesearch", "doom_loop"])
|
||||
|
||||
const InfoZod = z
|
||||
.union([zod(Action), z.record(z.string(), z.union([zod(Action), z.record(z.string(), zod(Action))]))])
|
||||
.transform(normalizeInput)
|
||||
.superRefine((input, ctx) => {
|
||||
for (const [key, value] of globalThis.Object.entries(input)) {
|
||||
if (!ACTION_ONLY.has(key) || typeof value === "string") continue
|
||||
ctx.addIssue({ code: "custom", message: `${key} must be a permission action`, path: [key] })
|
||||
}
|
||||
})
|
||||
|
||||
export const Info = InputSchema.pipe(
|
||||
Schema.decodeTo(InputObject, {
|
||||
decode: SchemaGetter.transform(normalizeInput),
|
||||
@@ -70,6 +76,7 @@ export const Info = InputSchema.pipe(
|
||||
}),
|
||||
)
|
||||
.annotate({ identifier: "PermissionConfig" })
|
||||
.annotate({ [ZodOverride]: InfoZod })
|
||||
.pipe(
|
||||
// Walker already emits the decodeTo transform into the derived zod (see
|
||||
// `encoded()` in effect-zod.ts), so just expose that directly.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
import { PositiveInt, withStatics } from "@/util/schema"
|
||||
|
||||
export const Model = Schema.Struct({
|
||||
id: Schema.optional(Schema.String),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { PositiveInt, withStatics } from "@/util/schema"
|
||||
|
||||
export const Server = Schema.Struct({
|
||||
port: Schema.optional(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))).annotate({
|
||||
port: Schema.optional(PositiveInt).annotate({
|
||||
description: "Port to listen on",
|
||||
}),
|
||||
hostname: Schema.optional(Schema.String).annotate({ description: "Hostname to listen on" }),
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { lazy } from "@/util/lazy"
|
||||
import type { ProjectID } from "@/project/schema"
|
||||
import type { WorkspaceAdaptor } from "../types"
|
||||
|
||||
export type WorkspaceAdaptorEntry = {
|
||||
type: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
import type { WorkspaceAdaptor, WorkspaceAdaptorEntry } from "../types"
|
||||
|
||||
const BUILTIN: Record<string, () => Promise<WorkspaceAdaptor>> = {
|
||||
worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
const WorktreeConfig = z.object({
|
||||
name: WorkspaceInfo.shape.name,
|
||||
branch: WorkspaceInfo.shape.branch.unwrap(),
|
||||
directory: WorkspaceInfo.shape.directory.unwrap(),
|
||||
})
|
||||
const WorktreeConfig = Schema.Struct({
|
||||
name: WorkspaceInfo.fields.name,
|
||||
branch: Schema.String,
|
||||
directory: Schema.String,
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
export const WorktreeAdaptor: WorkspaceAdaptor = {
|
||||
name: "Worktree",
|
||||
@@ -22,7 +24,7 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
|
||||
}
|
||||
},
|
||||
async create(info) {
|
||||
const config = WorktreeConfig.parse(info)
|
||||
const config = WorktreeConfig.zod.parse(info)
|
||||
await AppRuntime.runPromise(
|
||||
Worktree.Service.use((svc) =>
|
||||
svc.createFromInfo({
|
||||
@@ -34,11 +36,11 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
|
||||
)
|
||||
},
|
||||
async remove(info) {
|
||||
const config = WorktreeConfig.parse(info)
|
||||
const config = WorktreeConfig.zod.parse(info)
|
||||
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove({ directory: config.directory })))
|
||||
},
|
||||
target(info) {
|
||||
const config = WorktreeConfig.parse(info)
|
||||
const config = WorktreeConfig.zod.parse(info)
|
||||
return {
|
||||
type: "local",
|
||||
directory: config.directory,
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { type DeepMutable, withStatics } from "@/util/schema"
|
||||
|
||||
export const WorkspaceInfo = z.object({
|
||||
id: WorkspaceID.zod,
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
branch: z.string().nullable(),
|
||||
directory: z.string().nullable(),
|
||||
extra: z.unknown().nullable(),
|
||||
projectID: ProjectID.zod,
|
||||
export const WorkspaceInfo = Schema.Struct({
|
||||
id: WorkspaceID,
|
||||
type: Schema.String,
|
||||
name: Schema.String,
|
||||
branch: Schema.NullOr(Schema.String),
|
||||
directory: Schema.NullOr(Schema.String),
|
||||
extra: Schema.NullOr(Schema.Unknown),
|
||||
projectID: ProjectID,
|
||||
})
|
||||
export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
|
||||
.annotate({ identifier: "Workspace" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
|
||||
|
||||
export const WorkspaceAdaptorEntry = Schema.Struct({
|
||||
type: Schema.String,
|
||||
name: Schema.String,
|
||||
description: Schema.String,
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type WorkspaceAdaptorEntry = Schema.Schema.Type<typeof WorkspaceAdaptorEntry>
|
||||
|
||||
export type Target =
|
||||
| {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Database, asc, eq, inArray } from "@/storage"
|
||||
@@ -15,7 +15,7 @@ import { ProjectID } from "@/project/schema"
|
||||
import { Slug } from "@opencode-ai/shared/util/slug"
|
||||
import { WorkspaceTable } from "./workspace.sql"
|
||||
import { getAdaptor } from "./adaptors"
|
||||
import { WorkspaceInfo } from "./types"
|
||||
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import { parseSSE } from "./sse"
|
||||
import { Session } from "@/session"
|
||||
@@ -25,36 +25,36 @@ import { errorData } from "@/util/error"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { waitEvent } from "./util"
|
||||
import { WorkspaceContext } from "./workspace-context"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
||||
|
||||
export const Info = WorkspaceInfo.meta({
|
||||
ref: "Workspace",
|
||||
export const Info = WorkspaceInfoSchema
|
||||
export type Info = WorkspaceInfo
|
||||
|
||||
export const ConnectionStatus = Schema.Struct({
|
||||
workspaceID: WorkspaceID,
|
||||
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
export type ConnectionStatus = Schema.Schema.Type<typeof ConnectionStatus>
|
||||
|
||||
export const ConnectionStatus = z.object({
|
||||
workspaceID: WorkspaceID.zod,
|
||||
status: z.enum(["connected", "connecting", "disconnected", "error"]),
|
||||
})
|
||||
export type ConnectionStatus = z.infer<typeof ConnectionStatus>
|
||||
|
||||
const Restore = z.object({
|
||||
workspaceID: WorkspaceID.zod,
|
||||
sessionID: SessionID.zod,
|
||||
total: z.number().int().min(0),
|
||||
step: z.number().int().min(0),
|
||||
const Restore = Schema.Struct({
|
||||
workspaceID: WorkspaceID,
|
||||
sessionID: SessionID,
|
||||
total: NonNegativeInt,
|
||||
step: NonNegativeInt,
|
||||
})
|
||||
|
||||
export const Event = {
|
||||
Ready: BusEvent.define(
|
||||
"workspace.ready",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
Schema.Struct({
|
||||
name: Schema.String,
|
||||
}),
|
||||
),
|
||||
Failed: BusEvent.define(
|
||||
"workspace.failed",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
Schema.Struct({
|
||||
message: Schema.String,
|
||||
}),
|
||||
),
|
||||
Restore: BusEvent.define("workspace.restore", Restore),
|
||||
@@ -73,15 +73,16 @@ function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
|
||||
}
|
||||
}
|
||||
|
||||
const CreateInput = z.object({
|
||||
id: WorkspaceID.zod.optional(),
|
||||
type: Info.shape.type,
|
||||
branch: Info.shape.branch,
|
||||
projectID: ProjectID.zod,
|
||||
extra: Info.shape.extra,
|
||||
})
|
||||
export const CreateInput = Schema.Struct({
|
||||
id: Schema.optional(WorkspaceID),
|
||||
type: Info.fields.type,
|
||||
branch: Info.fields.branch,
|
||||
projectID: ProjectID,
|
||||
extra: Info.fields.extra,
|
||||
}).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
|
||||
export type CreateInput = Schema.Schema.Type<typeof CreateInput>
|
||||
|
||||
export const create = fn(CreateInput, async (input) => {
|
||||
export const create = fn(CreateInput.zod, async (input) => {
|
||||
const id = WorkspaceID.ascending(input.id)
|
||||
const adaptor = await getAdaptor(input.projectID, input.type)
|
||||
|
||||
@@ -137,12 +138,13 @@ export const create = fn(CreateInput, async (input) => {
|
||||
return info
|
||||
})
|
||||
|
||||
const SessionRestoreInput = z.object({
|
||||
workspaceID: WorkspaceID.zod,
|
||||
sessionID: SessionID.zod,
|
||||
})
|
||||
export const SessionRestoreInput = Schema.Struct({
|
||||
workspaceID: WorkspaceID,
|
||||
sessionID: SessionID,
|
||||
}).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
|
||||
export type SessionRestoreInput = Schema.Schema.Type<typeof SessionRestoreInput>
|
||||
|
||||
export const sessionRestore = fn(SessionRestoreInput, async (input) => {
|
||||
export const sessionRestore = fn(SessionRestoreInput.zod, async (input) => {
|
||||
log.info("session restore requested", {
|
||||
workspaceID: input.workspaceID,
|
||||
sessionID: input.sessionID,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"
|
||||
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Git } from "@/git"
|
||||
import { Effect, Layer, Context, Scope } from "effect"
|
||||
import { Effect, Layer, Context, Schema, Scope } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import fuzzysort from "fuzzysort"
|
||||
@@ -76,8 +76,8 @@ export type Content = z.infer<typeof Content>
|
||||
export const Event = {
|
||||
Edited: BusEvent.define(
|
||||
"file.edited",
|
||||
z.object({
|
||||
file: z.string(),
|
||||
Schema.Struct({
|
||||
file: Schema.String,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cause, Effect, Layer, Context } from "effect"
|
||||
import { Cause, Effect, Layer, Context, Schema } from "effect"
|
||||
// @ts-ignore
|
||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||
import type ParcelWatcher from "@parcel/watcher"
|
||||
@@ -25,9 +25,9 @@ const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||
export const Event = {
|
||||
Updated: BusEvent.define(
|
||||
"file.watcher.updated",
|
||||
z.object({
|
||||
file: z.string(),
|
||||
event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
|
||||
Schema.Struct({
|
||||
file: Schema.String,
|
||||
event: Schema.Literals(["add", "change", "unlink"]),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Log } from "../util"
|
||||
import { Process } from "@/util"
|
||||
@@ -17,8 +18,8 @@ const log = Log.create({ service: "ide" })
|
||||
export const Event = {
|
||||
Installed: BusEvent.define(
|
||||
"ide.installed",
|
||||
z.object({
|
||||
ide: z.string(),
|
||||
Schema.Struct({
|
||||
ide: Schema.String,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@ export type ReleaseType = "patch" | "minor" | "major"
|
||||
export const Event = {
|
||||
Updated: BusEvent.define(
|
||||
"installation.updated",
|
||||
z.object({
|
||||
version: z.string(),
|
||||
Schema.Struct({
|
||||
version: Schema.String,
|
||||
}),
|
||||
),
|
||||
UpdateAvailable: BusEvent.define(
|
||||
"installation.update-available",
|
||||
z.object({
|
||||
version: z.string(),
|
||||
Schema.Struct({
|
||||
version: Schema.String,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Log } from "../util"
|
||||
import { Process } from "../util"
|
||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import type * as LSPServer from "./server"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { withTimeout } from "../util/timeout"
|
||||
@@ -41,9 +42,9 @@ export const InitializeError = NamedError.create(
|
||||
export const Event = {
|
||||
Diagnostics: BusEvent.define(
|
||||
"lsp.client.diagnostics",
|
||||
z.object({
|
||||
serverID: z.string(),
|
||||
path: z.string(),
|
||||
Schema.Struct({
|
||||
serverID: Schema.String,
|
||||
path: Schema.String,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
const log = Log.create({ service: "lsp" })
|
||||
|
||||
export const Event = {
|
||||
Updated: BusEvent.define("lsp.updated", z.object({})),
|
||||
Updated: BusEvent.define("lsp.updated", Schema.Struct({})),
|
||||
}
|
||||
|
||||
const Position = Schema.Struct({
|
||||
|
||||
@@ -25,7 +25,7 @@ import { BusEvent } from "../bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||
import open from "open"
|
||||
import { Effect, Exit, Layer, Option, Context, Stream } from "effect"
|
||||
import { Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect"
|
||||
import { EffectBridge } from "@/effect"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
@@ -47,16 +47,16 @@ export type Resource = z.infer<typeof Resource>
|
||||
|
||||
export const ToolsChanged = BusEvent.define(
|
||||
"mcp.tools.changed",
|
||||
z.object({
|
||||
server: z.string(),
|
||||
Schema.Struct({
|
||||
server: Schema.String,
|
||||
}),
|
||||
)
|
||||
|
||||
export const BrowserOpenFailed = BusEvent.define(
|
||||
"mcp.browser.open.failed",
|
||||
z.object({
|
||||
mcpName: z.string(),
|
||||
url: z.string(),
|
||||
Schema.Struct({
|
||||
mcpName: Schema.String,
|
||||
url: Schema.String,
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -73,16 +73,14 @@ export class Approval extends Schema.Class<Approval>("PermissionApproval")({
|
||||
}
|
||||
|
||||
export const Event = {
|
||||
Asked: BusEvent.define("permission.asked", Request.zod),
|
||||
Asked: BusEvent.define("permission.asked", Request),
|
||||
Replied: BusEvent.define(
|
||||
"permission.replied",
|
||||
zod(
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
requestID: PermissionID,
|
||||
reply: Reply,
|
||||
}),
|
||||
),
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
requestID: PermissionID,
|
||||
reply: Reply,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Info = Schema.Struct({
|
||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
export const Event = {
|
||||
Updated: BusEvent.define("project.updated", Info.zod),
|
||||
Updated: BusEvent.define("project.updated", Info),
|
||||
}
|
||||
|
||||
type Row = typeof ProjectTable.$inferSelect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Layer, Context, Stream, Scope } from "effect"
|
||||
import { Effect, Layer, Context, Schema, Stream, Scope } from "effect"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import path from "path"
|
||||
import { Bus } from "@/bus"
|
||||
@@ -107,8 +107,8 @@ export type Mode = z.infer<typeof Mode>
|
||||
export const Event = {
|
||||
BranchUpdated: BusEvent.define(
|
||||
"vcs.branch.updated",
|
||||
z.object({
|
||||
branch: z.string().optional(),
|
||||
Schema.Struct({
|
||||
branch: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Auth } from "@/auth"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { namedSchemaError } from "@/util/named-schema-error"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { Plugin } from "../plugin"
|
||||
import { ProviderID } from "./schema"
|
||||
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
const When = Schema.Struct({
|
||||
key: Schema.String,
|
||||
@@ -70,22 +69,16 @@ export const CallbackInput = Schema.Struct({
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
|
||||
|
||||
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
|
||||
export const OauthMissing = namedSchemaError("ProviderAuthOauthMissing", { providerID: ProviderID })
|
||||
|
||||
export const OauthCodeMissing = NamedError.create(
|
||||
"ProviderAuthOauthCodeMissing",
|
||||
z.object({ providerID: ProviderID.zod }),
|
||||
)
|
||||
export const OauthCodeMissing = namedSchemaError("ProviderAuthOauthCodeMissing", { providerID: ProviderID })
|
||||
|
||||
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
|
||||
export const OauthCallbackFailed = namedSchemaError("ProviderAuthOauthCallbackFailed", {})
|
||||
|
||||
export const ValidationFailed = NamedError.create(
|
||||
"ProviderAuthValidationFailed",
|
||||
z.object({
|
||||
field: z.string(),
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
export const ValidationFailed = namedSchemaError("ProviderAuthValidationFailed", {
|
||||
field: Schema.String,
|
||||
message: Schema.String,
|
||||
})
|
||||
|
||||
export type Error =
|
||||
| Auth.AuthError
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Global } from "../global"
|
||||
import { Log } from "../util"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { Installation } from "../installation"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -21,91 +21,85 @@ const filepath = path.join(
|
||||
)
|
||||
const ttl = 5 * 60 * 1000
|
||||
|
||||
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
|
||||
|
||||
const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
|
||||
z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]),
|
||||
)
|
||||
|
||||
const Cost = z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache_read: z.number().optional(),
|
||||
cache_write: z.number().optional(),
|
||||
context_over_200k: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache_read: z.number().optional(),
|
||||
cache_write: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
const Cost = Schema.Struct({
|
||||
input: Schema.Number,
|
||||
output: Schema.Number,
|
||||
cache_read: Schema.optional(Schema.Number),
|
||||
cache_write: Schema.optional(Schema.Number),
|
||||
context_over_200k: Schema.optional(
|
||||
Schema.Struct({
|
||||
input: Schema.Number,
|
||||
output: Schema.Number,
|
||||
cache_read: Schema.optional(Schema.Number),
|
||||
cache_write: Schema.optional(Schema.Number),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const Model = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
family: z.string().optional(),
|
||||
release_date: z.string(),
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
temperature: z.boolean(),
|
||||
tool_call: z.boolean(),
|
||||
interleaved: z
|
||||
.union([
|
||||
z.literal(true),
|
||||
z
|
||||
.object({
|
||||
field: z.enum(["reasoning_content", "reasoning_details"]),
|
||||
})
|
||||
.strict(),
|
||||
])
|
||||
.optional(),
|
||||
cost: Cost.optional(),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
input: z.number().optional(),
|
||||
output: z.number(),
|
||||
export const Model = Schema.Struct({
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
family: Schema.optional(Schema.String),
|
||||
release_date: Schema.String,
|
||||
attachment: Schema.Boolean,
|
||||
reasoning: Schema.Boolean,
|
||||
temperature: Schema.Boolean,
|
||||
tool_call: Schema.Boolean,
|
||||
interleaved: Schema.optional(
|
||||
Schema.Union([
|
||||
Schema.Literal(true),
|
||||
Schema.Struct({
|
||||
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
cost: Schema.optional(Cost),
|
||||
limit: Schema.Struct({
|
||||
context: Schema.Number,
|
||||
input: Schema.optional(Schema.Number),
|
||||
output: Schema.Number,
|
||||
}),
|
||||
modalities: z
|
||||
.object({
|
||||
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z
|
||||
.object({
|
||||
modes: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
cost: Cost.optional(),
|
||||
provider: z
|
||||
.object({
|
||||
body: z.record(z.string(), JsonValue).optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
modalities: Schema.optional(
|
||||
Schema.Struct({
|
||||
input: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
|
||||
output: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
|
||||
}),
|
||||
),
|
||||
experimental: Schema.optional(
|
||||
Schema.Struct({
|
||||
modes: Schema.optional(
|
||||
Schema.Record(
|
||||
Schema.String,
|
||||
Schema.Struct({
|
||||
cost: Schema.optional(Cost),
|
||||
provider: Schema.optional(
|
||||
Schema.Struct({
|
||||
body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)),
|
||||
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])),
|
||||
provider: Schema.optional(
|
||||
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
|
||||
),
|
||||
})
|
||||
export type Model = z.infer<typeof Model>
|
||||
export type Model = Schema.Schema.Type<typeof Model>
|
||||
|
||||
export const Provider = z.object({
|
||||
api: z.string().optional(),
|
||||
name: z.string(),
|
||||
env: z.array(z.string()),
|
||||
id: z.string(),
|
||||
npm: z.string().optional(),
|
||||
models: z.record(z.string(), Model),
|
||||
export const Provider = Schema.Struct({
|
||||
api: Schema.optional(Schema.String),
|
||||
name: Schema.String,
|
||||
env: Schema.Array(Schema.String),
|
||||
id: Schema.String,
|
||||
npm: Schema.optional(Schema.String),
|
||||
models: Schema.Record(Schema.String, Model),
|
||||
})
|
||||
|
||||
export type Provider = z.infer<typeof Provider>
|
||||
export type Provider = Schema.Schema.Type<typeof Provider>
|
||||
|
||||
function url() {
|
||||
return Flag.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import z from "zod"
|
||||
import os from "os"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { Config } from "../config"
|
||||
@@ -8,7 +7,6 @@ import { Log } from "../util"
|
||||
import { Npm } from "../npm"
|
||||
import { Hash } from "@opencode-ai/shared/util/hash"
|
||||
import { Plugin } from "../plugin"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { type LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import * as ModelsDev from "./models"
|
||||
import { Auth } from "../auth"
|
||||
@@ -16,6 +14,7 @@ import { Env } from "../env"
|
||||
import { InstallationVersion } from "../installation/version"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { namedSchemaError } from "@/util/named-schema-error"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Global } from "../global"
|
||||
import path from "path"
|
||||
@@ -1047,7 +1046,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||
id: ProviderID.make(provider.id),
|
||||
source: "custom",
|
||||
name: provider.name,
|
||||
env: provider.env ?? [],
|
||||
env: [...(provider.env ?? [])],
|
||||
options: {},
|
||||
models,
|
||||
}
|
||||
@@ -1713,18 +1712,12 @@ export function parseModel(model: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export const ModelNotFoundError = NamedError.create(
|
||||
"ProviderModelNotFoundError",
|
||||
z.object({
|
||||
providerID: ProviderID.zod,
|
||||
modelID: ModelID.zod,
|
||||
suggestions: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
export const ModelNotFoundError = namedSchemaError("ProviderModelNotFoundError", {
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
||||
})
|
||||
|
||||
export const InitError = NamedError.create(
|
||||
"ProviderInitError",
|
||||
z.object({
|
||||
providerID: ProviderID.zod,
|
||||
}),
|
||||
)
|
||||
export const InitError = namedSchemaError("ProviderInitError", {
|
||||
providerID: ProviderID,
|
||||
})
|
||||
|
||||
@@ -3,13 +3,14 @@ import { Bus } from "@/bus"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { Instance } from "@/project/instance"
|
||||
import type { Proc } from "#pty"
|
||||
import z from "zod"
|
||||
import { Log } from "../util"
|
||||
import { lazy } from "@opencode-ai/shared/util/lazy"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { PtyID } from "./schema"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { Effect, Layer, Context, Schema, Types } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { EffectBridge } from "@/effect"
|
||||
|
||||
const log = Log.create({ service: "pty" })
|
||||
@@ -53,47 +54,47 @@ const meta = (cursor: number) => {
|
||||
|
||||
const pty = lazy(() => import("#pty"))
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: PtyID.zod,
|
||||
title: z.string(),
|
||||
command: z.string(),
|
||||
args: z.array(z.string()),
|
||||
cwd: z.string(),
|
||||
status: z.enum(["running", "exited"]),
|
||||
pid: z.number(),
|
||||
})
|
||||
.meta({ ref: "Pty" })
|
||||
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const CreateInput = z.object({
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
cwd: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
export const Info = Schema.Struct({
|
||||
id: PtyID,
|
||||
title: Schema.String,
|
||||
command: Schema.String,
|
||||
args: Schema.Array(Schema.String),
|
||||
cwd: Schema.String,
|
||||
status: Schema.Literals(["running", "exited"]),
|
||||
pid: Schema.Number,
|
||||
})
|
||||
.annotate({ identifier: "Pty" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
export type CreateInput = z.infer<typeof CreateInput>
|
||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
export const UpdateInput = z.object({
|
||||
title: z.string().optional(),
|
||||
size: z
|
||||
.object({
|
||||
rows: z.number(),
|
||||
cols: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
export const CreateInput = Schema.Struct({
|
||||
command: Schema.optional(Schema.String),
|
||||
args: Schema.optional(Schema.Array(Schema.String)),
|
||||
cwd: Schema.optional(Schema.String),
|
||||
title: Schema.optional(Schema.String),
|
||||
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
export type UpdateInput = z.infer<typeof UpdateInput>
|
||||
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
|
||||
|
||||
export const UpdateInput = Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
size: Schema.optional(
|
||||
Schema.Struct({
|
||||
rows: Schema.Number,
|
||||
cols: Schema.Number,
|
||||
}),
|
||||
),
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
export type UpdateInput = Types.DeepMutable<Schema.Schema.Type<typeof UpdateInput>>
|
||||
|
||||
export const Event = {
|
||||
Created: BusEvent.define("pty.created", z.object({ info: Info })),
|
||||
Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
|
||||
Exited: BusEvent.define("pty.exited", z.object({ id: PtyID.zod, exitCode: z.number() })),
|
||||
Deleted: BusEvent.define("pty.deleted", z.object({ id: PtyID.zod })),
|
||||
Created: BusEvent.define("pty.created", Schema.Struct({ info: Info })),
|
||||
Updated: BusEvent.define("pty.updated", Schema.Struct({ info: Info })),
|
||||
Exited: BusEvent.define("pty.exited", Schema.Struct({ id: PtyID, exitCode: Schema.Number })),
|
||||
Deleted: BusEvent.define("pty.deleted", Schema.Struct({ id: PtyID })),
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
|
||||
@@ -94,9 +94,9 @@ class Rejected extends Schema.Class<Rejected>("QuestionRejected")({
|
||||
}) {}
|
||||
|
||||
export const Event = {
|
||||
Asked: BusEvent.define("question.asked", Request.zod),
|
||||
Replied: BusEvent.define("question.replied", zod(Replied)),
|
||||
Rejected: BusEvent.define("question.rejected", zod(Rejected)),
|
||||
Asked: BusEvent.define("question.asked", Request),
|
||||
Replied: BusEvent.define("question.replied", Replied),
|
||||
Rejected: BusEvent.define("question.rejected", Rejected),
|
||||
}
|
||||
|
||||
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) {
|
||||
@@ -194,7 +194,7 @@ export const layer = Layer.effect(
|
||||
yield* bus.publish(Event.Replied, {
|
||||
sessionID: existing.info.sessionID,
|
||||
requestID: existing.info.id,
|
||||
answers: input.answers,
|
||||
answers: input.answers.map((a) => [...a]),
|
||||
})
|
||||
yield* Deferred.succeed(existing.deferred, input.answers)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Event = {
|
||||
Connected: BusEvent.define("server.connected", z.object({})),
|
||||
Disposed: BusEvent.define("global.disposed", z.object({})),
|
||||
Connected: BusEvent.define("server.connected", Schema.Struct({})),
|
||||
Disposed: BusEvent.define("global.disposed", Schema.Struct({})),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import z from "zod"
|
||||
import sessionProjectors from "../session/projectors"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { Session } from "@/session"
|
||||
@@ -10,7 +9,7 @@ export function initProjectors() {
|
||||
projectors: sessionProjectors,
|
||||
convertEvent: (type, data) => {
|
||||
if (type === "session.updated") {
|
||||
const id = (data as z.infer<typeof Session.Event.Updated.schema>).sessionID
|
||||
const id = (data as SyncEvent.Event<typeof Session.Event.Updated>["data"]).sessionID
|
||||
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
||||
|
||||
if (!row) return data
|
||||
|
||||
@@ -3,6 +3,8 @@ import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { listAdaptors } from "@/control-plane/adaptors"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { errors } from "../../error"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -24,15 +26,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace adaptors",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
),
|
||||
),
|
||||
schema: resolver(z.array(zodObject(WorkspaceAdaptorEntry))),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -53,7 +47,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace created",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info),
|
||||
schema: resolver(Workspace.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -62,12 +56,12 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
Workspace.create.schema.omit({
|
||||
Workspace.CreateInput.zodObject.omit({
|
||||
projectID: true,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
const body = c.req.valid("json") as Omit<Workspace.CreateInput, "projectID">
|
||||
const workspace = await Workspace.create({
|
||||
projectID: Instance.project.id,
|
||||
...body,
|
||||
@@ -86,7 +80,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspaces",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(Workspace.Info)),
|
||||
schema: resolver(z.array(Workspace.Info.zod)),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -107,7 +101,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(Workspace.ConnectionStatus)),
|
||||
schema: resolver(z.array(zodObject(Workspace.ConnectionStatus))),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -129,7 +123,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace removed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info.optional()),
|
||||
schema: resolver(Workspace.Info.zod.optional()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -139,7 +133,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: Workspace.Info.shape.id,
|
||||
id: zodObject(Workspace.Info).shape.id,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -169,11 +163,11 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ id: Workspace.Info.shape.id })),
|
||||
validator("json", Workspace.sessionRestore.schema.omit({ workspaceID: true })),
|
||||
validator("param", z.object({ id: zodObject(Workspace.Info).shape.id })),
|
||||
validator("json", Workspace.SessionRestoreInput.zodObject.omit({ workspaceID: true })),
|
||||
async (c) => {
|
||||
const { id } = c.req.valid("param")
|
||||
const body = c.req.valid("json")
|
||||
const body = c.req.valid("json") as Omit<Workspace.SessionRestoreInput, "workspaceID">
|
||||
log.info("session restore route requested", {
|
||||
workspaceID: id,
|
||||
sessionID: body.sessionID,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono, type Context } from "hono"
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import { streamSSE } from "hono/streaming"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
@@ -18,7 +18,7 @@ import { errors } from "../error"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export const GlobalDisposedEvent = BusEvent.define("global.disposed", z.object({}))
|
||||
export const GlobalDisposedEvent = BusEvent.define("global.disposed", Schema.Struct({}))
|
||||
|
||||
async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>) => () => void) {
|
||||
return streamSSE(c, async (stream) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import * as EffectZod from "@/util/effect-zod"
|
||||
import { ProviderID, ModelID } from "@/provider/schema"
|
||||
import { ToolRegistry } from "@/tool"
|
||||
import { Worktree } from "@/worktree"
|
||||
@@ -213,7 +214,7 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
tools.map((t) => ({
|
||||
id: t.id,
|
||||
description: t.description,
|
||||
parameters: z.toJSONSchema(t.parameters),
|
||||
parameters: EffectZod.toJsonSchema(t.parameters),
|
||||
})),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
description: "List of sessions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info.array()),
|
||||
schema: resolver(Pty.Info.zod.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -46,18 +46,18 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
description: "Created session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info),
|
||||
schema: resolver(Pty.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("json", Pty.CreateInput),
|
||||
validator("json", Pty.CreateInput.zod),
|
||||
async (c) =>
|
||||
jsonRequest("PtyRoutes.create", c, function* () {
|
||||
const pty = yield* Pty.Service
|
||||
return yield* pty.create(c.req.valid("json"))
|
||||
return yield* pty.create(c.req.valid("json") as Pty.CreateInput)
|
||||
}),
|
||||
)
|
||||
.get(
|
||||
@@ -71,7 +71,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
description: "Session info",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info),
|
||||
schema: resolver(Pty.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -105,7 +105,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
description: "Updated session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info),
|
||||
schema: resolver(Pty.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -113,11 +113,11 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ ptyID: PtyID.zod })),
|
||||
validator("json", Pty.UpdateInput),
|
||||
validator("json", Pty.UpdateInput.zod),
|
||||
async (c) =>
|
||||
jsonRequest("PtyRoutes.update", c, function* () {
|
||||
const pty = yield* Pty.Service
|
||||
return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
|
||||
return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json") as Pty.UpdateInput)
|
||||
}),
|
||||
)
|
||||
.delete(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Hono, type Context } from "hono"
|
||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { Bus } from "@/bus"
|
||||
import { Session } from "@/session"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
import { AsyncQueue } from "@/util/queue"
|
||||
import { errors } from "../../error"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -96,9 +99,9 @@ export const TuiRoutes = lazy(() =>
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("json", TuiEvent.PromptAppend.properties),
|
||||
validator("json", zodObject(TuiEvent.PromptAppend.properties)),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
|
||||
await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json") as { text: string })
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
@@ -305,9 +308,12 @@ export const TuiRoutes = lazy(() =>
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("json", TuiEvent.ToastShow.properties),
|
||||
validator("json", zodObject(TuiEvent.ToastShow.properties)),
|
||||
async (c) => {
|
||||
await Bus.publish(TuiEvent.ToastShow, c.req.valid("json"))
|
||||
await Bus.publish(
|
||||
TuiEvent.ToastShow,
|
||||
c.req.valid("json") as Schema.Schema.Type<typeof TuiEvent.ToastShow.properties>,
|
||||
)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
@@ -336,7 +342,7 @@ export const TuiRoutes = lazy(() =>
|
||||
return z
|
||||
.object({
|
||||
type: z.literal(def.type),
|
||||
properties: def.properties,
|
||||
properties: zodObject(def.properties),
|
||||
})
|
||||
.meta({
|
||||
ref: `Event.${def.type}`,
|
||||
@@ -345,8 +351,9 @@ export const TuiRoutes = lazy(() =>
|
||||
),
|
||||
),
|
||||
async (c) => {
|
||||
const evt = c.req.valid("json")
|
||||
await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)!, evt.properties)
|
||||
const evt = c.req.valid("json") as { type: string; properties: Record<string, unknown> }
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)! as any, evt.properties as any)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
@@ -368,9 +375,9 @@ export const TuiRoutes = lazy(() =>
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
validator("json", TuiEvent.SessionSelect.properties),
|
||||
validator("json", zodObject(TuiEvent.SessionSelect.properties)),
|
||||
async (c) => {
|
||||
const { sessionID } = c.req.valid("json")
|
||||
const { sessionID } = c.req.valid("json") as { sessionID: SessionID }
|
||||
await runRequest(
|
||||
"TuiRoutes.sessionSelect",
|
||||
c,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Plugin } from "@/plugin"
|
||||
import { Config } from "@/config"
|
||||
import { NotFoundError } from "@/storage"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { isOverflow as overflow, usable } from "./overflow"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
@@ -24,8 +24,8 @@ const log = Log.create({ service: "session.compaction" })
|
||||
export const Event = {
|
||||
Compacted: BusEvent.define(
|
||||
"session.compacted",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { Provider } from "@/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect, Schema, Types } from "effect"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { namedSchemaError } from "@/util/named-schema-error"
|
||||
import { EffectLogger } from "@/effect"
|
||||
|
||||
@@ -64,9 +64,7 @@ export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputForm
|
||||
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
|
||||
type: Schema.Literal("json_schema"),
|
||||
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
|
||||
retryCount: Schema.Number.check(Schema.isInt())
|
||||
.check(Schema.isGreaterThanOrEqualTo(0))
|
||||
.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
||||
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
@@ -138,8 +136,8 @@ export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof Reasonin
|
||||
const filePartSourceBase = {
|
||||
text: Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Number.check(Schema.isInt()),
|
||||
end: Schema.Number.check(Schema.isInt()),
|
||||
start: Schema.Int,
|
||||
end: Schema.Int,
|
||||
}).annotate({ identifier: "FilePartSourceText" }),
|
||||
}
|
||||
|
||||
@@ -157,7 +155,7 @@ export const SymbolSource = Schema.Struct({
|
||||
path: Schema.String,
|
||||
range: LSP.Range,
|
||||
name: Schema.String,
|
||||
kind: Schema.Number.check(Schema.isInt()),
|
||||
kind: Schema.Int,
|
||||
})
|
||||
.annotate({ identifier: "SymbolSource" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
@@ -196,8 +194,8 @@ export const AgentPart = Schema.Struct({
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Number.check(Schema.isInt()),
|
||||
end: Schema.Number.check(Schema.isInt()),
|
||||
start: Schema.Int,
|
||||
end: Schema.Int,
|
||||
}),
|
||||
),
|
||||
})
|
||||
@@ -501,8 +499,8 @@ export const AgentPartInput = Schema.Struct({
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Number.check(Schema.isInt()),
|
||||
end: Schema.Number.check(Schema.isInt()),
|
||||
start: Schema.Int,
|
||||
end: Schema.Int,
|
||||
}),
|
||||
),
|
||||
})
|
||||
@@ -576,54 +574,62 @@ export const Info = Object.assign(_Info, {
|
||||
})
|
||||
export type Info = User | Assistant
|
||||
|
||||
const UpdatedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
info: _Info,
|
||||
})
|
||||
|
||||
const RemovedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
})
|
||||
|
||||
const PartUpdatedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
part: _Part,
|
||||
time: Schema.Number,
|
||||
})
|
||||
|
||||
const PartRemovedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
partID: PartID,
|
||||
})
|
||||
|
||||
export const Event = {
|
||||
Updated: SyncEvent.define({
|
||||
type: "message.updated",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
info: Info.zod,
|
||||
}),
|
||||
schema: UpdatedEventSchema,
|
||||
}),
|
||||
Removed: SyncEvent.define({
|
||||
type: "message.removed",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
}),
|
||||
schema: RemovedEventSchema,
|
||||
}),
|
||||
PartUpdated: SyncEvent.define({
|
||||
type: "message.part.updated",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
part: Part.zod,
|
||||
time: z.number(),
|
||||
}),
|
||||
schema: PartUpdatedEventSchema,
|
||||
}),
|
||||
PartDelta: BusEvent.define(
|
||||
"message.part.delta",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: PartID.zod,
|
||||
field: z.string(),
|
||||
delta: z.string(),
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
partID: PartID,
|
||||
field: Schema.String,
|
||||
delta: Schema.String,
|
||||
}),
|
||||
),
|
||||
PartRemoved: SyncEvent.define({
|
||||
type: "message.part.removed",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: PartID.zod,
|
||||
}),
|
||||
schema: PartRemovedEventSchema,
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NotFoundError, eq, and } from "../storage"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import * as Session from "./session"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import "../v2/session-event"
|
||||
import { SessionTable, MessageTable, PartTable } from "./session.sql"
|
||||
import { Log } from "../util"
|
||||
|
||||
@@ -71,7 +72,7 @@ export default [
|
||||
const info = data.info
|
||||
const row = db
|
||||
.update(SessionTable)
|
||||
.set(toPartialRow(info))
|
||||
.set(toPartialRow(info as Session.Patch))
|
||||
.where(eq(SessionTable.id, data.sessionID))
|
||||
.returning()
|
||||
.get()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import z from "zod"
|
||||
import * as EffectZod from "@/util/effect-zod"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Log } from "../util"
|
||||
@@ -405,7 +406,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
providerID: input.model.providerID,
|
||||
agent: input.agent,
|
||||
})) {
|
||||
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
|
||||
const schema = ProviderTransform.schema(input.model, EffectZod.toJsonSchema(item.parameters))
|
||||
tools[item.id] = tool({
|
||||
description: item.description,
|
||||
inputSchema: jsonSchema(schema),
|
||||
@@ -794,7 +795,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
"-l",
|
||||
"-c",
|
||||
`
|
||||
__oc_cwd=$PWD
|
||||
__oc_cwd=$OPENCODE_CWD
|
||||
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
|
||||
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
|
||||
cd "$__oc_cwd"
|
||||
@@ -807,7 +808,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
"-l",
|
||||
"-c",
|
||||
`
|
||||
__oc_cwd=$PWD
|
||||
__oc_cwd=$OPENCODE_CWD
|
||||
shopt -s expand_aliases
|
||||
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
|
||||
cd "$__oc_cwd"
|
||||
@@ -832,7 +833,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
const cmd = ChildProcess.make(sh, args, {
|
||||
cwd,
|
||||
extendEnv: true,
|
||||
env: { ...shellEnv.env, TERM: "dumb" },
|
||||
env: { ...shellEnv.env, OPENCODE_CWD: cwd, TERM: "dumb" },
|
||||
stdin: "ignore",
|
||||
forceKillAfter: "3 seconds",
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ export const SessionID = Schema.String.annotate({ [ZodOverride]: Identifier.sche
|
||||
Schema.brand("SessionID"),
|
||||
withStatics((s) => ({
|
||||
descending: (id?: string) => s.make(Identifier.descending("session", id)),
|
||||
empty: () => s.make("ses_empty"),
|
||||
zod: zod(s),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ import { PartTable, SessionTable } from "./session.sql"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import { Storage } from "@/storage"
|
||||
import { Log } from "../util"
|
||||
import { updateSchema } from "../util/update-schema"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Instance } from "../project/instance"
|
||||
import { InstanceState } from "@/effect"
|
||||
@@ -28,7 +27,7 @@ import type { Provider } from "@/provider"
|
||||
import { Permission } from "@/permission"
|
||||
import { Global } from "@/global"
|
||||
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
|
||||
import { zod, zodObject } from "@/util/effect-zod"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -215,54 +214,77 @@ export const MessagesInput = Schema.Struct({
|
||||
limit: Schema.optional(Schema.Number),
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
const CreatedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
info: Info,
|
||||
})
|
||||
|
||||
const UpdatedShare = Schema.Struct({
|
||||
url: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
})
|
||||
|
||||
const UpdatedTime = Schema.Struct({
|
||||
created: Schema.optional(Schema.NullOr(Schema.Number)),
|
||||
updated: Schema.optional(Schema.NullOr(Schema.Number)),
|
||||
compacting: Schema.optional(Schema.NullOr(Schema.Number)),
|
||||
archived: Schema.optional(Schema.NullOr(Schema.Number)),
|
||||
})
|
||||
|
||||
const UpdatedInfo = Schema.Struct({
|
||||
id: Schema.optional(Schema.NullOr(SessionID)),
|
||||
slug: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
projectID: Schema.optional(Schema.NullOr(ProjectID)),
|
||||
workspaceID: Schema.optional(Schema.NullOr(WorkspaceID)),
|
||||
directory: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
parentID: Schema.optional(Schema.NullOr(SessionID)),
|
||||
summary: Schema.optional(Schema.NullOr(Summary)),
|
||||
share: Schema.optional(UpdatedShare),
|
||||
title: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
version: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
time: Schema.optional(UpdatedTime),
|
||||
permission: Schema.optional(Schema.NullOr(Permission.Ruleset)),
|
||||
revert: Schema.optional(Schema.NullOr(Revert)),
|
||||
})
|
||||
|
||||
const UpdatedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
info: UpdatedInfo,
|
||||
})
|
||||
|
||||
export const Event = {
|
||||
Created: SyncEvent.define({
|
||||
type: "session.created",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
info: Info.zod,
|
||||
}),
|
||||
schema: CreatedEventSchema,
|
||||
}),
|
||||
Updated: SyncEvent.define({
|
||||
type: "session.updated",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
info: updateSchema(zodObject(Info)).extend({
|
||||
share: updateSchema(zodObject(Share)).optional(),
|
||||
time: updateSchema(zodObject(Time)).optional(),
|
||||
}),
|
||||
}),
|
||||
busSchema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
info: Info.zod,
|
||||
}),
|
||||
schema: UpdatedEventSchema,
|
||||
busSchema: CreatedEventSchema,
|
||||
}),
|
||||
Deleted: SyncEvent.define({
|
||||
type: "session.deleted",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
info: Info.zod,
|
||||
}),
|
||||
schema: CreatedEventSchema,
|
||||
}),
|
||||
Diff: BusEvent.define(
|
||||
"session.diff",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
diff: Snapshot.FileDiff.zod.array(),
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
diff: Schema.Array(Snapshot.FileDiff),
|
||||
}),
|
||||
),
|
||||
Error: BusEvent.define(
|
||||
"session.error",
|
||||
z.object({
|
||||
sessionID: SessionID.zod.optional(),
|
||||
// z.lazy defers access to break circular dep: session → message-v2 → provider → plugin → session
|
||||
error: z.lazy(() => (MessageV2.Assistant.zod as unknown as z.ZodObject<any>).shape.error),
|
||||
Schema.Struct({
|
||||
sessionID: Schema.optional(SessionID),
|
||||
// Reuses MessageV2.Assistant.fields.error (already Schema.optional) so
|
||||
// the derived zod keeps the same discriminated-union shape on the bus.
|
||||
error: MessageV2.Assistant.fields.error,
|
||||
}),
|
||||
),
|
||||
}
|
||||
@@ -394,7 +416,7 @@ export interface Interface {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {}
|
||||
|
||||
type Patch = z.infer<typeof Event.Updated.schema>["info"]
|
||||
export type Patch = Types.DeepMutable<SyncEvent.Event<typeof Event.Updated>["data"]["info"]>
|
||||
|
||||
const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
|
||||
Effect.sync(() => Database.use(fn))
|
||||
|
||||
@@ -28,16 +28,16 @@ export type Info = Schema.Schema.Type<typeof Info>
|
||||
export const Event = {
|
||||
Status: BusEvent.define(
|
||||
"session.status",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
status: Info.zod,
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
status: Info,
|
||||
}),
|
||||
),
|
||||
// deprecated
|
||||
Idle: BusEvent.define(
|
||||
"session.idle",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ export type Info = Schema.Schema.Type<typeof Info>
|
||||
export const Event = {
|
||||
Updated: BusEvent.define(
|
||||
"todo.updated",
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
todos: z.array(Info.zod),
|
||||
Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
todos: Schema.Array(Info),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export const layer = Layer.effect(
|
||||
|
||||
yield* watch(Session.Event.Updated, (evt) =>
|
||||
Effect.gen(function* () {
|
||||
const info = yield* session.get(evt.properties.sessionID)
|
||||
const info = evt.properties.info
|
||||
yield* sync(info.id, [{ type: "session", data: info }])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import type { ZodObject } from "zod"
|
||||
import { Database, eq } from "@/storage"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Bus as ProjectBus } from "@/bus"
|
||||
@@ -9,34 +8,48 @@ import { EventSequenceTable, EventTable } from "./event.sql"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { EventID } from "./schema"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Schema as EffectSchema } from "effect"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
import type { DeepMutable } from "@/util/schema"
|
||||
|
||||
export type Definition = {
|
||||
type: string
|
||||
// Keep `Event["data"]` mutable because projectors mutate the persisted shape
|
||||
// when writing to the database. Bus payloads (`Properties`) stay readonly —
|
||||
// subscribers only read.
|
||||
|
||||
export type Definition<
|
||||
Type extends string = string,
|
||||
Schema extends EffectSchema.Top = EffectSchema.Top,
|
||||
BusSchema extends EffectSchema.Top = Schema,
|
||||
> = {
|
||||
type: Type
|
||||
version: number
|
||||
aggregate: string
|
||||
schema: z.ZodObject
|
||||
|
||||
// This is temporary and only exists for compatibility with bus
|
||||
// event definitions
|
||||
properties: z.ZodObject
|
||||
schema: Schema
|
||||
// Bus event payload schema. Defaults to `schema` unless `busSchema` was
|
||||
// passed at definition time (see `session.updated`, whose projector
|
||||
// expands the persisted data to a `{ sessionID, info }` bus payload).
|
||||
properties: BusSchema
|
||||
}
|
||||
|
||||
export type Event<Def extends Definition = Definition> = {
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: string
|
||||
data: z.infer<Def["schema"]>
|
||||
data: DeepMutable<EffectSchema.Schema.Type<Def["schema"]>>
|
||||
}
|
||||
|
||||
export type Properties<Def extends Definition = Definition> = EffectSchema.Schema.Type<Def["properties"]>
|
||||
|
||||
export type SerializedEvent<Def extends Definition = Definition> = Event<Def> & { type: string }
|
||||
|
||||
type ProjectorFunc = (db: Database.TxOrDb, data: unknown) => void
|
||||
type ConvertEvent = (type: string, data: Event["data"]) => unknown | Promise<unknown>
|
||||
|
||||
export const registry = new Map<string, Definition>()
|
||||
let projectors: Map<Definition, ProjectorFunc> | undefined
|
||||
const versions = new Map<string, number>()
|
||||
let frozen = false
|
||||
let convertEvent: (type: string, event: Event["data"]) => Promise<Record<string, unknown>> | Record<string, unknown>
|
||||
let convertEvent: ConvertEvent
|
||||
|
||||
export function reset() {
|
||||
frozen = false
|
||||
@@ -44,7 +57,7 @@ export function reset() {
|
||||
convertEvent = (_, data) => data
|
||||
}
|
||||
|
||||
export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; convertEvent?: typeof convertEvent }) {
|
||||
export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; convertEvent?: ConvertEvent }) {
|
||||
projectors = new Map(input.projectors)
|
||||
|
||||
// Install all the latest event defs to the bus. We only ever emit
|
||||
@@ -54,13 +67,13 @@ export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; co
|
||||
for (let [type, version] of versions.entries()) {
|
||||
let def = registry.get(versionedType(type, version))!
|
||||
|
||||
BusEvent.define(def.type, def.properties || def.schema)
|
||||
BusEvent.define(def.type, def.properties)
|
||||
}
|
||||
|
||||
// Freeze the system so it clearly errors if events are defined
|
||||
// after `init` which would cause bugs
|
||||
frozen = true
|
||||
convertEvent = input.convertEvent || ((_, data) => data)
|
||||
convertEvent = input.convertEvent ?? ((_, data) => data)
|
||||
}
|
||||
|
||||
export function versionedType<A extends string>(type: A): A
|
||||
@@ -72,9 +85,15 @@ export function versionedType(type: string, version?: number) {
|
||||
export function define<
|
||||
Type extends string,
|
||||
Agg extends string,
|
||||
Schema extends ZodObject<Record<Agg, z.ZodType<string>>>,
|
||||
BusSchema extends ZodObject = Schema,
|
||||
>(input: { type: Type; version: number; aggregate: Agg; schema: Schema; busSchema?: BusSchema }) {
|
||||
Schema extends EffectSchema.Top,
|
||||
BusSchema extends EffectSchema.Top = Schema,
|
||||
>(input: {
|
||||
type: Type
|
||||
version: number
|
||||
aggregate: Agg
|
||||
schema: Schema
|
||||
busSchema?: BusSchema
|
||||
}): Definition<Type, Schema, BusSchema> {
|
||||
if (frozen) {
|
||||
throw new Error("Error defining sync event: sync system has been frozen")
|
||||
}
|
||||
@@ -84,7 +103,7 @@ export function define<
|
||||
version: input.version,
|
||||
aggregate: input.aggregate,
|
||||
schema: input.schema,
|
||||
properties: input.busSchema ? input.busSchema : input.schema,
|
||||
properties: (input.busSchema ?? input.schema) as BusSchema,
|
||||
}
|
||||
|
||||
versions.set(def.type, Math.max(def.version, versions.get(def.type) || 0))
|
||||
@@ -141,12 +160,11 @@ function process<Def extends Definition>(def: Def, event: Event<Def>, options: {
|
||||
Database.effect(() => {
|
||||
if (options?.publish) {
|
||||
const result = convertEvent(def.type, event.data)
|
||||
const publish = (data: unknown) => ProjectBus.publish(def, data as Properties<Def>)
|
||||
if (result instanceof Promise) {
|
||||
void result.then((data) => {
|
||||
void ProjectBus.publish({ type: def.type, properties: def.schema }, data)
|
||||
})
|
||||
void result.then(publish)
|
||||
} else {
|
||||
void ProjectBus.publish({ type: def.type, properties: def.schema }, result)
|
||||
void publish(result)
|
||||
}
|
||||
|
||||
GlobalBus.emit("event", {
|
||||
@@ -266,7 +284,7 @@ export function payloads() {
|
||||
id: z.string(),
|
||||
seq: z.number(),
|
||||
aggregateID: z.literal(def.aggregate),
|
||||
data: def.schema,
|
||||
data: zodObject(def.schema),
|
||||
})
|
||||
.meta({
|
||||
ref: `SyncEvent.${def.type}`,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import z from "zod"
|
||||
import * as path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
import { Bus } from "../bus"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
@@ -16,8 +15,8 @@ import { File } from "../file"
|
||||
import { Format } from "../format"
|
||||
import * as Bom from "@/util/bom"
|
||||
|
||||
const PatchParams = z.object({
|
||||
patchText: z.string().describe("The full patch text that describes all changes to be made"),
|
||||
export const Parameters = Schema.Struct({
|
||||
patchText: Schema.String.annotate({ description: "The full patch text that describes all changes to be made" }),
|
||||
})
|
||||
|
||||
export const ApplyPatchTool = Tool.define(
|
||||
@@ -28,7 +27,10 @@ export const ApplyPatchTool = Tool.define(
|
||||
const format = yield* Format.Service
|
||||
const bus = yield* Bus.Service
|
||||
|
||||
const run = Effect.fn("ApplyPatchTool.execute")(function* (params: z.infer<typeof PatchParams>, ctx: Tool.Context) {
|
||||
const run = Effect.fn("ApplyPatchTool.execute")(function* (
|
||||
params: Schema.Schema.Type<typeof Parameters>,
|
||||
ctx: Tool.Context,
|
||||
) {
|
||||
if (!params.patchText) {
|
||||
return yield* Effect.fail(new Error("patchText is required"))
|
||||
}
|
||||
@@ -297,8 +299,9 @@ export const ApplyPatchTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: PatchParams,
|
||||
execute: (params: z.infer<typeof PatchParams>, ctx: Tool.Context) => run(params, ctx).pipe(Effect.orDie),
|
||||
parameters: Parameters,
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
run(params, ctx).pipe(Effect.orDie),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import os from "os"
|
||||
import { createWriteStream } from "node:fs"
|
||||
import * as Tool from "./tool"
|
||||
@@ -50,20 +50,16 @@ const FILES = new Set([
|
||||
const FLAGS = new Set(["-destination", "-literalpath", "-path"])
|
||||
const SWITCHES = new Set(["-confirm", "-debug", "-force", "-nonewline", "-recurse", "-verbose", "-whatif"])
|
||||
|
||||
const Parameters = z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
|
||||
workdir: z
|
||||
.string()
|
||||
.describe(
|
||||
`The working directory to run the command in. Defaults to the current directory. Use this instead of 'cd' commands.`,
|
||||
)
|
||||
.optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
export const Parameters = Schema.Struct({
|
||||
command: Schema.String.annotate({ description: "The command to execute" }),
|
||||
timeout: Schema.optional(Schema.Number).annotate({ description: "Optional timeout in milliseconds" }),
|
||||
workdir: Schema.optional(Schema.String).annotate({
|
||||
description: `The working directory to run the command in. Defaults to the current directory. Use this instead of 'cd' commands.`,
|
||||
}),
|
||||
description: Schema.String.annotate({
|
||||
description:
|
||||
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
type Part = {
|
||||
@@ -587,7 +583,7 @@ export const BashTool = Tool.define(
|
||||
.replaceAll("${maxLines}", String(Truncate.MAX_LINES))
|
||||
.replaceAll("${maxBytes}", String(Truncate.MAX_BYTES)),
|
||||
parameters: Parameters,
|
||||
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const cwd = params.workdir
|
||||
? yield* resolvePath(params.workdir, Instance.directory, shell)
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpClient } from "effect/unstable/http"
|
||||
import * as Tool from "./tool"
|
||||
import * as McpExa from "./mcp-exa"
|
||||
import DESCRIPTION from "./codesearch.txt"
|
||||
|
||||
export const Parameters = Schema.Struct({
|
||||
query: Schema.String.annotate({
|
||||
description:
|
||||
"Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'",
|
||||
}),
|
||||
tokensNum: Schema.Number.check(Schema.isGreaterThanOrEqualTo(1000))
|
||||
.check(Schema.isLessThanOrEqualTo(50000))
|
||||
.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(5000)))
|
||||
.annotate({
|
||||
description:
|
||||
"Number of tokens to return (1000-50000). Default is 5000 tokens. Adjust this value based on how much context you need - use lower values for focused queries and higher values for comprehensive documentation.",
|
||||
}),
|
||||
})
|
||||
|
||||
export const CodeSearchTool = Tool.define(
|
||||
"codesearch",
|
||||
Effect.gen(function* () {
|
||||
@@ -12,21 +25,7 @@ export const CodeSearchTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe(
|
||||
"Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'",
|
||||
),
|
||||
tokensNum: z
|
||||
.number()
|
||||
.min(1000)
|
||||
.max(50000)
|
||||
.default(5000)
|
||||
.describe(
|
||||
"Number of tokens to return (1000-50000). Default is 5000 tokens. Adjust this value based on how much context you need - use lower values for focused queries and higher values for comprehensive documentation.",
|
||||
),
|
||||
}),
|
||||
parameters: Parameters,
|
||||
execute: (params: { query: string; tokensNum: number }, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.ask({
|
||||
@@ -45,7 +44,7 @@ export const CodeSearchTool = Tool.define(
|
||||
McpExa.CodeArgs,
|
||||
{
|
||||
query: params.query,
|
||||
tokensNum: params.tokensNum || 5000,
|
||||
tokensNum: params.tokensNum,
|
||||
},
|
||||
"30 seconds",
|
||||
)
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
||||
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
|
||||
|
||||
import z from "zod"
|
||||
import * as path from "path"
|
||||
import { Effect, Semaphore } from "effect"
|
||||
import { Effect, Schema, Semaphore } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
import { LSP } from "../lsp"
|
||||
import { createTwoFilesPatch, diffLines } from "diff"
|
||||
@@ -45,11 +44,15 @@ function lock(filePath: string) {
|
||||
return next
|
||||
}
|
||||
|
||||
const Parameters = z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
oldString: z.string().describe("The text to replace"),
|
||||
newString: z.string().describe("The text to replace it with (must be different from oldString)"),
|
||||
replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
|
||||
export const Parameters = Schema.Struct({
|
||||
filePath: Schema.String.annotate({ description: "The absolute path to the file to modify" }),
|
||||
oldString: Schema.String.annotate({ description: "The text to replace" }),
|
||||
newString: Schema.String.annotate({
|
||||
description: "The text to replace it with (must be different from oldString)",
|
||||
}),
|
||||
replaceAll: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Replace all occurrences of oldString (default false)",
|
||||
}),
|
||||
})
|
||||
|
||||
export const EditTool = Tool.define(
|
||||
@@ -63,7 +66,7 @@ export const EditTool = Tool.define(
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: Parameters,
|
||||
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
if (!params.filePath) {
|
||||
throw new Error("filePath is required")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
@@ -9,6 +8,13 @@ import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import DESCRIPTION from "./glob.txt"
|
||||
import * as Tool from "./tool"
|
||||
|
||||
export const Parameters = Schema.Struct({
|
||||
pattern: Schema.String.annotate({ description: "The glob pattern to match files against" }),
|
||||
path: Schema.optional(Schema.String).annotate({
|
||||
description: `The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
|
||||
}),
|
||||
})
|
||||
|
||||
export const GlobTool = Tool.define(
|
||||
"glob",
|
||||
Effect.gen(function* () {
|
||||
@@ -17,15 +23,7 @@ export const GlobTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z.string().describe("The glob pattern to match files against"),
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
|
||||
),
|
||||
}),
|
||||
parameters: Parameters,
|
||||
execute: (params: { pattern: string; path?: string }, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const ins = yield* InstanceState.context
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { Effect, Option } from "effect"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
@@ -10,6 +10,16 @@ import * as Tool from "./tool"
|
||||
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
|
||||
export const Parameters = Schema.Struct({
|
||||
pattern: Schema.String.annotate({ description: "The regex pattern to search for in file contents" }),
|
||||
path: Schema.optional(Schema.String).annotate({
|
||||
description: "The directory to search in. Defaults to the current working directory.",
|
||||
}),
|
||||
include: Schema.optional(Schema.String).annotate({
|
||||
description: 'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
|
||||
}),
|
||||
})
|
||||
|
||||
export const GrepTool = Tool.define(
|
||||
"grep",
|
||||
Effect.gen(function* () {
|
||||
@@ -18,11 +28,7 @@ export const GrepTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z.string().describe("The regex pattern to search for in file contents"),
|
||||
path: z.string().optional().describe("The directory to search in. Defaults to the current working directory."),
|
||||
include: z.string().optional().describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'),
|
||||
}),
|
||||
parameters: Parameters,
|
||||
execute: (params: { pattern: string; path?: string; include?: string }, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const empty = {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
|
||||
export const Parameters = Schema.Struct({
|
||||
tool: Schema.String,
|
||||
error: Schema.String,
|
||||
})
|
||||
|
||||
export const InvalidTool = Tool.define(
|
||||
"invalid",
|
||||
Effect.succeed({
|
||||
description: "Do not use",
|
||||
parameters: z.object({
|
||||
tool: z.string(),
|
||||
error: z.string(),
|
||||
}),
|
||||
parameters: Parameters,
|
||||
execute: (params: { tool: string; error: string }) =>
|
||||
Effect.succeed({
|
||||
title: "Invalid Tool",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
import path from "path"
|
||||
import { LSP } from "../lsp"
|
||||
@@ -21,6 +20,17 @@ const operations = [
|
||||
"outgoingCalls",
|
||||
] as const
|
||||
|
||||
export const Parameters = Schema.Struct({
|
||||
operation: Schema.Literals(operations).annotate({ description: "The LSP operation to perform" }),
|
||||
filePath: Schema.String.annotate({ description: "The absolute or relative path to the file" }),
|
||||
line: Schema.Number.check(Schema.isInt())
|
||||
.check(Schema.isGreaterThanOrEqualTo(1))
|
||||
.annotate({ description: "The line number (1-based, as shown in editors)" }),
|
||||
character: Schema.Number.check(Schema.isInt())
|
||||
.check(Schema.isGreaterThanOrEqualTo(1))
|
||||
.annotate({ description: "The character offset (1-based, as shown in editors)" }),
|
||||
})
|
||||
|
||||
export const LspTool = Tool.define(
|
||||
"lsp",
|
||||
Effect.gen(function* () {
|
||||
@@ -29,12 +39,7 @@ export const LspTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
operation: z.enum(operations).describe("The LSP operation to perform"),
|
||||
filePath: z.string().describe("The absolute or relative path to the file"),
|
||||
line: z.number().int().min(1).describe("The line number (1-based, as shown in editors)"),
|
||||
character: z.number().int().min(1).describe("The character offset (1-based, as shown in editors)"),
|
||||
}),
|
||||
parameters: Parameters,
|
||||
execute: (
|
||||
args: { operation: (typeof operations)[number]; filePath: string; line: number; character: number },
|
||||
ctx: Tool.Context,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import z from "zod"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
import { Question } from "../question"
|
||||
import { Session } from "../session"
|
||||
@@ -17,6 +16,8 @@ function getLastModel(sessionID: SessionID) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const Parameters = Schema.Struct({})
|
||||
|
||||
export const PlanExitTool = Tool.define(
|
||||
"plan_exit",
|
||||
Effect.gen(function* () {
|
||||
@@ -26,7 +27,7 @@ export const PlanExitTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: EXIT_DESCRIPTION,
|
||||
parameters: z.object({}),
|
||||
parameters: Parameters,
|
||||
execute: (_params: {}, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const info = yield* session.get(ctx.sessionID)
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
import { Question } from "../question"
|
||||
import DESCRIPTION from "./question.txt"
|
||||
|
||||
const parameters = z.object({
|
||||
questions: z.array(Question.Prompt.zod).describe("Questions to ask"),
|
||||
export const Parameters = Schema.Struct({
|
||||
questions: Schema.mutable(Schema.Array(Question.Prompt)).annotate({ description: "Questions to ask" }),
|
||||
})
|
||||
|
||||
type Metadata = {
|
||||
answers: ReadonlyArray<Question.Answer>
|
||||
}
|
||||
|
||||
export const QuestionTool = Tool.define<typeof parameters, Metadata, Question.Service>(
|
||||
export const QuestionTool = Tool.define<typeof Parameters, Metadata, Question.Service>(
|
||||
"question",
|
||||
Effect.gen(function* () {
|
||||
const question = yield* Question.Service
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters,
|
||||
execute: (params: z.infer<typeof parameters>, ctx: Tool.Context<Metadata>) =>
|
||||
parameters: Parameters,
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context<Metadata>) =>
|
||||
Effect.gen(function* () {
|
||||
const answers = yield* question.ask({
|
||||
sessionID: ctx.sessionID,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Effect, Option, Scope } from "effect"
|
||||
import { Effect, Option, Schema, Scope } from "effect"
|
||||
import { createReadStream } from "fs"
|
||||
import * as path from "path"
|
||||
import { createInterface } from "readline"
|
||||
@@ -19,10 +18,19 @@ const MAX_BYTES = 50 * 1024
|
||||
const MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`
|
||||
const SAMPLE_BYTES = 4096
|
||||
|
||||
const parameters = z.object({
|
||||
filePath: z.string().describe("The absolute path to the file or directory to read"),
|
||||
offset: z.coerce.number().describe("The line number to start reading from (1-indexed)").optional(),
|
||||
limit: z.coerce.number().describe("The maximum number of lines to read (defaults to 2000)").optional(),
|
||||
// `offset` and `limit` were originally `z.coerce.number()` — the runtime
|
||||
// coercion was useful when the tool was called from a shell but serves no
|
||||
// purpose in the LLM tool-call path (the model emits typed JSON). The JSON
|
||||
// Schema output is identical (`type: "number"`), so the LLM view is
|
||||
// unchanged; purely CLI-facing uses must now send numbers rather than strings.
|
||||
export const Parameters = Schema.Struct({
|
||||
filePath: Schema.String.annotate({ description: "The absolute path to the file or directory to read" }),
|
||||
offset: Schema.optional(Schema.Number).annotate({
|
||||
description: "The line number to start reading from (1-indexed)",
|
||||
}),
|
||||
limit: Schema.optional(Schema.Number).annotate({
|
||||
description: "The maximum number of lines to read (defaults to 2000)",
|
||||
}),
|
||||
})
|
||||
|
||||
export const ReadTool = Tool.define(
|
||||
@@ -140,7 +148,10 @@ export const ReadTool = Tool.define(
|
||||
return nonPrintableCount / bytes.length > 0.3
|
||||
}
|
||||
|
||||
const run = Effect.fn("ReadTool.execute")(function* (params: z.infer<typeof parameters>, ctx: Tool.Context) {
|
||||
const run = Effect.fn("ReadTool.execute")(function* (
|
||||
params: Schema.Schema.Type<typeof Parameters>,
|
||||
ctx: Tool.Context,
|
||||
) {
|
||||
if (params.offset !== undefined && params.offset < 1) {
|
||||
return yield* Effect.fail(new Error("offset must be greater than or equal to 1"))
|
||||
}
|
||||
@@ -275,8 +286,9 @@ export const ReadTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters,
|
||||
execute: (params: z.infer<typeof parameters>, ctx: Tool.Context) => run(params, ctx).pipe(Effect.orDie),
|
||||
parameters: Parameters,
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
run(params, ctx).pipe(Effect.orDie),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -15,7 +15,9 @@ import { SkillTool } from "./skill"
|
||||
import * as Tool from "./tool"
|
||||
import { Config } from "../config"
|
||||
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { ZodOverride } from "@/util/effect-zod"
|
||||
import { Plugin } from "../plugin"
|
||||
import { Provider } from "../provider"
|
||||
import { ProviderID, type ModelID } from "../provider/schema"
|
||||
@@ -120,9 +122,17 @@ export const layer: Layer.Layer<
|
||||
const custom: Tool.Def[] = []
|
||||
|
||||
function fromPlugin(id: string, def: ToolDefinition): Tool.Def {
|
||||
// Plugin tools define their args as a raw Zod shape. Wrap the
|
||||
// derived Zod object in a `Schema.declare` so it slots into the
|
||||
// Schema-typed framework, and annotate with `ZodOverride` so the
|
||||
// walker emits the original Zod object for LLM JSON Schema.
|
||||
const zodParams = z.object(def.args)
|
||||
const parameters = Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success).annotate({
|
||||
[ZodOverride]: zodParams,
|
||||
})
|
||||
return {
|
||||
id,
|
||||
parameters: z.object(def.args),
|
||||
parameters,
|
||||
description: def.description,
|
||||
execute: (args, toolCtx) =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Skill } from "../skill"
|
||||
import * as Tool from "./tool"
|
||||
import DESCRIPTION from "./skill.txt"
|
||||
|
||||
const Parameters = z.object({
|
||||
name: z.string().describe("The name of the skill from available_skills"),
|
||||
export const Parameters = Schema.Struct({
|
||||
name: Schema.String.annotate({ description: "The name of the skill from available_skills" }),
|
||||
})
|
||||
|
||||
export const SkillTool = Tool.define(
|
||||
@@ -21,7 +20,7 @@ export const SkillTool = Tool.define(
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: Parameters,
|
||||
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const info = yield* skill.get(params.name)
|
||||
if (!info) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import * as Tool from "./tool"
|
||||
import DESCRIPTION from "./task.txt"
|
||||
import z from "zod"
|
||||
import { Session } from "../session"
|
||||
import { SessionID, MessageID } from "../session/schema"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
import { Agent } from "../agent/agent"
|
||||
import type { SessionPrompt } from "../session/prompt"
|
||||
import { Config } from "../config"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
export interface TaskPromptOps {
|
||||
cancel(sessionID: SessionID): void
|
||||
@@ -17,17 +16,15 @@ export interface TaskPromptOps {
|
||||
|
||||
const id = "task"
|
||||
|
||||
const parameters = z.object({
|
||||
description: z.string().describe("A short (3-5 words) description of the task"),
|
||||
prompt: z.string().describe("The task for the agent to perform"),
|
||||
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
|
||||
task_id: z
|
||||
.string()
|
||||
.describe(
|
||||
export const Parameters = Schema.Struct({
|
||||
description: Schema.String.annotate({ description: "A short (3-5 words) description of the task" }),
|
||||
prompt: Schema.String.annotate({ description: "The task for the agent to perform" }),
|
||||
subagent_type: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
|
||||
task_id: Schema.optional(Schema.String).annotate({
|
||||
description:
|
||||
"This should only be set if you mean to resume a previous task (you can pass a prior task_id and the task will continue the same subagent session as before instead of creating a fresh one)",
|
||||
)
|
||||
.optional(),
|
||||
command: z.string().describe("The command that triggered this task").optional(),
|
||||
}),
|
||||
command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
|
||||
})
|
||||
|
||||
export const TaskTool = Tool.define(
|
||||
@@ -37,7 +34,10 @@ export const TaskTool = Tool.define(
|
||||
const config = yield* Config.Service
|
||||
const sessions = yield* Session.Service
|
||||
|
||||
const run = Effect.fn("TaskTool.execute")(function* (params: z.infer<typeof parameters>, ctx: Tool.Context) {
|
||||
const run = Effect.fn("TaskTool.execute")(function* (
|
||||
params: Schema.Schema.Type<typeof Parameters>,
|
||||
ctx: Tool.Context,
|
||||
) {
|
||||
const cfg = yield* config.get()
|
||||
|
||||
if (!ctx.extra?.bypassAgentCheck) {
|
||||
@@ -168,8 +168,9 @@ export const TaskTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters,
|
||||
execute: (params: z.infer<typeof parameters>, ctx: Tool.Context) => run(params, ctx).pipe(Effect.orDie),
|
||||
parameters: Parameters,
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
run(params, ctx).pipe(Effect.orDie),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,39 +1,36 @@
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
import DESCRIPTION_WRITE from "./todowrite.txt"
|
||||
import { Todo } from "../session/todo"
|
||||
|
||||
// Parameters are kept inline rather than derived from Todo.Info because
|
||||
// Tool.define requires z.ZodObject-typed parameters for execute() inference,
|
||||
// and zodObject(Todo.Info) returns ZodObject<any> — reaching into .shape would
|
||||
// erase field types. Tool schemas migrate to Effect Schema as a separate slice
|
||||
// per specs/effect/schema.md.
|
||||
const parameters = z.object({
|
||||
todos: z
|
||||
.array(
|
||||
z.object({
|
||||
content: z.string().describe("Brief description of the task"),
|
||||
status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
|
||||
priority: z.string().describe("Priority level of the task: high, medium, low"),
|
||||
}),
|
||||
)
|
||||
.describe("The updated todo list"),
|
||||
// Todo.Info is still a zod schema (session/todo.ts). Inline the field shape
|
||||
// here rather than referencing its `.shape` — the LLM-visible JSON Schema is
|
||||
// identical, and it removes the last zod dependency from this tool.
|
||||
const TodoItem = Schema.Struct({
|
||||
content: Schema.String.annotate({ description: "Brief description of the task" }),
|
||||
status: Schema.String.annotate({
|
||||
description: "Current status of the task: pending, in_progress, completed, cancelled",
|
||||
}),
|
||||
priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
|
||||
})
|
||||
|
||||
export const Parameters = Schema.Struct({
|
||||
todos: Schema.mutable(Schema.Array(TodoItem)).annotate({ description: "The updated todo list" }),
|
||||
})
|
||||
|
||||
type Metadata = {
|
||||
todos: Todo.Info[]
|
||||
}
|
||||
|
||||
export const TodoWriteTool = Tool.define<typeof parameters, Metadata, Todo.Service>(
|
||||
export const TodoWriteTool = Tool.define<typeof Parameters, Metadata, Todo.Service>(
|
||||
"todowrite",
|
||||
Effect.gen(function* () {
|
||||
const todo = yield* Todo.Service
|
||||
|
||||
return {
|
||||
description: DESCRIPTION_WRITE,
|
||||
parameters,
|
||||
execute: (params: z.infer<typeof parameters>, ctx: Tool.Context<Metadata>) =>
|
||||
parameters: Parameters,
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context<Metadata>) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.ask({
|
||||
permission: "todowrite",
|
||||
@@ -55,6 +52,6 @@ export const TodoWriteTool = Tool.define<typeof parameters, Metadata, Todo.Servi
|
||||
},
|
||||
}
|
||||
}),
|
||||
} satisfies Tool.DefWithoutID<typeof parameters, Metadata>
|
||||
} satisfies Tool.DefWithoutID<typeof Parameters, Metadata>
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { MessageV2 } from "../session/message-v2"
|
||||
import type { Permission } from "../permission"
|
||||
import type { SessionID, MessageID } from "../session/schema"
|
||||
@@ -32,29 +31,39 @@ export interface ExecuteResult<M extends Metadata = Metadata> {
|
||||
attachments?: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[]
|
||||
}
|
||||
|
||||
export interface Def<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
|
||||
export interface Def<
|
||||
Parameters extends Schema.Decoder<unknown> = Schema.Decoder<unknown>,
|
||||
M extends Metadata = Metadata,
|
||||
> {
|
||||
id: string
|
||||
description: string
|
||||
parameters: Parameters
|
||||
execute(args: z.infer<Parameters>, ctx: Context): Effect.Effect<ExecuteResult<M>>
|
||||
formatValidationError?(error: z.ZodError): string
|
||||
execute(args: Schema.Schema.Type<Parameters>, ctx: Context): Effect.Effect<ExecuteResult<M>>
|
||||
formatValidationError?(error: unknown): string
|
||||
}
|
||||
export type DefWithoutID<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> = Omit<
|
||||
Def<Parameters, M>,
|
||||
"id"
|
||||
>
|
||||
export type DefWithoutID<
|
||||
Parameters extends Schema.Decoder<unknown> = Schema.Decoder<unknown>,
|
||||
M extends Metadata = Metadata,
|
||||
> = Omit<Def<Parameters, M>, "id">
|
||||
|
||||
export interface Info<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
|
||||
export interface Info<
|
||||
Parameters extends Schema.Decoder<unknown> = Schema.Decoder<unknown>,
|
||||
M extends Metadata = Metadata,
|
||||
> {
|
||||
id: string
|
||||
init: () => Effect.Effect<DefWithoutID<Parameters, M>>
|
||||
}
|
||||
|
||||
type Init<Parameters extends z.ZodType, M extends Metadata> =
|
||||
type Init<Parameters extends Schema.Decoder<unknown>, M extends Metadata> =
|
||||
| DefWithoutID<Parameters, M>
|
||||
| (() => Effect.Effect<DefWithoutID<Parameters, M>>)
|
||||
|
||||
export type InferParameters<T> =
|
||||
T extends Info<infer P, any> ? z.infer<P> : T extends Effect.Effect<Info<infer P, any>, any, any> ? z.infer<P> : never
|
||||
T extends Info<infer P, any>
|
||||
? Schema.Schema.Type<P>
|
||||
: T extends Effect.Effect<Info<infer P, any>, any, any>
|
||||
? Schema.Schema.Type<P>
|
||||
: never
|
||||
export type InferMetadata<T> =
|
||||
T extends Info<any, infer M> ? M : T extends Effect.Effect<Info<any, infer M>, any, any> ? M : never
|
||||
|
||||
@@ -65,7 +74,7 @@ export type InferDef<T> =
|
||||
? Def<P, M>
|
||||
: never
|
||||
|
||||
function wrap<Parameters extends z.ZodType, Result extends Metadata>(
|
||||
function wrap<Parameters extends Schema.Decoder<unknown>, Result extends Metadata>(
|
||||
id: string,
|
||||
init: Init<Parameters, Result>,
|
||||
truncate: Truncate.Interface,
|
||||
@@ -74,6 +83,10 @@ function wrap<Parameters extends z.ZodType, Result extends Metadata>(
|
||||
return () =>
|
||||
Effect.gen(function* () {
|
||||
const toolInfo = typeof init === "function" ? { ...(yield* init()) } : { ...init }
|
||||
// Compile the parser closure once per tool init; `decodeUnknownEffect`
|
||||
// allocates a new closure per call, so hoisting avoids re-closing it for
|
||||
// every LLM tool invocation.
|
||||
const decode = Schema.decodeUnknownEffect(toolInfo.parameters)
|
||||
const execute = toolInfo.execute
|
||||
toolInfo.execute = (args, ctx) => {
|
||||
const attrs = {
|
||||
@@ -83,19 +96,17 @@ function wrap<Parameters extends z.ZodType, Result extends Metadata>(
|
||||
...(ctx.callID ? { "tool.call_id": ctx.callID } : {}),
|
||||
}
|
||||
return Effect.gen(function* () {
|
||||
yield* Effect.try({
|
||||
try: () => toolInfo.parameters.parse(args),
|
||||
catch: (error) => {
|
||||
if (error instanceof z.ZodError && toolInfo.formatValidationError) {
|
||||
return new Error(toolInfo.formatValidationError(error), { cause: error })
|
||||
}
|
||||
return new Error(
|
||||
`The ${id} tool was called with invalid arguments: ${error}.\nPlease rewrite the input so it satisfies the expected schema.`,
|
||||
{ cause: error },
|
||||
)
|
||||
},
|
||||
})
|
||||
const result = yield* execute(args, ctx)
|
||||
const decoded = yield* decode(args).pipe(
|
||||
Effect.mapError((error) =>
|
||||
toolInfo.formatValidationError
|
||||
? new Error(toolInfo.formatValidationError(error), { cause: error })
|
||||
: new Error(
|
||||
`The ${id} tool was called with invalid arguments: ${error}.\nPlease rewrite the input so it satisfies the expected schema.`,
|
||||
{ cause: error },
|
||||
),
|
||||
),
|
||||
)
|
||||
const result = yield* execute(decoded as Schema.Schema.Type<Parameters>, ctx)
|
||||
if (result.metadata.truncated !== undefined) {
|
||||
return result
|
||||
}
|
||||
@@ -116,7 +127,12 @@ function wrap<Parameters extends z.ZodType, Result extends Metadata>(
|
||||
})
|
||||
}
|
||||
|
||||
export function define<Parameters extends z.ZodType, Result extends Metadata, R, ID extends string = string>(
|
||||
export function define<
|
||||
Parameters extends Schema.Decoder<unknown>,
|
||||
Result extends Metadata,
|
||||
R,
|
||||
ID extends string = string,
|
||||
>(
|
||||
id: ID,
|
||||
init: Effect.Effect<Init<Parameters, Result>, never, R>,
|
||||
): Effect.Effect<Info<Parameters, Result>, never, R | Truncate.Service | Agent.Service> & { id: ID } {
|
||||
@@ -131,7 +147,9 @@ export function define<Parameters extends z.ZodType, Result extends Metadata, R,
|
||||
)
|
||||
}
|
||||
|
||||
export function init<P extends z.ZodType, M extends Metadata>(info: Info<P, M>): Effect.Effect<Def<P, M>> {
|
||||
export function init<P extends Schema.Decoder<unknown>, M extends Metadata>(
|
||||
info: Info<P, M>,
|
||||
): Effect.Effect<Def<P, M>> {
|
||||
return Effect.gen(function* () {
|
||||
const init = yield* info.init()
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import * as Tool from "./tool"
|
||||
import TurndownService from "turndown"
|
||||
@@ -10,13 +9,14 @@ const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
|
||||
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
|
||||
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
||||
|
||||
const parameters = z.object({
|
||||
url: z.string().describe("The URL to fetch content from"),
|
||||
format: z
|
||||
.enum(["text", "markdown", "html"])
|
||||
.default("markdown")
|
||||
.describe("The format to return the content in (text, markdown, or html). Defaults to markdown."),
|
||||
timeout: z.number().describe("Optional timeout in seconds (max 120)").optional(),
|
||||
export const Parameters = Schema.Struct({
|
||||
url: Schema.String.annotate({ description: "The URL to fetch content from" }),
|
||||
format: Schema.Literals(["text", "markdown", "html"])
|
||||
.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("markdown" as const)))
|
||||
.annotate({
|
||||
description: "The format to return the content in (text, markdown, or html). Defaults to markdown.",
|
||||
}),
|
||||
timeout: Schema.optional(Schema.Number).annotate({ description: "Optional timeout in seconds (max 120)" }),
|
||||
})
|
||||
|
||||
export const WebFetchTool = Tool.define(
|
||||
@@ -27,8 +27,8 @@ export const WebFetchTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters,
|
||||
execute: (params: z.infer<typeof parameters>, ctx: Tool.Context) =>
|
||||
parameters: Parameters,
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
if (!params.url.startsWith("http://") && !params.url.startsWith("https://")) {
|
||||
throw new Error("URL must start with http:// or https://")
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpClient } from "effect/unstable/http"
|
||||
import * as Tool from "./tool"
|
||||
import * as McpExa from "./mcp-exa"
|
||||
import DESCRIPTION from "./websearch.txt"
|
||||
|
||||
const Parameters = z.object({
|
||||
query: z.string().describe("Websearch query"),
|
||||
numResults: z.number().optional().describe("Number of search results to return (default: 8)"),
|
||||
livecrawl: z
|
||||
.enum(["fallback", "preferred"])
|
||||
.optional()
|
||||
.describe(
|
||||
export const Parameters = Schema.Struct({
|
||||
query: Schema.String.annotate({ description: "Websearch query" }),
|
||||
numResults: Schema.optional(Schema.Number).annotate({
|
||||
description: "Number of search results to return (default: 8)",
|
||||
}),
|
||||
livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({
|
||||
description:
|
||||
"Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
|
||||
),
|
||||
type: z
|
||||
.enum(["auto", "fast", "deep"])
|
||||
.optional()
|
||||
.describe("Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search"),
|
||||
contextMaxCharacters: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Maximum characters for context string optimized for LLMs (default: 10000)"),
|
||||
}),
|
||||
type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({
|
||||
description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
|
||||
}),
|
||||
contextMaxCharacters: Schema.optional(Schema.Number).annotate({
|
||||
description: "Maximum characters for context string optimized for LLMs (default: 10000)",
|
||||
}),
|
||||
})
|
||||
|
||||
export const WebSearchTool = Tool.define(
|
||||
@@ -34,7 +31,7 @@ export const WebSearchTool = Tool.define(
|
||||
return DESCRIPTION.replace("{{year}}", new Date().getFullYear().toString())
|
||||
},
|
||||
parameters: Parameters,
|
||||
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.ask({
|
||||
permission: "websearch",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import * as path from "path"
|
||||
import { Effect } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
@@ -17,6 +17,13 @@ import * as Bom from "@/util/bom"
|
||||
|
||||
const MAX_PROJECT_DIAGNOSTICS_FILES = 5
|
||||
|
||||
export const Parameters = Schema.Struct({
|
||||
content: Schema.String.annotate({ description: "The content to write to the file" }),
|
||||
filePath: Schema.String.annotate({
|
||||
description: "The absolute path to the file to write (must be absolute, not relative)",
|
||||
}),
|
||||
})
|
||||
|
||||
export const WriteTool = Tool.define(
|
||||
"write",
|
||||
Effect.gen(function* () {
|
||||
@@ -27,10 +34,7 @@ export const WriteTool = Tool.define(
|
||||
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
content: z.string().describe("The content to write to the file"),
|
||||
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
|
||||
}),
|
||||
parameters: Parameters,
|
||||
execute: (params: { content: string; filePath: string }, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const filepath = path.isAbsolute(params.filePath)
|
||||
|
||||
@@ -49,6 +49,16 @@ function isZodType(value: unknown): value is z.ZodTypeAny {
|
||||
return typeof value === "object" && value !== null && "_zod" in value
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a JSON Schema for a tool/route parameter schema — derives the zod form
|
||||
* via the walker so Effect Schema inputs flow through the same zod-openapi
|
||||
* pipeline the LLM/SDK layer already depends on. `io: "input"` mirrors what
|
||||
* `session/prompt.ts` has always passed to `ai`'s `jsonSchema()` helper.
|
||||
*/
|
||||
export function toJsonSchema<S extends Schema.Top>(schema: S) {
|
||||
return z.toJSONSchema(zod(schema), { io: "input" })
|
||||
}
|
||||
|
||||
function walk(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
const cached = walkCache.get(ast)
|
||||
if (cached) return cached
|
||||
@@ -59,8 +69,17 @@ function walk(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
|
||||
function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
const override = (ast.annotations as any)?.[ZodOverride] as z.ZodTypeAny | undefined
|
||||
if (override) return override
|
||||
// `description` annotations layer on top of an override so callers can
|
||||
// reuse a shared override schema (e.g. `SessionID`) and still add a
|
||||
// per-field description on the outer wrapper.
|
||||
const base = override ?? bodyWithChecks(ast)
|
||||
const desc = SchemaAST.resolveDescription(ast)
|
||||
const ref = SchemaAST.resolveIdentifier(ast)
|
||||
const described = desc ? base.describe(desc) : base
|
||||
return ref ? described.meta({ ref }) : described
|
||||
}
|
||||
|
||||
function bodyWithChecks(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
// Schema.Class wraps its fields in a Declaration AST plus an encoding that
|
||||
// constructs the class instance. For the Zod derivation we want the plain
|
||||
// field shape (the decoded/consumer view), not the class instance — so
|
||||
@@ -74,11 +93,7 @@ function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
const hasEncoding = ast.encoding?.length && ast._tag !== "Declaration"
|
||||
const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined)
|
||||
const base = hasTransform ? encoded(ast) : body(ast)
|
||||
const checked = ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
||||
const desc = SchemaAST.resolveDescription(ast)
|
||||
const ref = SchemaAST.resolveIdentifier(ast)
|
||||
const described = desc ? checked.describe(desc) : checked
|
||||
return ref ? described.meta({ ref }) : described
|
||||
return ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
||||
}
|
||||
|
||||
// Walk the encoded side and apply each link's decode to produce the decoded
|
||||
@@ -345,6 +360,8 @@ function array(ast: SchemaAST.Arrays): z.ZodTypeAny {
|
||||
}
|
||||
|
||||
function decl(ast: SchemaAST.Declaration): z.ZodTypeAny {
|
||||
const typeConstructor = (ast.annotations as { typeConstructor?: { _tag?: string } }).typeConstructor
|
||||
if (typeConstructor?._tag === "effect/DateTime.Utc") return z.string().datetime()
|
||||
if (ast.typeParameters.length !== 1) return fail(ast)
|
||||
return walk(ast.typeParameters[0])
|
||||
}
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
/**
|
||||
* Integer greater than zero.
|
||||
*/
|
||||
export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0))
|
||||
|
||||
/**
|
||||
* Integer greater than or equal to zero.
|
||||
*/
|
||||
export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
||||
|
||||
/**
|
||||
* Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable`
|
||||
* until `effect:core/x228my` ("Types.DeepMutable widens unknown to `{}`") lands.
|
||||
*
|
||||
* The upstream version falls through `unknown` into `{ -readonly [K in keyof T]: ... }`
|
||||
* where `keyof unknown = never`, so `unknown` collapses to `{}`. This local
|
||||
* version gates the object branch on `extends object` (which `unknown` does
|
||||
* not) so `unknown` passes through untouched.
|
||||
*
|
||||
* Primitive bailout matches upstream — without it, branded strings like
|
||||
* `string & Brand<"SessionID">` fall into the object branch and get their
|
||||
* prototype methods walked.
|
||||
*
|
||||
* Tuple branch preserves readonly tuples (e.g. `ConfigPlugin.Spec`'s
|
||||
* `readonly [string, Options]`); the general array branch would otherwise
|
||||
* widen them to unbounded arrays.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type DeepMutable<T> = T extends string | number | boolean | bigint | symbol | Function
|
||||
? T
|
||||
: T extends readonly [unknown, ...unknown[]]
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T extends readonly (infer U)[]
|
||||
? DeepMutable<U>[]
|
||||
: T extends object
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T
|
||||
|
||||
/**
|
||||
* Attach static methods to a schema object. Designed to be used with `.pipe()`:
|
||||
*
|
||||
@@ -16,13 +54,16 @@ export const withStatics =
|
||||
(schema: S): S & M =>
|
||||
Object.assign(schema, methods(schema))
|
||||
|
||||
declare const NewtypeBrand: unique symbol
|
||||
type NewtypeBrand<Tag extends string> = { readonly [NewtypeBrand]: Tag }
|
||||
|
||||
/**
|
||||
* Nominal wrapper for scalar types. The class itself is a valid schema —
|
||||
* pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc.
|
||||
*
|
||||
* Overrides `~type.make` on the derived `Schema.Opaque` so `Schema.Schema.Type`
|
||||
* of a field using this newtype resolves to `Self` rather than the underlying
|
||||
* branded phantom. Without that override, passing a class instance to code
|
||||
* typed against `Schema.Schema.Type<FieldSchema>` would require a cast even
|
||||
* though the values are structurally equivalent at runtime.
|
||||
*
|
||||
* @example
|
||||
* class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {
|
||||
* static make(id: string): QuestionID {
|
||||
@@ -34,10 +75,8 @@ type NewtypeBrand<Tag extends string> = { readonly [NewtypeBrand]: Tag }
|
||||
*/
|
||||
export function Newtype<Self>() {
|
||||
return <const Tag extends string, S extends Schema.Top>(tag: Tag, schema: S) => {
|
||||
type Branded = NewtypeBrand<Tag>
|
||||
|
||||
abstract class Base {
|
||||
declare readonly [NewtypeBrand]: Tag
|
||||
declare readonly _newtype: Tag
|
||||
|
||||
static make(value: Schema.Schema.Type<S>): Self {
|
||||
return value as unknown as Self
|
||||
@@ -46,8 +85,10 @@ export function Newtype<Self>() {
|
||||
|
||||
Object.setPrototypeOf(Base, schema)
|
||||
|
||||
return Base as unknown as (abstract new (_: never) => Branded) & {
|
||||
return Base as unknown as (abstract new (_: never) => { readonly _newtype: Tag }) & {
|
||||
readonly make: (value: Schema.Schema.Type<S>) => Self
|
||||
} & Omit<Schema.Opaque<Self, S, {}>, "make">
|
||||
} & Omit<Schema.Opaque<Self, S, {}>, "make" | "~type.make"> & {
|
||||
readonly "~type.make": Self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +1,132 @@
|
||||
import { Identifier } from "@/id/id"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { Schema } from "effect"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
|
||||
export namespace SessionEvent {
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Session.Event.ID"),
|
||||
withStatics((s) => ({
|
||||
create: () => s.make(Identifier.create("evt", "ascending")),
|
||||
})),
|
||||
)
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
type Stamp = Schema.Schema.Type<typeof Schema.DateTimeUtc>
|
||||
type BaseInput = {
|
||||
id?: ID
|
||||
metadata?: Record<string, unknown>
|
||||
timestamp?: Stamp
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Session.Event.ID"),
|
||||
withStatics((s) => ({
|
||||
create: () => s.make(Identifier.create("evt", "ascending")),
|
||||
})),
|
||||
)
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
type Stamp = Schema.Schema.Type<typeof Schema.DateTimeUtc>
|
||||
type BaseInput = {
|
||||
id?: ID
|
||||
sessionID: SessionID
|
||||
metadata?: Record<string, unknown>
|
||||
timestamp?: Stamp
|
||||
}
|
||||
|
||||
function defineEvent<Self>(identifier: string) {
|
||||
return <const Type extends string, Fields extends Schema.Struct.Fields>(input: {
|
||||
type: Type
|
||||
schema: Fields
|
||||
version?: number
|
||||
}) => {
|
||||
const RawEvent = Schema.Class<Self>(identifier)({
|
||||
id: ID,
|
||||
sessionID: SessionID,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
timestamp: Schema.DateTimeUtc,
|
||||
type: Schema.Literal(input.type),
|
||||
...input.schema,
|
||||
})
|
||||
const Event = RawEvent as Exclude<typeof RawEvent, string>
|
||||
|
||||
const Sync = SyncEvent.define({
|
||||
type: input.type,
|
||||
version: input.version ?? 1,
|
||||
aggregate: "sessionID",
|
||||
schema: Event,
|
||||
})
|
||||
|
||||
return Object.assign(Event, {
|
||||
Sync,
|
||||
create(value: BaseInput & Record<string, unknown>) {
|
||||
return new (Event as unknown as new (value: Record<string, unknown>) => Self)({
|
||||
...value,
|
||||
id: value.id ?? ID.create(),
|
||||
sessionID: value.sessionID,
|
||||
timestamp: value.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
type: input.type,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const Base = {
|
||||
id: ID,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
timestamp: Schema.DateTimeUtc,
|
||||
export class Source extends Schema.Class<Source>("Session.Event.Source")({
|
||||
start: Schema.Number,
|
||||
end: Schema.Number,
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class FileAttachment extends Schema.Class<FileAttachment>("Session.Event.FileAttachment")({
|
||||
uri: Schema.String,
|
||||
mime: Schema.String,
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
source: Source.pipe(Schema.optional),
|
||||
}) {
|
||||
static create(input: FileAttachment) {
|
||||
return new FileAttachment({
|
||||
uri: input.uri,
|
||||
mime: input.mime,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
source: input.source,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Source extends Schema.Class<Source>("Session.Event.Source")({
|
||||
start: Schema.Number,
|
||||
end: Schema.Number,
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
export class AgentAttachment extends Schema.Class<AgentAttachment>("Session.Event.AgentAttachment")({
|
||||
name: Schema.String,
|
||||
source: Source.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class FileAttachment extends Schema.Class<FileAttachment>("Session.Event.FileAttachment")({
|
||||
uri: Schema.String,
|
||||
mime: Schema.String,
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
source: Source.pipe(Schema.optional),
|
||||
}) {
|
||||
static create(input: FileAttachment) {
|
||||
return new FileAttachment({
|
||||
uri: input.uri,
|
||||
mime: input.mime,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
source: input.source,
|
||||
})
|
||||
}
|
||||
}
|
||||
export class RetryError extends Schema.Class<RetryError>("Session.Event.Retry.Error")({
|
||||
message: Schema.String,
|
||||
statusCode: Schema.Number.pipe(Schema.optional),
|
||||
isRetryable: Schema.Boolean,
|
||||
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
responseBody: Schema.String.pipe(Schema.optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class AgentAttachment extends Schema.Class<AgentAttachment>("Session.Event.AgentAttachment")({
|
||||
name: Schema.String,
|
||||
source: Source.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class RetryError extends Schema.Class<RetryError>("Session.Event.Retry.Error")({
|
||||
message: Schema.String,
|
||||
statusCode: Schema.Number.pipe(Schema.optional),
|
||||
isRetryable: Schema.Boolean,
|
||||
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
responseBody: Schema.String.pipe(Schema.optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Prompt extends Schema.Class<Prompt>("Session.Event.Prompt")({
|
||||
...Base,
|
||||
type: Schema.Literal("prompt"),
|
||||
export class Prompt extends defineEvent<Prompt>("Session.Event.Prompt")({
|
||||
type: "prompt",
|
||||
schema: {
|
||||
text: Schema.String,
|
||||
files: Schema.Array(FileAttachment).pipe(Schema.optional),
|
||||
agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
|
||||
}) {
|
||||
static create(input: BaseInput & { text: string; files?: FileAttachment[]; agents?: AgentAttachment[] }) {
|
||||
return new Prompt({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "prompt",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
text: input.text,
|
||||
files: input.files,
|
||||
agents: input.agents,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export class Synthetic extends Schema.Class<Synthetic>("Session.Event.Synthetic")({
|
||||
...Base,
|
||||
type: Schema.Literal("synthetic"),
|
||||
export class Synthetic extends defineEvent<Synthetic>("Session.Event.Synthetic")({
|
||||
type: "synthetic",
|
||||
schema: {
|
||||
text: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { text: string }) {
|
||||
return new Synthetic({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "synthetic",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export namespace Step {
|
||||
export class Started extends Schema.Class<Started>("Session.Event.Step.Started")({
|
||||
...Base,
|
||||
type: Schema.Literal("step.started"),
|
||||
export namespace Step {
|
||||
export class Started extends defineEvent<Started>("Session.Event.Step.Started")({
|
||||
type: "step.started",
|
||||
schema: {
|
||||
model: Schema.Struct({
|
||||
id: Schema.String,
|
||||
providerID: Schema.String,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static create(input: BaseInput & { model: { id: string; providerID: string; variant?: string } }) {
|
||||
return new Started({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "step.started",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
model: input.model,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export class Ended extends Schema.Class<Ended>("Session.Event.Step.Ended")({
|
||||
...Base,
|
||||
type: Schema.Literal("step.ended"),
|
||||
export class Ended extends defineEvent<Ended>("Session.Event.Step.Ended")({
|
||||
type: "step.ended",
|
||||
schema: {
|
||||
reason: Schema.String,
|
||||
cost: Schema.Number,
|
||||
tokens: Schema.Struct({
|
||||
@@ -133,177 +138,82 @@ export namespace SessionEvent {
|
||||
write: Schema.Number,
|
||||
}),
|
||||
}),
|
||||
}) {
|
||||
static create(input: BaseInput & { reason: string; cost: number; tokens: Ended["tokens"] }) {
|
||||
return new Ended({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "step.ended",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
reason: input.reason,
|
||||
cost: input.cost,
|
||||
tokens: input.tokens,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
}
|
||||
|
||||
export namespace Text {
|
||||
export class Started extends Schema.Class<Started>("Session.Event.Text.Started")({
|
||||
...Base,
|
||||
type: Schema.Literal("text.started"),
|
||||
}) {
|
||||
static create(input: BaseInput = {}) {
|
||||
return new Started({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "text.started",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
export namespace Text {
|
||||
export class Started extends defineEvent<Started>("Session.Event.Text.Started")({
|
||||
type: "text.started",
|
||||
schema: {},
|
||||
}) {}
|
||||
|
||||
export class Delta extends Schema.Class<Delta>("Session.Event.Text.Delta")({
|
||||
...Base,
|
||||
type: Schema.Literal("text.delta"),
|
||||
export class Delta extends defineEvent<Delta>("Session.Event.Text.Delta")({
|
||||
type: "text.delta",
|
||||
schema: {
|
||||
delta: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { delta: string }) {
|
||||
return new Delta({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "text.delta",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
delta: input.delta,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export class Ended extends Schema.Class<Ended>("Session.Event.Text.Ended")({
|
||||
...Base,
|
||||
type: Schema.Literal("text.ended"),
|
||||
export class Ended extends defineEvent<Ended>("Session.Event.Text.Ended")({
|
||||
type: "text.ended",
|
||||
schema: {
|
||||
text: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { text: string }) {
|
||||
return new Ended({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "text.ended",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
}
|
||||
|
||||
export namespace Reasoning {
|
||||
export class Started extends Schema.Class<Started>("Session.Event.Reasoning.Started")({
|
||||
...Base,
|
||||
type: Schema.Literal("reasoning.started"),
|
||||
}) {
|
||||
static create(input: BaseInput = {}) {
|
||||
return new Started({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "reasoning.started",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
export namespace Reasoning {
|
||||
export class Started extends defineEvent<Started>("Session.Event.Reasoning.Started")({
|
||||
type: "reasoning.started",
|
||||
schema: {},
|
||||
}) {}
|
||||
|
||||
export class Delta extends Schema.Class<Delta>("Session.Event.Reasoning.Delta")({
|
||||
...Base,
|
||||
type: Schema.Literal("reasoning.delta"),
|
||||
export class Delta extends defineEvent<Delta>("Session.Event.Reasoning.Delta")({
|
||||
type: "reasoning.delta",
|
||||
schema: {
|
||||
delta: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { delta: string }) {
|
||||
return new Delta({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "reasoning.delta",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
delta: input.delta,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export class Ended extends Schema.Class<Ended>("Session.Event.Reasoning.Ended")({
|
||||
...Base,
|
||||
type: Schema.Literal("reasoning.ended"),
|
||||
export class Ended extends defineEvent<Ended>("Session.Event.Reasoning.Ended")({
|
||||
type: "reasoning.ended",
|
||||
schema: {
|
||||
text: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { text: string }) {
|
||||
return new Ended({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "reasoning.ended",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
}
|
||||
|
||||
export namespace Tool {
|
||||
export namespace Input {
|
||||
export class Started extends Schema.Class<Started>("Session.Event.Tool.Input.Started")({
|
||||
...Base,
|
||||
export namespace Tool {
|
||||
export namespace Input {
|
||||
export class Started extends defineEvent<Started>("Session.Event.Tool.Input.Started")({
|
||||
type: "tool.input.started",
|
||||
schema: {
|
||||
callID: Schema.String,
|
||||
name: Schema.String,
|
||||
type: Schema.Literal("tool.input.started"),
|
||||
}) {
|
||||
static create(input: BaseInput & { callID: string; name: string }) {
|
||||
return new Started({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.input.started",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
name: input.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export class Delta extends Schema.Class<Delta>("Session.Event.Tool.Input.Delta")({
|
||||
...Base,
|
||||
export class Delta extends defineEvent<Delta>("Session.Event.Tool.Input.Delta")({
|
||||
type: "tool.input.delta",
|
||||
schema: {
|
||||
callID: Schema.String,
|
||||
type: Schema.Literal("tool.input.delta"),
|
||||
delta: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { callID: string; delta: string }) {
|
||||
return new Delta({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.input.delta",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
delta: input.delta,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export class Ended extends Schema.Class<Ended>("Session.Event.Tool.Input.Ended")({
|
||||
...Base,
|
||||
export class Ended extends defineEvent<Ended>("Session.Event.Tool.Input.Ended")({
|
||||
type: "tool.input.ended",
|
||||
schema: {
|
||||
callID: Schema.String,
|
||||
type: Schema.Literal("tool.input.ended"),
|
||||
text: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { callID: string; text: string }) {
|
||||
return new Ended({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.input.ended",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
}
|
||||
|
||||
export class Called extends Schema.Class<Called>("Session.Event.Tool.Called")({
|
||||
...Base,
|
||||
type: Schema.Literal("tool.called"),
|
||||
export class Called extends defineEvent<Called>("Session.Event.Tool.Called")({
|
||||
type: "tool.called",
|
||||
schema: {
|
||||
callID: Schema.String,
|
||||
tool: Schema.String,
|
||||
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||
@@ -311,31 +221,12 @@ export namespace SessionEvent {
|
||||
executed: Schema.Boolean,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static create(
|
||||
input: BaseInput & {
|
||||
callID: string
|
||||
tool: string
|
||||
input: Record<string, unknown>
|
||||
provider: Called["provider"]
|
||||
},
|
||||
) {
|
||||
return new Called({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.called",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
tool: input.tool,
|
||||
input: input.input,
|
||||
provider: input.provider,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export class Success extends Schema.Class<Success>("Session.Event.Tool.Success")({
|
||||
...Base,
|
||||
type: Schema.Literal("tool.success"),
|
||||
export class Success extends defineEvent<Success>("Session.Event.Tool.Success")({
|
||||
type: "tool.success",
|
||||
schema: {
|
||||
callID: Schema.String,
|
||||
title: Schema.String,
|
||||
output: Schema.String.pipe(Schema.optional),
|
||||
@@ -344,115 +235,64 @@ export namespace SessionEvent {
|
||||
executed: Schema.Boolean,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static create(
|
||||
input: BaseInput & {
|
||||
callID: string
|
||||
title: string
|
||||
output?: string
|
||||
attachments?: FileAttachment[]
|
||||
provider: Success["provider"]
|
||||
},
|
||||
) {
|
||||
return new Success({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.success",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
title: input.title,
|
||||
output: input.output,
|
||||
attachments: input.attachments,
|
||||
provider: input.provider,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export class Error extends Schema.Class<Error>("Session.Event.Tool.Error")({
|
||||
...Base,
|
||||
type: Schema.Literal("tool.error"),
|
||||
export class Error extends defineEvent<Error>("Session.Event.Tool.Error")({
|
||||
type: "tool.error",
|
||||
schema: {
|
||||
callID: Schema.String,
|
||||
error: Schema.String,
|
||||
provider: Schema.Struct({
|
||||
executed: Schema.Boolean,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static create(input: BaseInput & { callID: string; error: string; provider: Error["provider"] }) {
|
||||
return new Error({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.error",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
error: input.error,
|
||||
provider: input.provider,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
}
|
||||
|
||||
export class Retried extends Schema.Class<Retried>("Session.Event.Retried")({
|
||||
...Base,
|
||||
type: Schema.Literal("retried"),
|
||||
export class Retried extends defineEvent<Retried>("Session.Event.Retried")({
|
||||
type: "retried",
|
||||
schema: {
|
||||
attempt: Schema.Number,
|
||||
error: RetryError,
|
||||
}) {
|
||||
static create(input: BaseInput & { attempt: number; error: RetryError }) {
|
||||
return new Retried({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "retried",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
attempt: input.attempt,
|
||||
error: input.error,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export class Compacted extends Schema.Class<Compacted>("Session.Event.Compated")({
|
||||
...Base,
|
||||
type: Schema.Literal("compacted"),
|
||||
export class Compacted extends defineEvent<Compacted>("Session.Event.Compacted")({
|
||||
type: "compacted",
|
||||
schema: {
|
||||
auto: Schema.Boolean,
|
||||
overflow: Schema.Boolean.pipe(Schema.optional),
|
||||
}) {
|
||||
static create(input: BaseInput & { auto: boolean; overflow?: boolean }) {
|
||||
return new Compacted({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "compacted",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
auto: input.auto,
|
||||
overflow: input.overflow,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
||||
export const Event = Schema.Union(
|
||||
[
|
||||
Prompt,
|
||||
Synthetic,
|
||||
Step.Started,
|
||||
Step.Ended,
|
||||
Text.Started,
|
||||
Text.Delta,
|
||||
Text.Ended,
|
||||
Tool.Input.Started,
|
||||
Tool.Input.Delta,
|
||||
Tool.Input.Ended,
|
||||
Tool.Called,
|
||||
Tool.Success,
|
||||
Tool.Error,
|
||||
Reasoning.Started,
|
||||
Reasoning.Delta,
|
||||
Reasoning.Ended,
|
||||
Retried,
|
||||
Compacted,
|
||||
],
|
||||
{
|
||||
mode: "oneOf",
|
||||
},
|
||||
).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Event = Schema.Schema.Type<typeof Event>
|
||||
export type Type = Event["type"]
|
||||
}
|
||||
export const Event = Schema.Union(
|
||||
[
|
||||
Prompt,
|
||||
Synthetic,
|
||||
Step.Started,
|
||||
Step.Ended,
|
||||
Text.Started,
|
||||
Text.Delta,
|
||||
Text.Ended,
|
||||
Tool.Input.Started,
|
||||
Tool.Input.Delta,
|
||||
Tool.Input.Ended,
|
||||
Tool.Called,
|
||||
Tool.Success,
|
||||
Tool.Error,
|
||||
Reasoning.Started,
|
||||
Reasoning.Delta,
|
||||
Reasoning.Ended,
|
||||
Retried,
|
||||
Compacted,
|
||||
],
|
||||
{
|
||||
mode: "oneOf",
|
||||
},
|
||||
).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Event = Schema.Schema.Type<typeof Event>
|
||||
export type Type = Event["type"]
|
||||
|
||||
export * as SessionEvent from "./session-event"
|
||||
|
||||
@@ -13,7 +13,7 @@ import { errorMessage } from "../util/error"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Git } from "@/git"
|
||||
import { Effect, Layer, Path, Scope, Context, Stream } from "effect"
|
||||
import { Effect, Layer, Path, Schema, Scope, Context, Stream } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { NodePath } from "@effect/platform-node"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
@@ -26,15 +26,15 @@ const log = Log.create({ service: "worktree" })
|
||||
export const Event = {
|
||||
Ready: BusEvent.define(
|
||||
"worktree.ready",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
branch: z.string(),
|
||||
Schema.Struct({
|
||||
name: Schema.String,
|
||||
branch: Schema.String,
|
||||
}),
|
||||
),
|
||||
Failed: BusEvent.define(
|
||||
"worktree.failed",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
Schema.Struct({
|
||||
message: Schema.String,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Deferred, Effect, Layer, Stream } from "effect"
|
||||
import z from "zod"
|
||||
import { Deferred, Effect, Layer, Schema, Stream } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
@@ -9,8 +8,8 @@ import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const TestEvent = {
|
||||
Ping: BusEvent.define("test.effect.ping", z.object({ value: z.number() })),
|
||||
Pong: BusEvent.define("test.effect.pong", z.object({ message: z.string() })),
|
||||
Ping: BusEvent.define("test.effect.ping", Schema.Struct({ value: Schema.Number })),
|
||||
Pong: BusEvent.define("test.effect.pong", Schema.Struct({ message: Schema.String })),
|
||||
}
|
||||
|
||||
const node = CrossSpawnSpawner.defaultLayer
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const TestEvent = BusEvent.define("test.integration", z.object({ value: z.number() }))
|
||||
const TestEvent = BusEvent.define("test.integration", Schema.Struct({ value: Schema.Number }))
|
||||
|
||||
function withInstance(directory: string, fn: () => Promise<void>) {
|
||||
return Instance.provide({ directory, fn })
|
||||
@@ -42,7 +42,7 @@ describe("Bus integration: acquireRelease subscriber pattern", () => {
|
||||
await using tmp = await tmpdir()
|
||||
const received: Array<{ type: string; value?: number }> = []
|
||||
|
||||
const OtherEvent = BusEvent.define("test.other", z.object({ value: z.number() }))
|
||||
const OtherEvent = BusEvent.define("test.other", Schema.Struct({ value: Schema.Number }))
|
||||
|
||||
await withInstance(tmp.path, async () => {
|
||||
Bus.subscribeAll((evt) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const TestEvent = {
|
||||
Ping: BusEvent.define("test.ping", z.object({ value: z.number() })),
|
||||
Pong: BusEvent.define("test.pong", z.object({ message: z.string() })),
|
||||
Ping: BusEvent.define("test.ping", Schema.Struct({ value: Schema.Number })),
|
||||
Pong: BusEvent.define("test.pong", Schema.Struct({ message: Schema.String })),
|
||||
}
|
||||
|
||||
function withInstance(directory: string, fn: () => Promise<void>) {
|
||||
|
||||
@@ -4,8 +4,10 @@ import * as FastCheck from "effect/testing/FastCheck"
|
||||
import { SessionEntry } from "../../src/v2/session-entry"
|
||||
import { SessionEntryStepper } from "../../src/v2/session-entry-stepper"
|
||||
import { SessionEvent } from "../../src/v2/session-event"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
|
||||
const time = (n: number) => DateTime.makeUnsafe(n)
|
||||
const sessionID = SessionID.empty()
|
||||
|
||||
const word = FastCheck.string({ minLength: 1, maxLength: 8 })
|
||||
const text = FastCheck.string({ maxLength: 16 })
|
||||
@@ -147,24 +149,34 @@ describe("session-entry-stepper", () => {
|
||||
const store = adapterStore()
|
||||
store.committed.push(assistant())
|
||||
|
||||
SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Prompt.create({ text: "hello", timestamp: time(1) }))
|
||||
SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Started.create({ timestamp: time(2) }))
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Reasoning.Delta.create({ delta: "thinking", timestamp: time(3) }),
|
||||
SessionEvent.Prompt.create({ sessionID, text: "hello", timestamp: time(1) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Reasoning.Ended.create({ text: "thought", timestamp: time(4) }),
|
||||
SessionEvent.Reasoning.Started.create({ sessionID, timestamp: time(2) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Text.Started.create({ timestamp: time(5) }))
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Text.Delta.create({ delta: "world", timestamp: time(6) }),
|
||||
SessionEvent.Reasoning.Delta.create({ sessionID, delta: "thinking", timestamp: time(3) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Reasoning.Ended.create({ sessionID, text: "thought", timestamp: time(4) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Text.Started.create({ sessionID, timestamp: time(5) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Text.Delta.create({ sessionID, delta: "world", timestamp: time(6) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Step.Ended.create({
|
||||
sessionID,
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
@@ -199,15 +211,12 @@ describe("session-entry-stepper", () => {
|
||||
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Retried.create({
|
||||
attempt: 1,
|
||||
error: retryError("rate limited"),
|
||||
timestamp: time(1),
|
||||
}),
|
||||
SessionEvent.Retried.create({ sessionID, attempt: 1, error: retryError("rate limited"), timestamp: time(1) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Retried.create({
|
||||
sessionID,
|
||||
attempt: 2,
|
||||
error: retryError("provider overloaded"),
|
||||
timestamp: time(2),
|
||||
@@ -253,9 +262,11 @@ describe("session-entry-stepper", () => {
|
||||
const state = memoryState()
|
||||
const adapter = SessionEntryStepper.memory(state)
|
||||
const committed = SessionEntry.User.fromEvent(
|
||||
SessionEvent.Prompt.create({ text: "committed", timestamp: time(1) }),
|
||||
SessionEvent.Prompt.create({ sessionID, text: "committed", timestamp: time(1) }),
|
||||
)
|
||||
const pending = SessionEntry.User.fromEvent(
|
||||
SessionEvent.Prompt.create({ sessionID, text: "pending", timestamp: time(2) }),
|
||||
)
|
||||
const pending = SessionEntry.User.fromEvent(SessionEvent.Prompt.create({ text: "pending", timestamp: time(2) }))
|
||||
|
||||
adapter.appendEntry(committed)
|
||||
adapter.appendPending(pending)
|
||||
@@ -269,15 +280,15 @@ describe("session-entry-stepper", () => {
|
||||
|
||||
SessionEntryStepper.stepWith(
|
||||
SessionEntryStepper.memory(state),
|
||||
SessionEvent.Reasoning.Started.create({ timestamp: time(1) }),
|
||||
SessionEvent.Reasoning.Started.create({ sessionID, timestamp: time(1) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
SessionEntryStepper.memory(state),
|
||||
SessionEvent.Reasoning.Delta.create({ delta: "draft", timestamp: time(2) }),
|
||||
SessionEvent.Reasoning.Delta.create({ sessionID, delta: "draft", timestamp: time(2) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
SessionEntryStepper.memory(state),
|
||||
SessionEvent.Reasoning.Ended.create({ text: "final", timestamp: time(3) }),
|
||||
SessionEvent.Reasoning.Ended.create({ sessionID, text: "final", timestamp: time(3) }),
|
||||
)
|
||||
|
||||
expect(reasons(state)).toEqual([{ type: "reasoning", text: "final" }])
|
||||
@@ -288,11 +299,7 @@ describe("session-entry-stepper", () => {
|
||||
|
||||
SessionEntryStepper.stepWith(
|
||||
SessionEntryStepper.memory(state),
|
||||
SessionEvent.Retried.create({
|
||||
attempt: 1,
|
||||
error: retryError("rate limited"),
|
||||
timestamp: time(1),
|
||||
}),
|
||||
SessionEvent.Retried.create({ sessionID, attempt: 1, error: retryError("rate limited"), timestamp: time(1) }),
|
||||
)
|
||||
|
||||
expect(retriesOf(state)).toEqual([retry(1, "rate limited", 1)])
|
||||
@@ -306,7 +313,7 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(word, (body) => {
|
||||
const next = SessionEntryStepper.step(
|
||||
memoryState(),
|
||||
SessionEvent.Prompt.create({ text: body, timestamp: time(1) }),
|
||||
SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(1) }),
|
||||
)
|
||||
expect(next.entries).toHaveLength(1)
|
||||
expect(next.entries[0]?.type).toBe("user")
|
||||
@@ -322,7 +329,7 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(word, (body) => {
|
||||
const next = SessionEntryStepper.step(
|
||||
active(),
|
||||
SessionEvent.Prompt.create({ text: body, timestamp: time(1) }),
|
||||
SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(1) }),
|
||||
)
|
||||
expect(next.pending).toHaveLength(1)
|
||||
expect(next.pending[0]?.type).toBe("user")
|
||||
@@ -340,9 +347,9 @@ describe("session-entry-stepper", () => {
|
||||
(state, part, i) =>
|
||||
SessionEntryStepper.step(
|
||||
state,
|
||||
SessionEvent.Text.Delta.create({ delta: part, timestamp: time(i + 2) }),
|
||||
SessionEvent.Text.Delta.create({ sessionID, delta: part, timestamp: time(i + 2) }),
|
||||
),
|
||||
SessionEntryStepper.step(active(), SessionEvent.Text.Started.create({ timestamp: time(1) })),
|
||||
SessionEntryStepper.step(active(), SessionEvent.Text.Started.create({ sessionID, timestamp: time(1) })),
|
||||
)
|
||||
|
||||
expect(texts_of(next)).toEqual([
|
||||
@@ -361,10 +368,12 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(texts, texts, (a, b) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Text.Started.create({ timestamp: time(1) }),
|
||||
...a.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 2) })),
|
||||
SessionEvent.Text.Started.create({ timestamp: time(a.length + 2) }),
|
||||
...b.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + a.length + 3) })),
|
||||
SessionEvent.Text.Started.create({ sessionID, timestamp: time(1) }),
|
||||
...a.map((x, i) => SessionEvent.Text.Delta.create({ sessionID, delta: x, timestamp: time(i + 2) })),
|
||||
SessionEvent.Text.Started.create({ sessionID, timestamp: time(a.length + 2) }),
|
||||
...b.map((x, i) =>
|
||||
SessionEvent.Text.Delta.create({ sessionID, delta: x, timestamp: time(i + a.length + 3) }),
|
||||
),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
@@ -383,9 +392,11 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(texts, text, (parts, end) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Reasoning.Started.create({ timestamp: time(1) }),
|
||||
...parts.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 2) })),
|
||||
SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(parts.length + 2) }),
|
||||
SessionEvent.Reasoning.Started.create({ sessionID, timestamp: time(1) }),
|
||||
...parts.map((x, i) =>
|
||||
SessionEvent.Reasoning.Delta.create({ sessionID, delta: x, timestamp: time(i + 2) }),
|
||||
),
|
||||
SessionEvent.Reasoning.Ended.create({ sessionID, text: end, timestamp: time(parts.length + 2) }),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
@@ -414,11 +425,12 @@ describe("session-entry-stepper", () => {
|
||||
(callID, title, input, output, metadata, attachments, parts) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Input.Started.create({ sessionID, callID, name: "bash", timestamp: time(1) }),
|
||||
...parts.map((x, i) =>
|
||||
SessionEvent.Tool.Input.Delta.create({ callID, delta: x, timestamp: time(i + 2) }),
|
||||
SessionEvent.Tool.Input.Delta.create({ sessionID, callID, delta: x, timestamp: time(i + 2) }),
|
||||
),
|
||||
SessionEvent.Tool.Called.create({
|
||||
sessionID,
|
||||
callID,
|
||||
tool: "bash",
|
||||
input,
|
||||
@@ -426,6 +438,7 @@ describe("session-entry-stepper", () => {
|
||||
timestamp: time(parts.length + 2),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
sessionID,
|
||||
callID,
|
||||
title,
|
||||
output,
|
||||
@@ -459,8 +472,9 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(word, dict, word, maybe(dict), (callID, input, error, metadata) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Input.Started.create({ sessionID, callID, name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Called.create({
|
||||
sessionID,
|
||||
callID,
|
||||
tool: "bash",
|
||||
input,
|
||||
@@ -468,6 +482,7 @@ describe("session-entry-stepper", () => {
|
||||
timestamp: time(2),
|
||||
}),
|
||||
SessionEvent.Tool.Error.create({
|
||||
sessionID,
|
||||
callID,
|
||||
error,
|
||||
metadata,
|
||||
@@ -496,8 +511,9 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(word, word, (callID, title) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Input.Started.create({ sessionID, callID, name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Success.create({
|
||||
sessionID,
|
||||
callID,
|
||||
title,
|
||||
provider: { executed: true },
|
||||
@@ -520,6 +536,7 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(FastCheck.integer({ min: 1, max: 1000 }), (n) => {
|
||||
const event = SessionEvent.Step.Ended.create({
|
||||
sessionID,
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
@@ -552,7 +569,10 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, (body) => {
|
||||
const old = memoryState()
|
||||
const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) }))
|
||||
const next = SessionEntryStepper.step(
|
||||
old,
|
||||
SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(1) }),
|
||||
)
|
||||
expect(old).not.toBe(next)
|
||||
expect(old.entries).toHaveLength(0)
|
||||
expect(next.entries).toHaveLength(1)
|
||||
@@ -565,7 +585,10 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, (body) => {
|
||||
const old = active()
|
||||
const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) }))
|
||||
const next = SessionEntryStepper.step(
|
||||
old,
|
||||
SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(1) }),
|
||||
)
|
||||
expect(old).not.toBe(next)
|
||||
expect(old.pending).toHaveLength(0)
|
||||
expect(next.pending).toHaveLength(1)
|
||||
@@ -579,15 +602,17 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(texts, (parts) => {
|
||||
const next = run([
|
||||
SessionEvent.Step.Started.create({
|
||||
sessionID,
|
||||
model: {
|
||||
id: "model",
|
||||
providerID: "provider",
|
||||
},
|
||||
timestamp: time(1),
|
||||
}),
|
||||
SessionEvent.Text.Started.create({ timestamp: time(2) }),
|
||||
...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })),
|
||||
SessionEvent.Text.Started.create({ sessionID, timestamp: time(2) }),
|
||||
...parts.map((x, i) => SessionEvent.Text.Delta.create({ sessionID, delta: x, timestamp: time(i + 3) })),
|
||||
SessionEvent.Step.Ended.create({
|
||||
sessionID,
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
@@ -623,17 +648,19 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, texts, (body, parts) => {
|
||||
const next = run([
|
||||
SessionEvent.Prompt.create({ text: body, timestamp: time(0) }),
|
||||
SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(0) }),
|
||||
SessionEvent.Step.Started.create({
|
||||
sessionID,
|
||||
model: {
|
||||
id: "model",
|
||||
providerID: "provider",
|
||||
},
|
||||
timestamp: time(1),
|
||||
}),
|
||||
SessionEvent.Text.Started.create({ timestamp: time(2) }),
|
||||
...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })),
|
||||
SessionEvent.Text.Started.create({ sessionID, timestamp: time(2) }),
|
||||
...parts.map((x, i) => SessionEvent.Text.Delta.create({ sessionID, delta: x, timestamp: time(i + 3) })),
|
||||
SessionEvent.Step.Ended.create({
|
||||
sessionID,
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
@@ -680,19 +707,28 @@ describe("session-entry-stepper", () => {
|
||||
(body, reason, end, input, title, output, metadata, attachments) => {
|
||||
const callID = "call"
|
||||
const next = run([
|
||||
SessionEvent.Prompt.create({ text: body, timestamp: time(0) }),
|
||||
SessionEvent.Prompt.create({ sessionID, text: body, timestamp: time(0) }),
|
||||
SessionEvent.Step.Started.create({
|
||||
sessionID,
|
||||
model: {
|
||||
id: "model",
|
||||
providerID: "provider",
|
||||
},
|
||||
timestamp: time(1),
|
||||
}),
|
||||
SessionEvent.Reasoning.Started.create({ timestamp: time(2) }),
|
||||
...reason.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 3) })),
|
||||
SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(reason.length + 3) }),
|
||||
SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(reason.length + 4) }),
|
||||
SessionEvent.Reasoning.Started.create({ sessionID, timestamp: time(2) }),
|
||||
...reason.map((x, i) =>
|
||||
SessionEvent.Reasoning.Delta.create({ sessionID, delta: x, timestamp: time(i + 3) }),
|
||||
),
|
||||
SessionEvent.Reasoning.Ended.create({ sessionID, text: end, timestamp: time(reason.length + 3) }),
|
||||
SessionEvent.Tool.Input.Started.create({
|
||||
sessionID,
|
||||
callID,
|
||||
name: "bash",
|
||||
timestamp: time(reason.length + 4),
|
||||
}),
|
||||
SessionEvent.Tool.Called.create({
|
||||
sessionID,
|
||||
callID,
|
||||
tool: "bash",
|
||||
input,
|
||||
@@ -700,6 +736,7 @@ describe("session-entry-stepper", () => {
|
||||
timestamp: time(reason.length + 5),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
sessionID,
|
||||
callID,
|
||||
title,
|
||||
output,
|
||||
@@ -709,6 +746,7 @@ describe("session-entry-stepper", () => {
|
||||
timestamp: time(reason.length + 6),
|
||||
}),
|
||||
SessionEvent.Step.Ended.create({
|
||||
sessionID,
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
@@ -747,6 +785,7 @@ describe("session-entry-stepper", () => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Step.Started.create({
|
||||
sessionID,
|
||||
model: {
|
||||
id: "model",
|
||||
providerID: "provider",
|
||||
@@ -771,8 +810,9 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(dict, dict, word, word, (a, b, title, error) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Input.Started.create({ sessionID, callID: "a", name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Called.create({
|
||||
sessionID,
|
||||
callID: "a",
|
||||
tool: "bash",
|
||||
input: a,
|
||||
@@ -780,14 +820,16 @@ describe("session-entry-stepper", () => {
|
||||
timestamp: time(2),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
sessionID,
|
||||
callID: "a",
|
||||
title,
|
||||
output: "done",
|
||||
provider: { executed: true },
|
||||
timestamp: time(3),
|
||||
}),
|
||||
SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(4) }),
|
||||
SessionEvent.Tool.Input.Started.create({ sessionID, callID: "b", name: "grep", timestamp: time(4) }),
|
||||
SessionEvent.Tool.Called.create({
|
||||
sessionID,
|
||||
callID: "b",
|
||||
tool: "bash",
|
||||
input: b,
|
||||
@@ -795,6 +837,7 @@ describe("session-entry-stepper", () => {
|
||||
timestamp: time(5),
|
||||
}),
|
||||
SessionEvent.Tool.Error.create({
|
||||
sessionID,
|
||||
callID: "b",
|
||||
error,
|
||||
provider: { executed: true },
|
||||
@@ -827,11 +870,12 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(dict, dict, word, word, text, text, (a, b, titleA, titleB, deltaA, deltaB) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(2) }),
|
||||
SessionEvent.Tool.Input.Delta.create({ callID: "a", delta: deltaA, timestamp: time(3) }),
|
||||
SessionEvent.Tool.Input.Delta.create({ callID: "b", delta: deltaB, timestamp: time(4) }),
|
||||
SessionEvent.Tool.Input.Started.create({ sessionID, callID: "a", name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Input.Started.create({ sessionID, callID: "b", name: "grep", timestamp: time(2) }),
|
||||
SessionEvent.Tool.Input.Delta.create({ sessionID, callID: "a", delta: deltaA, timestamp: time(3) }),
|
||||
SessionEvent.Tool.Input.Delta.create({ sessionID, callID: "b", delta: deltaB, timestamp: time(4) }),
|
||||
SessionEvent.Tool.Called.create({
|
||||
sessionID,
|
||||
callID: "a",
|
||||
tool: "bash",
|
||||
input: a,
|
||||
@@ -839,6 +883,7 @@ describe("session-entry-stepper", () => {
|
||||
timestamp: time(5),
|
||||
}),
|
||||
SessionEvent.Tool.Called.create({
|
||||
sessionID,
|
||||
callID: "b",
|
||||
tool: "grep",
|
||||
input: b,
|
||||
@@ -846,6 +891,7 @@ describe("session-entry-stepper", () => {
|
||||
timestamp: time(6),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
sessionID,
|
||||
callID: "a",
|
||||
title: titleA,
|
||||
output: "done-a",
|
||||
@@ -853,6 +899,7 @@ describe("session-entry-stepper", () => {
|
||||
timestamp: time(7),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
sessionID,
|
||||
callID: "b",
|
||||
title: titleB,
|
||||
output: "done-b",
|
||||
@@ -884,7 +931,7 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(word, (body) => {
|
||||
const next = SessionEntryStepper.step(
|
||||
memoryState(),
|
||||
SessionEvent.Synthetic.create({ text: body, timestamp: time(1) }),
|
||||
SessionEvent.Synthetic.create({ sessionID, text: body, timestamp: time(1) }),
|
||||
)
|
||||
expect(next.entries).toHaveLength(1)
|
||||
expect(next.entries[0]?.type).toBe("synthetic")
|
||||
@@ -900,7 +947,7 @@ describe("session-entry-stepper", () => {
|
||||
FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => {
|
||||
const next = SessionEntryStepper.step(
|
||||
memoryState(),
|
||||
SessionEvent.Compacted.create({ auto, overflow, timestamp: time(1) }),
|
||||
SessionEvent.Compacted.create({ sessionID, auto, overflow, timestamp: time(1) }),
|
||||
)
|
||||
expect(next.entries).toHaveLength(1)
|
||||
expect(next.entries[0]?.type).toBe("compaction")
|
||||
|
||||
@@ -111,9 +111,12 @@ describe("step-finish token propagation via Bus event", () => {
|
||||
mode: "",
|
||||
} as unknown as MessageV2.Info)
|
||||
|
||||
// Bus subscribers receive readonly Schema.Type payloads; `MessageV2.Part`
|
||||
// is the mutable domain type. Cast bridges the two — safe because the
|
||||
// test only reads the value afterwards.
|
||||
let received: MessageV2.Part | undefined
|
||||
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, (event) => {
|
||||
received = event.properties.part
|
||||
received = event.properties.part as MessageV2.Part
|
||||
})
|
||||
|
||||
const tokens = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, afterAll } from "bun:test"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
@@ -43,13 +43,13 @@ describe("SyncEvent", () => {
|
||||
type: "item.created",
|
||||
version: 1,
|
||||
aggregate: "id",
|
||||
schema: z.object({ id: z.string(), name: z.string() }),
|
||||
schema: Schema.Struct({ id: Schema.String, name: Schema.String }),
|
||||
})
|
||||
const Sent = SyncEvent.define({
|
||||
type: "item.sent",
|
||||
version: 1,
|
||||
aggregate: "item_id",
|
||||
schema: z.object({ item_id: z.string(), to: z.string() }),
|
||||
schema: Schema.Struct({ item_id: Schema.String, to: Schema.String }),
|
||||
})
|
||||
|
||||
SyncEvent.init({
|
||||
|
||||
@@ -0,0 +1,495 @@
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) apply_patch 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"patchText": {
|
||||
"description": "The full patch text that describes all changes to be made",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"patchText",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) bash 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "The command to execute",
|
||||
"type": "string",
|
||||
},
|
||||
"description": {
|
||||
"description":
|
||||
"Clear, concise description of what this command does in 5-10 words. Examples:
|
||||
Input: ls
|
||||
Output: Lists files in current directory
|
||||
|
||||
Input: git status
|
||||
Output: Shows working tree status
|
||||
|
||||
Input: npm install
|
||||
Output: Installs package dependencies
|
||||
|
||||
Input: mkdir foo
|
||||
Output: Creates directory 'foo'"
|
||||
,
|
||||
"type": "string",
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Optional timeout in milliseconds",
|
||||
"type": "number",
|
||||
},
|
||||
"workdir": {
|
||||
"description": "The working directory to run the command in. Defaults to the current directory. Use this instead of 'cd' commands.",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"description",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) codesearch 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"query": {
|
||||
"description": "Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'",
|
||||
"type": "string",
|
||||
},
|
||||
"tokensNum": {
|
||||
"default": 5000,
|
||||
"description": "Number of tokens to return (1000-50000). Default is 5000 tokens. Adjust this value based on how much context you need - use lower values for focused queries and higher values for comprehensive documentation.",
|
||||
"maximum": 50000,
|
||||
"minimum": 1000,
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"query",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) edit 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"filePath": {
|
||||
"description": "The absolute path to the file to modify",
|
||||
"type": "string",
|
||||
},
|
||||
"newString": {
|
||||
"description": "The text to replace it with (must be different from oldString)",
|
||||
"type": "string",
|
||||
},
|
||||
"oldString": {
|
||||
"description": "The text to replace",
|
||||
"type": "string",
|
||||
},
|
||||
"replaceAll": {
|
||||
"description": "Replace all occurrences of oldString (default false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"filePath",
|
||||
"oldString",
|
||||
"newString",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) glob 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"path": {
|
||||
"description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.",
|
||||
"type": "string",
|
||||
},
|
||||
"pattern": {
|
||||
"description": "The glob pattern to match files against",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"pattern",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) grep 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"include": {
|
||||
"description": "File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")",
|
||||
"type": "string",
|
||||
},
|
||||
"path": {
|
||||
"description": "The directory to search in. Defaults to the current working directory.",
|
||||
"type": "string",
|
||||
},
|
||||
"pattern": {
|
||||
"description": "The regex pattern to search for in file contents",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"pattern",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) invalid 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
},
|
||||
"tool": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"tool",
|
||||
"error",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) lsp 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"character": {
|
||||
"description": "The character offset (1-based, as shown in editors)",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
"filePath": {
|
||||
"description": "The absolute or relative path to the file",
|
||||
"type": "string",
|
||||
},
|
||||
"line": {
|
||||
"description": "The line number (1-based, as shown in editors)",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
},
|
||||
"operation": {
|
||||
"description": "The LSP operation to perform",
|
||||
"enum": [
|
||||
"goToDefinition",
|
||||
"findReferences",
|
||||
"hover",
|
||||
"documentSymbol",
|
||||
"workspaceSymbol",
|
||||
"goToImplementation",
|
||||
"prepareCallHierarchy",
|
||||
"incomingCalls",
|
||||
"outgoingCalls",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"operation",
|
||||
"filePath",
|
||||
"line",
|
||||
"character",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) plan 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) question 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"questions": {
|
||||
"description": "Questions to ask",
|
||||
"items": {
|
||||
"properties": {
|
||||
"header": {
|
||||
"description": "Very short label (max 30 chars)",
|
||||
"type": "string",
|
||||
},
|
||||
"multiple": {
|
||||
"description": "Allow selecting multiple choices",
|
||||
"type": "boolean",
|
||||
},
|
||||
"options": {
|
||||
"description": "Available choices",
|
||||
"items": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "Explanation of choice",
|
||||
"type": "string",
|
||||
},
|
||||
"label": {
|
||||
"description": "Display text (1-5 words, concise)",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"ref": "QuestionOption",
|
||||
"required": [
|
||||
"label",
|
||||
"description",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"question": {
|
||||
"description": "Complete question",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"ref": "QuestionPrompt",
|
||||
"required": [
|
||||
"question",
|
||||
"header",
|
||||
"options",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"questions",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) read 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"filePath": {
|
||||
"description": "The absolute path to the file or directory to read",
|
||||
"type": "string",
|
||||
},
|
||||
"limit": {
|
||||
"description": "The maximum number of lines to read (defaults to 2000)",
|
||||
"type": "number",
|
||||
},
|
||||
"offset": {
|
||||
"description": "The line number to start reading from (1-indexed)",
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"filePath",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) skill 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name of the skill from available_skills",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) task 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "The command that triggered this task",
|
||||
"type": "string",
|
||||
},
|
||||
"description": {
|
||||
"description": "A short (3-5 words) description of the task",
|
||||
"type": "string",
|
||||
},
|
||||
"prompt": {
|
||||
"description": "The task for the agent to perform",
|
||||
"type": "string",
|
||||
},
|
||||
"subagent_type": {
|
||||
"description": "The type of specialized agent to use for this task",
|
||||
"type": "string",
|
||||
},
|
||||
"task_id": {
|
||||
"description": "This should only be set if you mean to resume a previous task (you can pass a prior task_id and the task will continue the same subagent session as before instead of creating a fresh one)",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"prompt",
|
||||
"subagent_type",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) todo 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"todos": {
|
||||
"description": "The updated todo list",
|
||||
"items": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "Brief description of the task",
|
||||
"type": "string",
|
||||
},
|
||||
"priority": {
|
||||
"description": "Priority level of the task: high, medium, low",
|
||||
"type": "string",
|
||||
},
|
||||
"status": {
|
||||
"description": "Current status of the task: pending, in_progress, completed, cancelled",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"status",
|
||||
"priority",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"todos",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) webfetch 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"format": {
|
||||
"default": "markdown",
|
||||
"description": "The format to return the content in (text, markdown, or html). Defaults to markdown.",
|
||||
"enum": [
|
||||
"text",
|
||||
"markdown",
|
||||
"html",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Optional timeout in seconds (max 120)",
|
||||
"type": "number",
|
||||
},
|
||||
"url": {
|
||||
"description": "The URL to fetch content from",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) websearch 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"contextMaxCharacters": {
|
||||
"description": "Maximum characters for context string optimized for LLMs (default: 10000)",
|
||||
"type": "number",
|
||||
},
|
||||
"livecrawl": {
|
||||
"description": "Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
|
||||
"enum": [
|
||||
"fallback",
|
||||
"preferred",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"numResults": {
|
||||
"description": "Number of search results to return (default: 8)",
|
||||
"type": "number",
|
||||
},
|
||||
"query": {
|
||||
"description": "Websearch query",
|
||||
"type": "string",
|
||||
},
|
||||
"type": {
|
||||
"description": "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
|
||||
"enum": [
|
||||
"auto",
|
||||
"fast",
|
||||
"deep",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"query",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`tool parameters JSON Schema (wire shape) write 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "The content to write to the file",
|
||||
"type": "string",
|
||||
},
|
||||
"filePath": {
|
||||
"description": "The absolute path to the file to write (must be absolute, not relative)",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"content",
|
||||
"filePath",
|
||||
],
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,260 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Result, Schema } from "effect"
|
||||
import { toJsonSchema } from "../../src/util/effect-zod"
|
||||
|
||||
// Each tool exports its parameters schema at module scope so this test can
|
||||
// import them without running the tool's Effect-based init. The JSON Schema
|
||||
// snapshot captures what the LLM sees; the parse assertions pin down the
|
||||
// accepts/rejects contract. `toJsonSchema` is the same helper `session/
|
||||
// prompt.ts` uses to emit tool schemas to the LLM, so the snapshots stay
|
||||
// byte-identical regardless of whether a tool has migrated from zod to Schema.
|
||||
|
||||
import { Parameters as ApplyPatch } from "../../src/tool/apply_patch"
|
||||
import { Parameters as Bash } from "../../src/tool/bash"
|
||||
import { Parameters as CodeSearch } from "../../src/tool/codesearch"
|
||||
import { Parameters as Edit } from "../../src/tool/edit"
|
||||
import { Parameters as Glob } from "../../src/tool/glob"
|
||||
import { Parameters as Grep } from "../../src/tool/grep"
|
||||
import { Parameters as Invalid } from "../../src/tool/invalid"
|
||||
import { Parameters as Lsp } from "../../src/tool/lsp"
|
||||
import { Parameters as Plan } from "../../src/tool/plan"
|
||||
import { Parameters as Question } from "../../src/tool/question"
|
||||
import { Parameters as Read } from "../../src/tool/read"
|
||||
import { Parameters as Skill } from "../../src/tool/skill"
|
||||
import { Parameters as Task } from "../../src/tool/task"
|
||||
import { Parameters as Todo } from "../../src/tool/todo"
|
||||
import { Parameters as WebFetch } from "../../src/tool/webfetch"
|
||||
import { Parameters as WebSearch } from "../../src/tool/websearch"
|
||||
import { Parameters as Write } from "../../src/tool/write"
|
||||
|
||||
const parse = <S extends Schema.Decoder<unknown>>(schema: S, input: unknown): S["Type"] =>
|
||||
Schema.decodeUnknownSync(schema)(input)
|
||||
|
||||
const accepts = (schema: Schema.Decoder<unknown>, input: unknown): boolean =>
|
||||
Result.isSuccess(Schema.decodeUnknownResult(schema)(input))
|
||||
|
||||
describe("tool parameters", () => {
|
||||
describe("JSON Schema (wire shape)", () => {
|
||||
test("apply_patch", () => expect(toJsonSchema(ApplyPatch)).toMatchSnapshot())
|
||||
test("bash", () => expect(toJsonSchema(Bash)).toMatchSnapshot())
|
||||
test("codesearch", () => expect(toJsonSchema(CodeSearch)).toMatchSnapshot())
|
||||
test("edit", () => expect(toJsonSchema(Edit)).toMatchSnapshot())
|
||||
test("glob", () => expect(toJsonSchema(Glob)).toMatchSnapshot())
|
||||
test("grep", () => expect(toJsonSchema(Grep)).toMatchSnapshot())
|
||||
test("invalid", () => expect(toJsonSchema(Invalid)).toMatchSnapshot())
|
||||
test("lsp", () => expect(toJsonSchema(Lsp)).toMatchSnapshot())
|
||||
test("plan", () => expect(toJsonSchema(Plan)).toMatchSnapshot())
|
||||
test("question", () => expect(toJsonSchema(Question)).toMatchSnapshot())
|
||||
test("read", () => expect(toJsonSchema(Read)).toMatchSnapshot())
|
||||
test("skill", () => expect(toJsonSchema(Skill)).toMatchSnapshot())
|
||||
test("task", () => expect(toJsonSchema(Task)).toMatchSnapshot())
|
||||
test("todo", () => expect(toJsonSchema(Todo)).toMatchSnapshot())
|
||||
test("webfetch", () => expect(toJsonSchema(WebFetch)).toMatchSnapshot())
|
||||
test("websearch", () => expect(toJsonSchema(WebSearch)).toMatchSnapshot())
|
||||
test("write", () => expect(toJsonSchema(Write)).toMatchSnapshot())
|
||||
})
|
||||
|
||||
describe("apply_patch", () => {
|
||||
test("accepts patchText", () => {
|
||||
expect(parse(ApplyPatch, { patchText: "*** Begin Patch\n*** End Patch" })).toEqual({
|
||||
patchText: "*** Begin Patch\n*** End Patch",
|
||||
})
|
||||
})
|
||||
test("rejects missing patchText", () => {
|
||||
expect(accepts(ApplyPatch, {})).toBe(false)
|
||||
})
|
||||
test("rejects non-string patchText", () => {
|
||||
expect(accepts(ApplyPatch, { patchText: 123 })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("bash", () => {
|
||||
test("accepts minimum: command + description", () => {
|
||||
expect(parse(Bash, { command: "ls", description: "list" })).toEqual({ command: "ls", description: "list" })
|
||||
})
|
||||
test("accepts optional timeout + workdir", () => {
|
||||
const parsed = parse(Bash, { command: "ls", description: "list", timeout: 5000, workdir: "/tmp" })
|
||||
expect(parsed.timeout).toBe(5000)
|
||||
expect(parsed.workdir).toBe("/tmp")
|
||||
})
|
||||
test("rejects missing description (required by zod)", () => {
|
||||
expect(accepts(Bash, { command: "ls" })).toBe(false)
|
||||
})
|
||||
test("rejects missing command", () => {
|
||||
expect(accepts(Bash, { description: "list" })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("codesearch", () => {
|
||||
test("accepts query; tokensNum defaults to 5000", () => {
|
||||
expect(parse(CodeSearch, { query: "hooks" })).toEqual({ query: "hooks", tokensNum: 5000 })
|
||||
})
|
||||
test("accepts override tokensNum", () => {
|
||||
expect(parse(CodeSearch, { query: "hooks", tokensNum: 10000 }).tokensNum).toBe(10000)
|
||||
})
|
||||
test("rejects tokensNum under 1000", () => {
|
||||
expect(accepts(CodeSearch, { query: "x", tokensNum: 500 })).toBe(false)
|
||||
})
|
||||
test("rejects tokensNum over 50000", () => {
|
||||
expect(accepts(CodeSearch, { query: "x", tokensNum: 60000 })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edit", () => {
|
||||
test("accepts all four fields", () => {
|
||||
expect(parse(Edit, { filePath: "/a", oldString: "x", newString: "y", replaceAll: true })).toEqual({
|
||||
filePath: "/a",
|
||||
oldString: "x",
|
||||
newString: "y",
|
||||
replaceAll: true,
|
||||
})
|
||||
})
|
||||
test("replaceAll is optional", () => {
|
||||
const parsed = parse(Edit, { filePath: "/a", oldString: "x", newString: "y" })
|
||||
expect(parsed.replaceAll).toBeUndefined()
|
||||
})
|
||||
test("rejects missing filePath", () => {
|
||||
expect(accepts(Edit, { oldString: "x", newString: "y" })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("glob", () => {
|
||||
test("accepts pattern-only", () => {
|
||||
expect(parse(Glob, { pattern: "**/*.ts" })).toEqual({ pattern: "**/*.ts" })
|
||||
})
|
||||
test("accepts optional path", () => {
|
||||
expect(parse(Glob, { pattern: "**/*.ts", path: "/tmp" }).path).toBe("/tmp")
|
||||
})
|
||||
test("rejects missing pattern", () => {
|
||||
expect(accepts(Glob, {})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("grep", () => {
|
||||
test("accepts pattern-only", () => {
|
||||
expect(parse(Grep, { pattern: "TODO" })).toEqual({ pattern: "TODO" })
|
||||
})
|
||||
test("accepts optional path + include", () => {
|
||||
const parsed = parse(Grep, { pattern: "TODO", path: "/tmp", include: "*.ts" })
|
||||
expect(parsed.path).toBe("/tmp")
|
||||
expect(parsed.include).toBe("*.ts")
|
||||
})
|
||||
test("rejects missing pattern", () => {
|
||||
expect(accepts(Grep, {})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("invalid", () => {
|
||||
test("accepts tool + error", () => {
|
||||
expect(parse(Invalid, { tool: "foo", error: "bar" })).toEqual({ tool: "foo", error: "bar" })
|
||||
})
|
||||
test("rejects missing fields", () => {
|
||||
expect(accepts(Invalid, { tool: "foo" })).toBe(false)
|
||||
expect(accepts(Invalid, { error: "bar" })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("lsp", () => {
|
||||
test("accepts all fields", () => {
|
||||
const parsed = parse(Lsp, { operation: "hover", filePath: "/a.ts", line: 1, character: 1 })
|
||||
expect(parsed.operation).toBe("hover")
|
||||
})
|
||||
test("rejects line < 1", () => {
|
||||
expect(accepts(Lsp, { operation: "hover", filePath: "/a.ts", line: 0, character: 1 })).toBe(false)
|
||||
})
|
||||
test("rejects character < 1", () => {
|
||||
expect(accepts(Lsp, { operation: "hover", filePath: "/a.ts", line: 1, character: 0 })).toBe(false)
|
||||
})
|
||||
test("rejects unknown operation", () => {
|
||||
expect(accepts(Lsp, { operation: "bogus", filePath: "/a.ts", line: 1, character: 1 })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("plan", () => {
|
||||
test("accepts empty object", () => {
|
||||
expect(parse(Plan, {})).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("question", () => {
|
||||
test("accepts questions array", () => {
|
||||
const parsed = parse(Question, {
|
||||
questions: [
|
||||
{
|
||||
question: "pick one",
|
||||
header: "Header",
|
||||
custom: false,
|
||||
options: [{ label: "a", description: "desc" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(parsed.questions.length).toBe(1)
|
||||
})
|
||||
test("rejects missing questions", () => {
|
||||
expect(accepts(Question, {})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("read", () => {
|
||||
test("accepts filePath-only", () => {
|
||||
expect(parse(Read, { filePath: "/a" }).filePath).toBe("/a")
|
||||
})
|
||||
test("accepts optional offset + limit", () => {
|
||||
const parsed = parse(Read, { filePath: "/a", offset: 10, limit: 100 })
|
||||
expect(parsed.offset).toBe(10)
|
||||
expect(parsed.limit).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe("skill", () => {
|
||||
test("accepts name", () => {
|
||||
expect(parse(Skill, { name: "foo" }).name).toBe("foo")
|
||||
})
|
||||
test("rejects missing name", () => {
|
||||
expect(accepts(Skill, {})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("task", () => {
|
||||
test("accepts description + prompt + subagent_type", () => {
|
||||
const parsed = parse(Task, { description: "d", prompt: "p", subagent_type: "general" })
|
||||
expect(parsed.subagent_type).toBe("general")
|
||||
})
|
||||
test("rejects missing prompt", () => {
|
||||
expect(accepts(Task, { description: "d", subagent_type: "general" })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("todo", () => {
|
||||
test("accepts todos array", () => {
|
||||
const parsed = parse(Todo, {
|
||||
todos: [{ id: "t1", content: "do x", status: "pending", priority: "medium" }],
|
||||
})
|
||||
expect(parsed.todos.length).toBe(1)
|
||||
})
|
||||
test("rejects missing todos", () => {
|
||||
expect(accepts(Todo, {})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("webfetch", () => {
|
||||
test("accepts url-only", () => {
|
||||
expect(parse(WebFetch, { url: "https://example.com" }).url).toBe("https://example.com")
|
||||
})
|
||||
})
|
||||
|
||||
describe("websearch", () => {
|
||||
test("accepts query", () => {
|
||||
expect(parse(WebSearch, { query: "opencode" }).query).toBe("opencode")
|
||||
})
|
||||
})
|
||||
|
||||
describe("write", () => {
|
||||
test("accepts content + filePath", () => {
|
||||
expect(parse(Write, { content: "hi", filePath: "/a" })).toEqual({ content: "hi", filePath: "/a" })
|
||||
})
|
||||
test("rejects missing filePath", () => {
|
||||
expect(accepts(Write, { content: "hi" })).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||
import z from "zod"
|
||||
import { Effect, Layer, ManagedRuntime, Schema } from "effect"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { Tool } from "../../src/tool"
|
||||
import { Truncate } from "../../src/tool"
|
||||
|
||||
const runtime = ManagedRuntime.make(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer))
|
||||
|
||||
const params = z.object({ input: z.string() })
|
||||
const params = Schema.Struct({ input: Schema.String })
|
||||
|
||||
function makeTool(id: string, executeFn?: () => void) {
|
||||
return {
|
||||
@@ -56,4 +56,44 @@ describe("Tool.define", () => {
|
||||
|
||||
expect(first).not.toBe(second)
|
||||
})
|
||||
|
||||
test("execute receives decoded parameters", async () => {
|
||||
const parameters = Schema.Struct({
|
||||
count: Schema.NumberFromString.pipe(Schema.optional, Schema.withDecodingDefaultType(Effect.succeed(5))),
|
||||
})
|
||||
const calls: Array<Schema.Schema.Type<typeof parameters>> = []
|
||||
const info = await runtime.runPromise(
|
||||
Tool.define(
|
||||
"test-decoded",
|
||||
Effect.succeed({
|
||||
description: "test tool",
|
||||
parameters,
|
||||
execute(args: Schema.Schema.Type<typeof parameters>) {
|
||||
calls.push(args)
|
||||
return Effect.succeed({ title: "test", output: "ok", metadata: { truncated: false } })
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
const ctx: Tool.Context = {
|
||||
sessionID: SessionID.descending(),
|
||||
messageID: MessageID.ascending(),
|
||||
agent: "build",
|
||||
abort: new AbortController().signal,
|
||||
messages: [],
|
||||
metadata() {
|
||||
return Effect.void
|
||||
},
|
||||
ask() {
|
||||
return Effect.void
|
||||
},
|
||||
}
|
||||
const tool = await Effect.runPromise(info.init())
|
||||
const execute = tool.execute as unknown as (args: unknown, ctx: Tool.Context) => ReturnType<typeof tool.execute>
|
||||
|
||||
await Effect.runPromise(execute({}, ctx))
|
||||
await Effect.runPromise(execute({ count: "7" }, ctx))
|
||||
|
||||
expect(calls).toEqual([{ count: 5 }, { count: 7 }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -987,6 +987,371 @@ export type EventSessionDeleted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionEventSource = {
|
||||
start: number
|
||||
end: number
|
||||
text: string
|
||||
}
|
||||
|
||||
export type SessionEventFileAttachment = {
|
||||
uri: string
|
||||
mime: string
|
||||
name?: string
|
||||
description?: string
|
||||
source?: SessionEventSource
|
||||
}
|
||||
|
||||
export type SessionEventAgentAttachment = {
|
||||
name: string
|
||||
source?: SessionEventSource
|
||||
}
|
||||
|
||||
export type SessionEventPrompt = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "prompt"
|
||||
text: string
|
||||
files?: Array<SessionEventFileAttachment>
|
||||
agents?: Array<SessionEventAgentAttachment>
|
||||
}
|
||||
|
||||
export type EventPrompt = {
|
||||
type: "prompt"
|
||||
properties: SessionEventPrompt
|
||||
}
|
||||
|
||||
export type SessionEventSynthetic = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "synthetic"
|
||||
text: string
|
||||
}
|
||||
|
||||
export type EventSynthetic = {
|
||||
type: "synthetic"
|
||||
properties: SessionEventSynthetic
|
||||
}
|
||||
|
||||
export type SessionEventStepStarted = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "step.started"
|
||||
model: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventStepStarted = {
|
||||
type: "step.started"
|
||||
properties: SessionEventStepStarted
|
||||
}
|
||||
|
||||
export type SessionEventStepEnded = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "step.ended"
|
||||
reason: string
|
||||
cost: number
|
||||
tokens: {
|
||||
input: number
|
||||
output: number
|
||||
reasoning: number
|
||||
cache: {
|
||||
read: number
|
||||
write: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type EventStepEnded = {
|
||||
type: "step.ended"
|
||||
properties: SessionEventStepEnded
|
||||
}
|
||||
|
||||
export type SessionEventTextStarted = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "text.started"
|
||||
}
|
||||
|
||||
export type EventTextStarted = {
|
||||
type: "text.started"
|
||||
properties: SessionEventTextStarted
|
||||
}
|
||||
|
||||
export type SessionEventTextDelta = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "text.delta"
|
||||
delta: string
|
||||
}
|
||||
|
||||
export type EventTextDelta = {
|
||||
type: "text.delta"
|
||||
properties: SessionEventTextDelta
|
||||
}
|
||||
|
||||
export type SessionEventTextEnded = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "text.ended"
|
||||
text: string
|
||||
}
|
||||
|
||||
export type EventTextEnded = {
|
||||
type: "text.ended"
|
||||
properties: SessionEventTextEnded
|
||||
}
|
||||
|
||||
export type SessionEventReasoningStarted = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "reasoning.started"
|
||||
}
|
||||
|
||||
export type EventReasoningStarted = {
|
||||
type: "reasoning.started"
|
||||
properties: SessionEventReasoningStarted
|
||||
}
|
||||
|
||||
export type SessionEventReasoningDelta = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "reasoning.delta"
|
||||
delta: string
|
||||
}
|
||||
|
||||
export type EventReasoningDelta = {
|
||||
type: "reasoning.delta"
|
||||
properties: SessionEventReasoningDelta
|
||||
}
|
||||
|
||||
export type SessionEventReasoningEnded = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "reasoning.ended"
|
||||
text: string
|
||||
}
|
||||
|
||||
export type EventReasoningEnded = {
|
||||
type: "reasoning.ended"
|
||||
properties: SessionEventReasoningEnded
|
||||
}
|
||||
|
||||
export type SessionEventToolInputStarted = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "tool.input.started"
|
||||
callID: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type EventToolInputStarted = {
|
||||
type: "tool.input.started"
|
||||
properties: SessionEventToolInputStarted
|
||||
}
|
||||
|
||||
export type SessionEventToolInputDelta = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "tool.input.delta"
|
||||
callID: string
|
||||
delta: string
|
||||
}
|
||||
|
||||
export type EventToolInputDelta = {
|
||||
type: "tool.input.delta"
|
||||
properties: SessionEventToolInputDelta
|
||||
}
|
||||
|
||||
export type SessionEventToolInputEnded = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "tool.input.ended"
|
||||
callID: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export type EventToolInputEnded = {
|
||||
type: "tool.input.ended"
|
||||
properties: SessionEventToolInputEnded
|
||||
}
|
||||
|
||||
export type SessionEventToolCalled = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "tool.called"
|
||||
callID: string
|
||||
tool: string
|
||||
input: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
provider: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type EventToolCalled = {
|
||||
type: "tool.called"
|
||||
properties: SessionEventToolCalled
|
||||
}
|
||||
|
||||
export type SessionEventToolSuccess = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "tool.success"
|
||||
callID: string
|
||||
title: string
|
||||
output?: string
|
||||
attachments?: Array<SessionEventFileAttachment>
|
||||
provider: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type EventToolSuccess = {
|
||||
type: "tool.success"
|
||||
properties: SessionEventToolSuccess
|
||||
}
|
||||
|
||||
export type SessionEventToolError = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "tool.error"
|
||||
callID: string
|
||||
error: string
|
||||
provider: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type EventToolError = {
|
||||
type: "tool.error"
|
||||
properties: SessionEventToolError
|
||||
}
|
||||
|
||||
export type SessionEventRetryError = {
|
||||
message: string
|
||||
statusCode?: number
|
||||
isRetryable: boolean
|
||||
responseHeaders?: {
|
||||
[key: string]: string
|
||||
}
|
||||
responseBody?: string
|
||||
metadata?: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionEventRetried = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "retried"
|
||||
attempt: number
|
||||
error: SessionEventRetryError
|
||||
}
|
||||
|
||||
export type EventRetried = {
|
||||
type: "retried"
|
||||
properties: SessionEventRetried
|
||||
}
|
||||
|
||||
export type SessionEventCompacted = {
|
||||
id: string
|
||||
sessionID: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
timestamp: string
|
||||
type: "compacted"
|
||||
auto: boolean
|
||||
overflow?: boolean
|
||||
}
|
||||
|
||||
export type EventCompacted = {
|
||||
type: "compacted"
|
||||
properties: SessionEventCompacted
|
||||
}
|
||||
|
||||
export type SyncEventMessageUpdated = {
|
||||
type: "sync"
|
||||
name: "message.updated.1"
|
||||
@@ -1058,31 +1423,31 @@ export type SyncEventSessionUpdated = {
|
||||
data: {
|
||||
sessionID: string
|
||||
info: {
|
||||
id: string | null
|
||||
slug: string | null
|
||||
projectID: string | null
|
||||
workspaceID: string | null
|
||||
directory: string | null
|
||||
parentID: string | null
|
||||
summary: {
|
||||
id?: string | null
|
||||
slug?: string | null
|
||||
projectID?: string | null
|
||||
workspaceID?: string | null
|
||||
directory?: string | null
|
||||
parentID?: string | null
|
||||
summary?: {
|
||||
additions: number
|
||||
deletions: number
|
||||
files: number
|
||||
diffs?: Array<SnapshotFileDiff>
|
||||
} | null
|
||||
share?: {
|
||||
url: string | null
|
||||
url?: string | null
|
||||
}
|
||||
title: string | null
|
||||
version: string | null
|
||||
title?: string | null
|
||||
version?: string | null
|
||||
time?: {
|
||||
created: number | null
|
||||
updated: number | null
|
||||
compacting: number | null
|
||||
archived: number | null
|
||||
created?: number | null
|
||||
updated?: number | null
|
||||
compacting?: number | null
|
||||
archived?: number | null
|
||||
}
|
||||
permission: PermissionRuleset | null
|
||||
revert: {
|
||||
permission?: PermissionRuleset | null
|
||||
revert?: {
|
||||
messageID: string
|
||||
partID?: string
|
||||
snapshot?: string
|
||||
@@ -1104,6 +1469,168 @@ export type SyncEventSessionDeleted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventPrompt = {
|
||||
type: "sync"
|
||||
name: "prompt.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventPrompt
|
||||
}
|
||||
|
||||
export type SyncEventSynthetic = {
|
||||
type: "sync"
|
||||
name: "synthetic.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventSynthetic
|
||||
}
|
||||
|
||||
export type SyncEventStepStarted = {
|
||||
type: "sync"
|
||||
name: "step.started.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventStepStarted
|
||||
}
|
||||
|
||||
export type SyncEventStepEnded = {
|
||||
type: "sync"
|
||||
name: "step.ended.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventStepEnded
|
||||
}
|
||||
|
||||
export type SyncEventTextStarted = {
|
||||
type: "sync"
|
||||
name: "text.started.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventTextStarted
|
||||
}
|
||||
|
||||
export type SyncEventTextDelta = {
|
||||
type: "sync"
|
||||
name: "text.delta.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventTextDelta
|
||||
}
|
||||
|
||||
export type SyncEventTextEnded = {
|
||||
type: "sync"
|
||||
name: "text.ended.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventTextEnded
|
||||
}
|
||||
|
||||
export type SyncEventReasoningStarted = {
|
||||
type: "sync"
|
||||
name: "reasoning.started.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventReasoningStarted
|
||||
}
|
||||
|
||||
export type SyncEventReasoningDelta = {
|
||||
type: "sync"
|
||||
name: "reasoning.delta.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventReasoningDelta
|
||||
}
|
||||
|
||||
export type SyncEventReasoningEnded = {
|
||||
type: "sync"
|
||||
name: "reasoning.ended.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventReasoningEnded
|
||||
}
|
||||
|
||||
export type SyncEventToolInputStarted = {
|
||||
type: "sync"
|
||||
name: "tool.input.started.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventToolInputStarted
|
||||
}
|
||||
|
||||
export type SyncEventToolInputDelta = {
|
||||
type: "sync"
|
||||
name: "tool.input.delta.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventToolInputDelta
|
||||
}
|
||||
|
||||
export type SyncEventToolInputEnded = {
|
||||
type: "sync"
|
||||
name: "tool.input.ended.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventToolInputEnded
|
||||
}
|
||||
|
||||
export type SyncEventToolCalled = {
|
||||
type: "sync"
|
||||
name: "tool.called.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventToolCalled
|
||||
}
|
||||
|
||||
export type SyncEventToolSuccess = {
|
||||
type: "sync"
|
||||
name: "tool.success.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventToolSuccess
|
||||
}
|
||||
|
||||
export type SyncEventToolError = {
|
||||
type: "sync"
|
||||
name: "tool.error.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventToolError
|
||||
}
|
||||
|
||||
export type SyncEventRetried = {
|
||||
type: "sync"
|
||||
name: "retried.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventRetried
|
||||
}
|
||||
|
||||
export type SyncEventCompacted = {
|
||||
type: "sync"
|
||||
name: "compacted.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: SessionEventCompacted
|
||||
}
|
||||
|
||||
export type GlobalEvent = {
|
||||
directory: string
|
||||
project?: string
|
||||
@@ -1156,6 +1683,24 @@ export type GlobalEvent = {
|
||||
| EventSessionCreated
|
||||
| EventSessionUpdated
|
||||
| EventSessionDeleted
|
||||
| EventPrompt
|
||||
| EventSynthetic
|
||||
| EventStepStarted
|
||||
| EventStepEnded
|
||||
| EventTextStarted
|
||||
| EventTextDelta
|
||||
| EventTextEnded
|
||||
| EventReasoningStarted
|
||||
| EventReasoningDelta
|
||||
| EventReasoningEnded
|
||||
| EventToolInputStarted
|
||||
| EventToolInputDelta
|
||||
| EventToolInputEnded
|
||||
| EventToolCalled
|
||||
| EventToolSuccess
|
||||
| EventToolError
|
||||
| EventRetried
|
||||
| EventCompacted
|
||||
| SyncEventMessageUpdated
|
||||
| SyncEventMessageRemoved
|
||||
| SyncEventMessagePartUpdated
|
||||
@@ -1163,6 +1708,24 @@ export type GlobalEvent = {
|
||||
| SyncEventSessionCreated
|
||||
| SyncEventSessionUpdated
|
||||
| SyncEventSessionDeleted
|
||||
| SyncEventPrompt
|
||||
| SyncEventSynthetic
|
||||
| SyncEventStepStarted
|
||||
| SyncEventStepEnded
|
||||
| SyncEventTextStarted
|
||||
| SyncEventTextDelta
|
||||
| SyncEventTextEnded
|
||||
| SyncEventReasoningStarted
|
||||
| SyncEventReasoningDelta
|
||||
| SyncEventReasoningEnded
|
||||
| SyncEventToolInputStarted
|
||||
| SyncEventToolInputDelta
|
||||
| SyncEventToolInputEnded
|
||||
| SyncEventToolCalled
|
||||
| SyncEventToolSuccess
|
||||
| SyncEventToolError
|
||||
| SyncEventRetried
|
||||
| SyncEventCompacted
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1198,32 +1761,14 @@ export type ServerConfig = {
|
||||
|
||||
export type PermissionActionConfig = "ask" | "allow" | "deny"
|
||||
|
||||
export type PermissionObjectConfig = {
|
||||
[key: string]: PermissionActionConfig
|
||||
}
|
||||
|
||||
export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig
|
||||
|
||||
export type PermissionConfig =
|
||||
| PermissionActionConfig
|
||||
| {
|
||||
read?: PermissionRuleConfig
|
||||
edit?: PermissionRuleConfig
|
||||
glob?: PermissionRuleConfig
|
||||
grep?: PermissionRuleConfig
|
||||
list?: PermissionRuleConfig
|
||||
bash?: PermissionRuleConfig
|
||||
task?: PermissionRuleConfig
|
||||
external_directory?: PermissionRuleConfig
|
||||
todowrite?: PermissionActionConfig
|
||||
question?: PermissionActionConfig
|
||||
webfetch?: PermissionActionConfig
|
||||
websearch?: PermissionActionConfig
|
||||
codesearch?: PermissionActionConfig
|
||||
lsp?: PermissionRuleConfig
|
||||
doom_loop?: PermissionActionConfig
|
||||
skill?: PermissionRuleConfig
|
||||
[key: string]: PermissionRuleConfig | PermissionActionConfig | undefined
|
||||
[key: string]:
|
||||
| PermissionActionConfig
|
||||
| {
|
||||
[key: string]: PermissionActionConfig
|
||||
}
|
||||
}
|
||||
|
||||
export type AgentConfig = {
|
||||
@@ -2082,6 +2627,24 @@ export type Event =
|
||||
| EventSessionCreated
|
||||
| EventSessionUpdated
|
||||
| EventSessionDeleted
|
||||
| EventPrompt
|
||||
| EventSynthetic
|
||||
| EventStepStarted
|
||||
| EventStepEnded
|
||||
| EventTextStarted
|
||||
| EventTextDelta
|
||||
| EventTextEnded
|
||||
| EventReasoningStarted
|
||||
| EventReasoningDelta
|
||||
| EventReasoningEnded
|
||||
| EventToolInputStarted
|
||||
| EventToolInputDelta
|
||||
| EventToolInputEnded
|
||||
| EventToolCalled
|
||||
| EventToolSuccess
|
||||
| EventToolError
|
||||
| EventRetried
|
||||
| EventCompacted
|
||||
|
||||
export type McpStatusConnected = {
|
||||
status: "connected"
|
||||
|
||||
@@ -6804,7 +6804,6 @@
|
||||
},
|
||||
"duration": {
|
||||
"description": "Duration in milliseconds",
|
||||
"default": 5000,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
@@ -7638,20 +7637,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"event": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"const": "add"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": "change"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": "unlink"
|
||||
}
|
||||
]
|
||||
"type": "string",
|
||||
"enum": ["add", "change", "unlink"]
|
||||
}
|
||||
},
|
||||
"required": ["file", "event"]
|
||||
@@ -8507,7 +8494,6 @@
|
||||
},
|
||||
"duration": {
|
||||
"description": "Duration in milliseconds",
|
||||
"default": 5000,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
@@ -10570,8 +10556,7 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
@@ -10636,8 +10621,7 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["created", "updated", "compacting", "archived"]
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"anyOf": [
|
||||
@@ -10676,20 +10660,7 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"slug",
|
||||
"projectID",
|
||||
"workspaceID",
|
||||
"directory",
|
||||
"parentID",
|
||||
"summary",
|
||||
"title",
|
||||
"version",
|
||||
"permission",
|
||||
"revert"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "info"]
|
||||
|
||||
Reference in New Issue
Block a user