Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d81e6334 | ||
|
|
459af775e8 | ||
|
|
a771859362 | ||
|
|
31d01d404a | ||
|
|
814e83ffec | ||
|
|
4c3e65c877 | ||
|
|
98ea5b6e7e |
@@ -254,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:
|
||||
|
||||
@@ -284,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
|
||||
|
||||
@@ -313,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`
|
||||
@@ -346,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`
|
||||
@@ -354,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`
|
||||
@@ -376,7 +376,7 @@ piecewise.
|
||||
- [ ] `src/snapshot/index.ts`
|
||||
- [ ] `src/storage/db.ts`
|
||||
- [ ] `src/storage/storage.ts`
|
||||
- [x] `src/sync/index.ts` — public API (`SyncEvent.define`) is Schema-first; still stores derived zod `schema`/`properties` on each `Definition` as a compat bridge for `BusEvent` until `bus/bus-event.ts` migrates
|
||||
- [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`
|
||||
|
||||
@@ -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,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,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,3 @@
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { fn } from "@/util/fn"
|
||||
@@ -16,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"
|
||||
@@ -26,12 +25,11 @@ import { errorData } from "@/util/error"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { waitEvent } from "./util"
|
||||
import { WorkspaceContext } from "./workspace-context"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
||||
|
||||
export const Info = WorkspaceInfo.meta({
|
||||
ref: "Workspace",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
export const Info = WorkspaceInfoSchema
|
||||
export type Info = WorkspaceInfo
|
||||
|
||||
export const ConnectionStatus = Schema.Struct({
|
||||
workspaceID: WorkspaceID,
|
||||
@@ -75,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)
|
||||
|
||||
@@ -139,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,6 +3,7 @@ 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"
|
||||
@@ -25,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))),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -54,7 +47,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace created",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info),
|
||||
schema: resolver(Workspace.Info.zod),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -63,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,
|
||||
@@ -87,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)),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -130,7 +123,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
description: "Workspace removed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info.optional()),
|
||||
schema: resolver(Workspace.Info.zod.optional()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -140,7 +133,7 @@ export const WorkspaceRoutes = lazy(() =>
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: Workspace.Info.shape.id,
|
||||
id: zodObject(Workspace.Info).shape.id,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
@@ -170,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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -795,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"
|
||||
@@ -808,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"
|
||||
@@ -833,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),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -360,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,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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user