Merge branch 'dev' into fix/lsp-dead-root-prune
This commit is contained in:
commit
e8ba72bef6
45 changed files with 3282 additions and 1284 deletions
|
|
@ -155,7 +155,12 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||||
resetHeartbeat()
|
resetHeartbeat()
|
||||||
streamErrorLogged = false
|
streamErrorLogged = false
|
||||||
const directory = event.directory ?? "global"
|
const directory = event.directory ?? "global"
|
||||||
const payload = event.payload
|
if (event.payload.type === "sync") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = event.payload as Event
|
||||||
|
|
||||||
const k = key(directory, payload)
|
const k = key(directory, payload)
|
||||||
if (k) {
|
if (k) {
|
||||||
const i = coalesced.get(k)
|
const i = coalesced.get(k)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
CREATE TABLE `session_entry` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`session_id` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`time_created` integer NOT NULL,
|
||||||
|
`time_updated` integer NOT NULL,
|
||||||
|
`data` text NOT NULL,
|
||||||
|
CONSTRAINT `fk_session_entry_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `session_entry_session_idx` ON `session_entry` (`session_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `session_entry_session_type_idx` ON `session_entry` (`session_id`,`type`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `session_entry_time_created_idx` ON `session_entry` (`time_created`);
|
||||||
File diff suppressed because it is too large
Load diff
238
packages/opencode/specs/effect/facades.md
Normal file
238
packages/opencode/specs/effect/facades.md
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
# Facade removal checklist
|
||||||
|
|
||||||
|
Concrete inventory of the remaining `makeRuntime(...)`-backed service facades in `packages/opencode`.
|
||||||
|
|
||||||
|
As of 2026-04-13, latest `origin/dev`:
|
||||||
|
|
||||||
|
- `src/` still has 15 `makeRuntime(...)` call sites.
|
||||||
|
- 13 of those are still in scope for facade removal.
|
||||||
|
- 2 are excluded from this checklist: `bus/index.ts` and `effect/cross-spawn-spawner.ts`.
|
||||||
|
|
||||||
|
Recent progress:
|
||||||
|
|
||||||
|
- Wave 1 is merged: `Pty`, `Skill`, `Vcs`, `ToolRegistry`, `Auth`.
|
||||||
|
- Wave 2 is merged: `Config`, `Provider`, `File`, `LSP`, `MCP`.
|
||||||
|
|
||||||
|
## Priority hotspots
|
||||||
|
|
||||||
|
- `server/instance/session.ts` still depends on `Session`, `SessionPrompt`, `SessionRevert`, `SessionCompaction`, `SessionSummary`, `ShareSession`, `Agent`, and `Permission` facades.
|
||||||
|
- `src/effect/app-runtime.ts` still references many facade namespaces directly, so it should stay in view during each deletion.
|
||||||
|
|
||||||
|
## Completed Batches
|
||||||
|
|
||||||
|
Low-risk batch, all merged:
|
||||||
|
|
||||||
|
1. `src/pty/index.ts`
|
||||||
|
2. `src/skill/index.ts`
|
||||||
|
3. `src/project/vcs.ts`
|
||||||
|
4. `src/tool/registry.ts`
|
||||||
|
5. `src/auth/index.ts`
|
||||||
|
|
||||||
|
Caller-heavy batch, all merged:
|
||||||
|
|
||||||
|
1. `src/config/config.ts`
|
||||||
|
2. `src/provider/provider.ts`
|
||||||
|
3. `src/file/index.ts`
|
||||||
|
4. `src/lsp/index.ts`
|
||||||
|
5. `src/mcp/index.ts`
|
||||||
|
|
||||||
|
Shared pattern:
|
||||||
|
|
||||||
|
- one service file still exports `makeRuntime(...)` + async facades
|
||||||
|
- one or two route or CLI entrypoints call those facades directly
|
||||||
|
- tests call the facade directly and need to switch to `yield* svc.method(...)`
|
||||||
|
- once callers are gone, delete `makeRuntime(...)`, remove async facade exports, and drop the `makeRuntime` import
|
||||||
|
|
||||||
|
## Done means
|
||||||
|
|
||||||
|
For each service in the low-risk batch, the work is complete only when all of these are true:
|
||||||
|
|
||||||
|
1. all production callers stop using `Namespace.method(...)` facade calls
|
||||||
|
2. all direct test callers stop using the facade and instead yield the service from context
|
||||||
|
3. the service file no longer has `makeRuntime(...)`
|
||||||
|
4. the service file no longer exports runtime-backed facade helpers
|
||||||
|
5. `grep` for the migrated facade methods only finds the service implementation itself or unrelated names
|
||||||
|
|
||||||
|
## Caller templates
|
||||||
|
|
||||||
|
### Route handlers
|
||||||
|
|
||||||
|
Use one `AppRuntime.runPromise(Effect.gen(...))` body and yield the service inside it.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const value = await AppRuntime.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const pty = yield* Pty.Service
|
||||||
|
return yield* pty.list()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
If two service calls are independent, keep them in the same effect body and use `Effect.all(...)`.
|
||||||
|
|
||||||
|
### Plain async CLI or script entrypoints
|
||||||
|
|
||||||
|
If the caller is not itself an Effect service yet, still prefer one contiguous `AppRuntime.runPromise(Effect.gen(...))` block for the whole unit of work.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const skills = await AppRuntime.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const auth = yield* Auth.Service
|
||||||
|
const skill = yield* Skill.Service
|
||||||
|
yield* auth.set(key, info)
|
||||||
|
return yield* skill.all()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Only fall back to `AppRuntime.runPromise(Service.use(...))` for truly isolated one-off calls or awkward callback boundaries. Do not stack multiple tiny `runPromise(...)` calls in the same contiguous workflow.
|
||||||
|
|
||||||
|
This is the right intermediate state. Do not block facade removal on effectifying the whole CLI file.
|
||||||
|
|
||||||
|
### Bootstrap or fire-and-forget startup code
|
||||||
|
|
||||||
|
If the old facade call existed only to kick off initialization, call the service through the existing runtime for that file.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
void BootstrapRuntime.runPromise(Vcs.Service.use((svc) => svc.init()))
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not reintroduce a dedicated runtime in the service just for bootstrap.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
Convert facade tests to full effect style.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it.effect("does the thing", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const svc = yield* Pty.Service
|
||||||
|
const info = yield* svc.create({ command: "cat", title: "a" })
|
||||||
|
yield* svc.remove(info.id)
|
||||||
|
}).pipe(Effect.provide(Pty.defaultLayer)),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
If the repo test already uses `testEffect(...)`, prefer `testEffect(Service.defaultLayer)` and `yield* Service.Service` inside the test body.
|
||||||
|
|
||||||
|
Do not route tests through `AppRuntime` unless the test is explicitly exercising the app runtime. For facade removal, tests should usually provide the specific service layer they need.
|
||||||
|
|
||||||
|
If the test uses `provideTmpdirInstance(...)`, remember that fixture needs a live `ChildProcessSpawner` layer. For services whose `defaultLayer` does not already provide that infra, prefer the repo-standard cross-spawn layer:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const infra = CrossSpawnSpawner.defaultLayer
|
||||||
|
|
||||||
|
const it = testEffect(Layer.mergeAll(MyService.defaultLayer, infra))
|
||||||
|
```
|
||||||
|
|
||||||
|
Without that extra layer, tests fail at runtime with `Service not found: effect/process/ChildProcessSpawner`.
|
||||||
|
|
||||||
|
## Questions already answered
|
||||||
|
|
||||||
|
### Do we need to effectify the whole caller first?
|
||||||
|
|
||||||
|
No.
|
||||||
|
|
||||||
|
- route files: compose the handler with `AppRuntime.runPromise(Effect.gen(...))`
|
||||||
|
- CLI and scripts: use `AppRuntime.runPromise(Service.use(...))`
|
||||||
|
- bootstrap: use the existing bootstrap runtime
|
||||||
|
|
||||||
|
Facade removal does not require a bigger refactor than that.
|
||||||
|
|
||||||
|
### Should tests keep calling the namespace from async test bodies?
|
||||||
|
|
||||||
|
No. Convert them now.
|
||||||
|
|
||||||
|
The end state is `yield* svc.method(...)`, not `await Namespace.method(...)` inside `async` tests.
|
||||||
|
|
||||||
|
### Should we keep `runPromise` exported for convenience?
|
||||||
|
|
||||||
|
No. For this batch the goal is to delete the service-local runtime entirely.
|
||||||
|
|
||||||
|
### What if a route has websocket callbacks or nested async handlers?
|
||||||
|
|
||||||
|
Keep the route shape, but replace each facade call with `AppRuntime.runPromise(Service.use(...))` or wrap the surrounding async section in one `Effect.gen(...)` when practical. Do not keep the service facade just because the route has callback-shaped code.
|
||||||
|
|
||||||
|
### Should we use one `runPromise` per service call?
|
||||||
|
|
||||||
|
No.
|
||||||
|
|
||||||
|
Default to one contiguous `AppRuntime.runPromise(Effect.gen(...))` block per handler, command, or workflow. Yield every service you need inside that block.
|
||||||
|
|
||||||
|
Multiple tiny `runPromise(...)` calls are only acceptable when the caller structure forces it, such as websocket lifecycle callbacks, external callback APIs, or genuinely unrelated one-off operations.
|
||||||
|
|
||||||
|
### Should we wrap a single service expression in `Effect.gen(...)`?
|
||||||
|
|
||||||
|
Usually no.
|
||||||
|
|
||||||
|
Prefer the direct form when there is only one expression:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await AppRuntime.runPromise(File.Service.use((svc) => svc.read(path)))
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `Effect.gen(...)` when the workflow actually needs multiple yielded values or branching.
|
||||||
|
|
||||||
|
## Learnings
|
||||||
|
|
||||||
|
These were the recurring mistakes and useful corrections from the first two batches:
|
||||||
|
|
||||||
|
1. Tests should usually provide the specific service layer, not `AppRuntime`.
|
||||||
|
2. If a test uses `provideTmpdirInstance(...)` and needs child processes, prefer `CrossSpawnSpawner.defaultLayer`.
|
||||||
|
3. Instance-scoped services may need both the service layer and the right instance fixture. `File` tests, for example, needed `provideInstance(...)` plus `File.defaultLayer`.
|
||||||
|
4. Do not wrap a single `Service.use(...)` call in `Effect.gen(...)` just to return it. Use the direct form.
|
||||||
|
5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
|
||||||
|
6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
|
||||||
|
|
||||||
|
## Next batch
|
||||||
|
|
||||||
|
Recommended next five, in order:
|
||||||
|
|
||||||
|
1. `src/permission/index.ts`
|
||||||
|
2. `src/agent/agent.ts`
|
||||||
|
3. `src/session/summary.ts`
|
||||||
|
4. `src/session/revert.ts`
|
||||||
|
5. `src/mcp/auth.ts`
|
||||||
|
|
||||||
|
Why this batch:
|
||||||
|
|
||||||
|
- It keeps pushing the session-adjacent cleanup without jumping straight into `session/index.ts` or `session/prompt.ts`.
|
||||||
|
- `Permission`, `Agent`, `SessionSummary`, and `SessionRevert` all reduce fanout in `server/instance/session.ts`.
|
||||||
|
- `McpAuth` is small and closely related to the just-landed `MCP` cleanup.
|
||||||
|
|
||||||
|
After that batch, the expected follow-up is the main session cluster:
|
||||||
|
|
||||||
|
1. `src/session/index.ts`
|
||||||
|
2. `src/session/prompt.ts`
|
||||||
|
3. `src/session/compaction.ts`
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] `src/session/index.ts` (`Session`) - facades: `create`, `fork`, `get`, `setTitle`, `setArchived`, `setPermission`, `setRevert`, `messages`, `children`, `remove`, `updateMessage`, `removeMessage`, `removePart`, `updatePart`; main callers: `server/instance/session.ts`, `cli/cmd/session.ts`, `cli/cmd/export.ts`, `cli/cmd/github.ts`; tests: `test/server/session-actions.test.ts`, `test/server/session-list.test.ts`, `test/server/global-session-list.test.ts`
|
||||||
|
- [ ] `src/session/prompt.ts` (`SessionPrompt`) - facades: `prompt`, `resolvePromptParts`, `cancel`, `loop`, `shell`, `command`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`; tests: `test/session/prompt.test.ts`, `test/session/prompt-effect.test.ts`, `test/session/structured-output-integration.test.ts`
|
||||||
|
- [ ] `src/session/revert.ts` (`SessionRevert`) - facades: `revert`, `unrevert`, `cleanup`; main callers: `server/instance/session.ts`; tests: `test/session/revert-compact.test.ts`
|
||||||
|
- [ ] `src/session/compaction.ts` (`SessionCompaction`) - facades: `isOverflow`, `prune`, `create`; main callers: `server/instance/session.ts`; tests: `test/session/compaction.test.ts`
|
||||||
|
- [ ] `src/session/summary.ts` (`SessionSummary`) - facades: `summarize`, `diff`; main callers: `session/prompt.ts`, `session/processor.ts`, `server/instance/session.ts`; tests: `test/session/snapshot-tool-race.test.ts`
|
||||||
|
- [ ] `src/share/session.ts` (`ShareSession`) - facades: `create`, `share`, `unshare`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`
|
||||||
|
- [ ] `src/agent/agent.ts` (`Agent`) - facades: `get`, `list`, `defaultAgent`, `generate`; main callers: `cli/cmd/agent.ts`, `server/instance/session.ts`, `server/instance/experimental.ts`; tests: `test/agent/agent.test.ts`
|
||||||
|
- [ ] `src/permission/index.ts` (`Permission`) - facades: `ask`, `reply`, `list`; main callers: `server/instance/permission.ts`, `server/instance/session.ts`, `session/llm.ts`; tests: `test/permission/next.test.ts`
|
||||||
|
- [x] `src/file/index.ts` (`File`) - facades removed and merged.
|
||||||
|
- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged.
|
||||||
|
- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged.
|
||||||
|
- [x] `src/config/config.ts` (`Config`) - facades removed and merged.
|
||||||
|
- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged.
|
||||||
|
- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged.
|
||||||
|
- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged.
|
||||||
|
- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged.
|
||||||
|
- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged.
|
||||||
|
- [ ] `src/worktree/index.ts` (`Worktree`) - facades: `makeWorktreeInfo`, `createFromInfo`, `create`, `remove`, `reset`; main callers: `control-plane/adaptors/worktree.ts`, `server/instance/experimental.ts`; tests: `test/project/worktree.test.ts`, `test/project/worktree-remove.test.ts`
|
||||||
|
- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged.
|
||||||
|
- [ ] `src/mcp/auth.ts` (`McpAuth`) - facades: `get`, `getForUrl`, `all`, `set`, `remove`, `updateTokens`, `updateClientInfo`, `updateCodeVerifier`, `updateOAuthState`; main callers: `mcp/oauth-provider.ts`, `cli/cmd/mcp.ts`; tests: `test/mcp/oauth-auto-connect.test.ts`
|
||||||
|
- [ ] `src/plugin/index.ts` (`Plugin`) - facades: `trigger`, `list`, `init`; main callers: `agent/agent.ts`, `session/llm.ts`, `project/bootstrap.ts`; tests: `test/plugin/trigger.test.ts`, `test/provider/provider.test.ts`
|
||||||
|
- [ ] `src/project/project.ts` (`Project`) - facades: `fromDirectory`, `discover`, `initGit`, `update`, `sandboxes`, `addSandbox`, `removeSandbox`; main callers: `project/instance.ts`, `server/instance/project.ts`, `server/instance/experimental.ts`; tests: `test/project/project.test.ts`, `test/project/migrate-global.test.ts`
|
||||||
|
- [ ] `src/snapshot/index.ts` (`Snapshot`) - facades: `init`, `track`, `patch`, `restore`, `revert`, `diff`, `diffFull`; main callers: `project/bootstrap.ts`, `cli/cmd/debug/snapshot.ts`; tests: `test/snapshot/snapshot.test.ts`, `test/session/revert-compact.test.ts`
|
||||||
|
|
||||||
|
## Excluded `makeRuntime(...)` sites
|
||||||
|
|
||||||
|
- `src/bus/index.ts` - core bus plumbing, not a normal facade-removal target.
|
||||||
|
- `src/effect/cross-spawn-spawner.ts` - runtime helper for `ChildProcessSpawner`, not a service namespace facade.
|
||||||
|
|
@ -180,7 +180,7 @@ That is fine for leaf files like `schema.ts`. Keep the service surface in the ow
|
||||||
|
|
||||||
Service-shape migrated (single namespace, traced methods, `InstanceState` where needed).
|
Service-shape migrated (single namespace, traced methods, `InstanceState` where needed).
|
||||||
|
|
||||||
This checklist is only about the service shape migration. Many of these services still keep `makeRuntime(...)` plus async facade exports; that facade-removal phase is tracked separately in [Destroying the facades](#destroying-the-facades).
|
This checklist is only about the service shape migration. Many of these services still keep `makeRuntime(...)` plus async facade exports; that facade-removal phase is tracked separately in `facades.md`.
|
||||||
|
|
||||||
- [x] `Account` — `account/index.ts`
|
- [x] `Account` — `account/index.ts`
|
||||||
- [x] `Agent` — `agent/agent.ts`
|
- [x] `Agent` — `agent/agent.ts`
|
||||||
|
|
@ -263,7 +263,7 @@ Tool-specific filesystem cleanup notes live in `tools.md`.
|
||||||
|
|
||||||
## Destroying the facades
|
## Destroying the facades
|
||||||
|
|
||||||
This phase is still broadly open. As of 2026-04-11 there are still 31 `makeRuntime(...)` call sites under `src/`, and many service namespaces still export async facade helpers like `export async function read(...) { return runPromise(...) }`.
|
This phase is still broadly open. As of 2026-04-13 there are still 15 `makeRuntime(...)` call sites under `src/`, with 13 still in scope for facade removal. The live checklist now lives in `facades.md`.
|
||||||
|
|
||||||
These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
|
These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import type { ACPConfig } from "./types"
|
||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
import { ModelID, ProviderID } from "../provider/schema"
|
import { ModelID, ProviderID } from "../provider/schema"
|
||||||
import { Agent as AgentModule } from "../agent/agent"
|
import { Agent as AgentModule } from "../agent/agent"
|
||||||
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { MessageV2 } from "@/session/message-v2"
|
import { MessageV2 } from "@/session/message-v2"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
|
|
@ -1166,7 +1167,7 @@ export namespace ACP {
|
||||||
this.sessionManager.get(sessionId).modeId ||
|
this.sessionManager.get(sessionId).modeId ||
|
||||||
(await (async () => {
|
(await (async () => {
|
||||||
if (!availableModes.length) return undefined
|
if (!availableModes.length) return undefined
|
||||||
const defaultAgentName = await AgentModule.defaultAgent()
|
const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))
|
||||||
const resolvedModeId =
|
const resolvedModeId =
|
||||||
availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
|
availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
|
||||||
this.sessionManager.setMode(sessionId, resolvedModeId)
|
this.sessionManager.setMode(sessionId, resolvedModeId)
|
||||||
|
|
@ -1367,7 +1368,8 @@ export namespace ACP {
|
||||||
if (!current) {
|
if (!current) {
|
||||||
this.sessionManager.setModel(session.id, model)
|
this.sessionManager.setModel(session.id, model)
|
||||||
}
|
}
|
||||||
const agent = session.modeId ?? (await AgentModule.defaultAgent())
|
const agent =
|
||||||
|
session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())))
|
||||||
|
|
||||||
const parts: Array<
|
const parts: Array<
|
||||||
| { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
|
| { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import { Plugin } from "@/plugin"
|
||||||
import { Skill } from "../skill"
|
import { Skill } from "../skill"
|
||||||
import { Effect, Context, Layer } from "effect"
|
import { Effect, Context, Layer } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
|
||||||
|
|
||||||
export namespace Agent {
|
export namespace Agent {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
|
|
@ -404,22 +403,4 @@ export namespace Agent {
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Skill.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
||||||
|
|
||||||
export async function get(agent: string) {
|
|
||||||
return runPromise((svc) => svc.get(agent))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function list() {
|
|
||||||
return runPromise((svc) => svc.list())
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function defaultAgent() {
|
|
||||||
return runPromise((svc) => svc.defaultAgent())
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generate(input: { description: string; model?: { providerID: ProviderID; modelID: ModelID } }) {
|
|
||||||
return runPromise((svc) => svc.generate(input))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,25 +16,18 @@ export namespace BusEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function payloads() {
|
export function payloads() {
|
||||||
return z
|
return registry
|
||||||
.discriminatedUnion(
|
.entries()
|
||||||
"type",
|
.map(([type, def]) => {
|
||||||
registry
|
return z
|
||||||
.entries()
|
.object({
|
||||||
.map(([type, def]) => {
|
type: z.literal(type),
|
||||||
return z
|
properties: def.properties,
|
||||||
.object({
|
})
|
||||||
type: z.literal(type),
|
.meta({
|
||||||
properties: def.properties,
|
ref: "Event" + "." + def.type,
|
||||||
})
|
|
||||||
.meta({
|
|
||||||
ref: "Event" + "." + def.type,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.toArray() as any,
|
|
||||||
)
|
|
||||||
.meta({
|
|
||||||
ref: "Event",
|
|
||||||
})
|
})
|
||||||
|
.toArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent/agent"
|
||||||
|
|
@ -110,7 +111,9 @@ const AgentCreateCommand = cmd({
|
||||||
const spinner = prompts.spinner()
|
const spinner = prompts.spinner()
|
||||||
spinner.start("Generating agent configuration...")
|
spinner.start("Generating agent configuration...")
|
||||||
const model = args.model ? Provider.parseModel(args.model) : undefined
|
const model = args.model ? Provider.parseModel(args.model) : undefined
|
||||||
const generated = await Agent.generate({ description, model }).catch((error) => {
|
const generated = await AppRuntime.runPromise(
|
||||||
|
Agent.Service.use((svc) => svc.generate({ description, model })),
|
||||||
|
).catch((error) => {
|
||||||
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
||||||
if (isFullyNonInteractive) process.exit(1)
|
if (isFullyNonInteractive) process.exit(1)
|
||||||
throw new UI.CancelledError()
|
throw new UI.CancelledError()
|
||||||
|
|
@ -220,7 +223,7 @@ const AgentListCommand = cmd({
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: process.cwd(),
|
directory: process.cwd(),
|
||||||
async fn() {
|
async fn() {
|
||||||
const agents = await Agent.list()
|
const agents = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.list()))
|
||||||
const sortedAgents = agents.sort((a, b) => {
|
const sortedAgents = agents.sort((a, b) => {
|
||||||
if (a.native !== b.native) {
|
if (a.native !== b.native) {
|
||||||
return a.native ? -1 : 1
|
return a.native ? -1 : 1
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const AgentCommand = cmd({
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const agentName = args.name as string
|
const agentName = args.name as string
|
||||||
const agent = await Agent.get(agentName)
|
const agent = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.get(agentName)))
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
|
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
|
||||||
|
|
|
||||||
|
|
@ -361,7 +361,6 @@ export const McpLogoutCommand = cmd({
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Logout")
|
prompts.intro("MCP OAuth Logout")
|
||||||
|
|
||||||
const authPath = path.join(Global.Path.data, "mcp-auth.json")
|
|
||||||
const credentials = await AppRuntime.runPromise(McpAuth.Service.use((auth) => auth.all()))
|
const credentials = await AppRuntime.runPromise(McpAuth.Service.use((auth) => auth.all()))
|
||||||
const serverNames = Object.keys(credentials)
|
const serverNames = Object.keys(credentials)
|
||||||
|
|
||||||
|
|
@ -717,6 +716,11 @@ export const McpDebugCommand = cmd({
|
||||||
|
|
||||||
// Try to discover OAuth metadata
|
// Try to discover OAuth metadata
|
||||||
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
|
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
|
||||||
|
const auth = await AppRuntime.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
return yield* McpAuth.Service
|
||||||
|
}),
|
||||||
|
)
|
||||||
const authProvider = new McpOAuthProvider(
|
const authProvider = new McpOAuthProvider(
|
||||||
serverName,
|
serverName,
|
||||||
serverConfig.url,
|
serverConfig.url,
|
||||||
|
|
@ -729,6 +733,7 @@ export const McpDebugCommand = cmd({
|
||||||
{
|
{
|
||||||
onRedirect: async () => {},
|
onRedirect: async () => {},
|
||||||
},
|
},
|
||||||
|
auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
prompts.log.info("Testing OAuth flow (without completing authorization)...")
|
prompts.log.info("Testing OAuth flow (without completing authorization)...")
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { SkillTool } from "../../tool/skill"
|
||||||
import { BashTool } from "../../tool/bash"
|
import { BashTool } from "../../tool/bash"
|
||||||
import { TodoWriteTool } from "../../tool/todo"
|
import { TodoWriteTool } from "../../tool/todo"
|
||||||
import { Locale } from "../../util/locale"
|
import { Locale } from "../../util/locale"
|
||||||
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
|
|
||||||
type ToolProps<T> = {
|
type ToolProps<T> = {
|
||||||
input: Tool.InferParameters<T>
|
input: Tool.InferParameters<T>
|
||||||
|
|
@ -573,6 +574,7 @@ export const RunCommand = cmd({
|
||||||
// Validate agent if specified
|
// Validate agent if specified
|
||||||
const agent = await (async () => {
|
const agent = await (async () => {
|
||||||
if (!args.agent) return undefined
|
if (!args.agent) return undefined
|
||||||
|
const name = args.agent
|
||||||
|
|
||||||
// When attaching, validate against the running server instead of local Instance state.
|
// When attaching, validate against the running server instead of local Instance state.
|
||||||
if (args.attach) {
|
if (args.attach) {
|
||||||
|
|
@ -590,12 +592,12 @@ export const RunCommand = cmd({
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = modes.find((a) => a.name === args.agent)
|
const agent = modes.find((a) => a.name === name)
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
`agent "${args.agent}" not found. Falling back to default agent`,
|
`agent "${name}" not found. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
@ -604,20 +606,20 @@ export const RunCommand = cmd({
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
`agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`,
|
`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return args.agent
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = await Agent.get(args.agent)
|
const entry = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.get(name)))
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
`agent "${args.agent}" not found. Falling back to default agent`,
|
`agent "${name}" not found. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
@ -625,11 +627,11 @@ export const RunCommand = cmd({
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
`agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`,
|
`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return args.agent
|
return name
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const sessionID = await session(sdk)
|
const sessionID = await session(sdk)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ export function useEvent() {
|
||||||
|
|
||||||
function subscribe(handler: (event: Event) => void) {
|
function subscribe(handler: (event: Event) => void) {
|
||||||
return sdk.event.on("event", (event) => {
|
return sdk.event.on("event", (event) => {
|
||||||
|
if (event.payload.type === "sync") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Special hack for truly global events
|
// Special hack for truly global events
|
||||||
if (event.directory === "global") {
|
if (event.directory === "global") {
|
||||||
handler(event.payload)
|
handler(event.payload)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export namespace Identifier {
|
||||||
pty: "pty",
|
pty: "pty",
|
||||||
tool: "tool",
|
tool: "tool",
|
||||||
workspace: "wrk",
|
workspace: "wrk",
|
||||||
|
entry: "ent",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function schema(prefix: keyof typeof prefixes) {
|
export function schema(prefix: keyof typeof prefixes) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import z from "zod"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
import { AppFileSystem } from "@/filesystem"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
|
||||||
|
|
||||||
export namespace McpAuth {
|
export namespace McpAuth {
|
||||||
export const Tokens = z.object({
|
export const Tokens = z.object({
|
||||||
|
|
@ -142,32 +141,4 @@ export namespace McpAuth {
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
||||||
|
|
||||||
// Async facades for backward compat (used by McpOAuthProvider, CLI)
|
|
||||||
|
|
||||||
export const get = async (mcpName: string) => runPromise((svc) => svc.get(mcpName))
|
|
||||||
|
|
||||||
export const getForUrl = async (mcpName: string, serverUrl: string) =>
|
|
||||||
runPromise((svc) => svc.getForUrl(mcpName, serverUrl))
|
|
||||||
|
|
||||||
export const all = async () => runPromise((svc) => svc.all())
|
|
||||||
|
|
||||||
export const set = async (mcpName: string, entry: Entry, serverUrl?: string) =>
|
|
||||||
runPromise((svc) => svc.set(mcpName, entry, serverUrl))
|
|
||||||
|
|
||||||
export const remove = async (mcpName: string) => runPromise((svc) => svc.remove(mcpName))
|
|
||||||
|
|
||||||
export const updateTokens = async (mcpName: string, tokens: Tokens, serverUrl?: string) =>
|
|
||||||
runPromise((svc) => svc.updateTokens(mcpName, tokens, serverUrl))
|
|
||||||
|
|
||||||
export const updateClientInfo = async (mcpName: string, clientInfo: ClientInfo, serverUrl?: string) =>
|
|
||||||
runPromise((svc) => svc.updateClientInfo(mcpName, clientInfo, serverUrl))
|
|
||||||
|
|
||||||
export const updateCodeVerifier = async (mcpName: string, codeVerifier: string) =>
|
|
||||||
runPromise((svc) => svc.updateCodeVerifier(mcpName, codeVerifier))
|
|
||||||
|
|
||||||
export const updateOAuthState = async (mcpName: string, oauthState: string) =>
|
|
||||||
runPromise((svc) => svc.updateOAuthState(mcpName, oauthState))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,7 @@ export namespace MCP {
|
||||||
log.info("oauth redirect requested", { key, url: url.toString() })
|
log.info("oauth redirect requested", { key, url: url.toString() })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
auth,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -744,6 +745,7 @@ export namespace MCP {
|
||||||
capturedUrl = url
|
capturedUrl = url
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider })
|
const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider })
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import type {
|
||||||
OAuthClientInformation,
|
OAuthClientInformation,
|
||||||
OAuthClientInformationFull,
|
OAuthClientInformationFull,
|
||||||
} from "@modelcontextprotocol/sdk/shared/auth.js"
|
} from "@modelcontextprotocol/sdk/shared/auth.js"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { McpAuth } from "./auth"
|
import { McpAuth } from "./auth"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
|
||||||
|
|
@ -30,6 +31,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||||
private serverUrl: string,
|
private serverUrl: string,
|
||||||
private config: McpOAuthConfig,
|
private config: McpOAuthConfig,
|
||||||
private callbacks: McpOAuthCallbacks,
|
private callbacks: McpOAuthCallbacks,
|
||||||
|
private auth: McpAuth.Interface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get redirectUrl(): string {
|
get redirectUrl(): string {
|
||||||
|
|
@ -61,7 +63,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||||
|
|
||||||
// Check stored client info (from dynamic registration)
|
// Check stored client info (from dynamic registration)
|
||||||
// Use getForUrl to validate credentials are for the current server URL
|
// Use getForUrl to validate credentials are for the current server URL
|
||||||
const entry = await McpAuth.getForUrl(this.mcpName, this.serverUrl)
|
const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
|
||||||
if (entry?.clientInfo) {
|
if (entry?.clientInfo) {
|
||||||
// Check if client secret has expired
|
// Check if client secret has expired
|
||||||
if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
|
if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
|
||||||
|
|
@ -79,15 +81,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
|
async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
|
||||||
await McpAuth.updateClientInfo(
|
await Effect.runPromise(
|
||||||
this.mcpName,
|
this.auth.updateClientInfo(
|
||||||
{
|
this.mcpName,
|
||||||
clientId: info.client_id,
|
{
|
||||||
clientSecret: info.client_secret,
|
clientId: info.client_id,
|
||||||
clientIdIssuedAt: info.client_id_issued_at,
|
clientSecret: info.client_secret,
|
||||||
clientSecretExpiresAt: info.client_secret_expires_at,
|
clientIdIssuedAt: info.client_id_issued_at,
|
||||||
},
|
clientSecretExpiresAt: info.client_secret_expires_at,
|
||||||
this.serverUrl,
|
},
|
||||||
|
this.serverUrl,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
log.info("saved dynamically registered client", {
|
log.info("saved dynamically registered client", {
|
||||||
mcpName: this.mcpName,
|
mcpName: this.mcpName,
|
||||||
|
|
@ -97,7 +101,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||||
|
|
||||||
async tokens(): Promise<OAuthTokens | undefined> {
|
async tokens(): Promise<OAuthTokens | undefined> {
|
||||||
// Use getForUrl to validate tokens are for the current server URL
|
// Use getForUrl to validate tokens are for the current server URL
|
||||||
const entry = await McpAuth.getForUrl(this.mcpName, this.serverUrl)
|
const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
|
||||||
if (!entry?.tokens) return undefined
|
if (!entry?.tokens) return undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -112,15 +116,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
||||||
await McpAuth.updateTokens(
|
await Effect.runPromise(
|
||||||
this.mcpName,
|
this.auth.updateTokens(
|
||||||
{
|
this.mcpName,
|
||||||
accessToken: tokens.access_token,
|
{
|
||||||
refreshToken: tokens.refresh_token,
|
accessToken: tokens.access_token,
|
||||||
expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
|
refreshToken: tokens.refresh_token,
|
||||||
scope: tokens.scope,
|
expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
|
||||||
},
|
scope: tokens.scope,
|
||||||
this.serverUrl,
|
},
|
||||||
|
this.serverUrl,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
log.info("saved oauth tokens", { mcpName: this.mcpName })
|
log.info("saved oauth tokens", { mcpName: this.mcpName })
|
||||||
}
|
}
|
||||||
|
|
@ -131,11 +137,11 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveCodeVerifier(codeVerifier: string): Promise<void> {
|
async saveCodeVerifier(codeVerifier: string): Promise<void> {
|
||||||
await McpAuth.updateCodeVerifier(this.mcpName, codeVerifier)
|
await Effect.runPromise(this.auth.updateCodeVerifier(this.mcpName, codeVerifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
async codeVerifier(): Promise<string> {
|
async codeVerifier(): Promise<string> {
|
||||||
const entry = await McpAuth.get(this.mcpName)
|
const entry = await Effect.runPromise(this.auth.get(this.mcpName))
|
||||||
if (!entry?.codeVerifier) {
|
if (!entry?.codeVerifier) {
|
||||||
throw new Error(`No code verifier saved for MCP server: ${this.mcpName}`)
|
throw new Error(`No code verifier saved for MCP server: ${this.mcpName}`)
|
||||||
}
|
}
|
||||||
|
|
@ -143,11 +149,11 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveState(state: string): Promise<void> {
|
async saveState(state: string): Promise<void> {
|
||||||
await McpAuth.updateOAuthState(this.mcpName, state)
|
await Effect.runPromise(this.auth.updateOAuthState(this.mcpName, state))
|
||||||
}
|
}
|
||||||
|
|
||||||
async state(): Promise<string> {
|
async state(): Promise<string> {
|
||||||
const entry = await McpAuth.get(this.mcpName)
|
const entry = await Effect.runPromise(this.auth.get(this.mcpName))
|
||||||
if (entry?.oauthState) {
|
if (entry?.oauthState) {
|
||||||
return entry.oauthState
|
return entry.oauthState
|
||||||
}
|
}
|
||||||
|
|
@ -159,28 +165,28 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
||||||
const newState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
const newState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join("")
|
.join("")
|
||||||
await McpAuth.updateOAuthState(this.mcpName, newState)
|
await Effect.runPromise(this.auth.updateOAuthState(this.mcpName, newState))
|
||||||
return newState
|
return newState
|
||||||
}
|
}
|
||||||
|
|
||||||
async invalidateCredentials(type: "all" | "client" | "tokens"): Promise<void> {
|
async invalidateCredentials(type: "all" | "client" | "tokens"): Promise<void> {
|
||||||
log.info("invalidating credentials", { mcpName: this.mcpName, type })
|
log.info("invalidating credentials", { mcpName: this.mcpName, type })
|
||||||
const entry = await McpAuth.get(this.mcpName)
|
const entry = await Effect.runPromise(this.auth.get(this.mcpName))
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "all":
|
case "all":
|
||||||
await McpAuth.remove(this.mcpName)
|
await Effect.runPromise(this.auth.remove(this.mcpName))
|
||||||
break
|
break
|
||||||
case "client":
|
case "client":
|
||||||
delete entry.clientInfo
|
delete entry.clientInfo
|
||||||
await McpAuth.set(this.mcpName, entry)
|
await Effect.runPromise(this.auth.set(this.mcpName, entry))
|
||||||
break
|
break
|
||||||
case "tokens":
|
case "tokens":
|
||||||
delete entry.tokens
|
delete entry.tokens
|
||||||
await McpAuth.set(this.mcpName, entry)
|
await Effect.runPromise(this.auth.set(this.mcpName, entry))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ const disposal = {
|
||||||
all: undefined as Promise<void> | undefined,
|
all: undefined as Promise<void> | undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitDisposed(directory: string) {}
|
|
||||||
|
|
||||||
function boot(input: { directory: string; init?: () => Promise<any>; worktree?: string; project?: Project.Info }) {
|
function boot(input: { directory: string; init?: () => Promise<any>; worktree?: string; project?: Project.Info }) {
|
||||||
return iife(async () => {
|
return iife(async () => {
|
||||||
const ctx =
|
const ctx =
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
import z from "zod"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { describeRoute, resolver } from "hono-openapi"
|
import { describeRoute, resolver } from "hono-openapi"
|
||||||
import { streamSSE } from "hono/streaming"
|
import { streamSSE } from "hono/streaming"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { SyncEvent } from "@/sync"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { AsyncQueue } from "../../util/queue"
|
import { AsyncQueue } from "../../util/queue"
|
||||||
|
|
||||||
|
|
@ -20,7 +22,11 @@ export const EventRoutes = () =>
|
||||||
description: "Event stream",
|
description: "Event stream",
|
||||||
content: {
|
content: {
|
||||||
"text/event-stream": {
|
"text/event-stream": {
|
||||||
schema: resolver(BusEvent.payloads()),
|
schema: resolver(
|
||||||
|
z.union(BusEvent.payloads()).meta({
|
||||||
|
ref: "Event",
|
||||||
|
}),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ export const GlobalRoutes = lazy(() =>
|
||||||
directory: z.string(),
|
directory: z.string(),
|
||||||
project: z.string().optional(),
|
project: z.string().optional(),
|
||||||
workspace: z.string().optional(),
|
workspace: z.string().optional(),
|
||||||
payload: BusEvent.payloads(),
|
payload: z.union([...BusEvent.payloads(), ...SyncEvent.payloads()]),
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
ref: "GlobalEvent",
|
ref: "GlobalEvent",
|
||||||
|
|
@ -135,52 +135,6 @@ export const GlobalRoutes = lazy(() =>
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
|
||||||
"/sync-event",
|
|
||||||
describeRoute({
|
|
||||||
summary: "Subscribe to global sync events",
|
|
||||||
description: "Get global sync events",
|
|
||||||
operationId: "global.sync-event.subscribe",
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Event stream",
|
|
||||||
content: {
|
|
||||||
"text/event-stream": {
|
|
||||||
schema: resolver(
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
payload: SyncEvent.payloads(),
|
|
||||||
})
|
|
||||||
.meta({
|
|
||||||
ref: "SyncEvent",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
async (c) => {
|
|
||||||
log.info("global sync event connected")
|
|
||||||
c.header("Cache-Control", "no-cache, no-transform")
|
|
||||||
c.header("X-Accel-Buffering", "no")
|
|
||||||
c.header("X-Content-Type-Options", "nosniff")
|
|
||||||
return streamEvents(c, (q) => {
|
|
||||||
return SyncEvent.subscribeAll(({ def, event }) => {
|
|
||||||
// TODO: don't pass def, just pass the type (and it should
|
|
||||||
// be versioned)
|
|
||||||
q.push(
|
|
||||||
JSON.stringify({
|
|
||||||
payload: {
|
|
||||||
...event,
|
|
||||||
type: SyncEvent.versionedType(def.type, def.version),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.get(
|
.get(
|
||||||
"/config",
|
"/config",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const modes = await Agent.list()
|
const modes = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.list()))
|
||||||
return c.json(modes)
|
return c.json(modes)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -474,10 +474,14 @@ export const SessionRoutes = lazy(() =>
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const query = c.req.valid("query")
|
const query = c.req.valid("query")
|
||||||
const params = c.req.valid("param")
|
const params = c.req.valid("param")
|
||||||
const result = await SessionSummary.diff({
|
const result = await AppRuntime.runPromise(
|
||||||
sessionID: params.sessionID,
|
SessionSummary.Service.use((summary) =>
|
||||||
messageID: query.messageID,
|
summary.diff({
|
||||||
})
|
sessionID: params.sessionID,
|
||||||
|
messageID: query.messageID,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
return c.json(result)
|
return c.json(result)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -547,27 +551,38 @@ export const SessionRoutes = lazy(() =>
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
const session = await Session.get(sessionID)
|
await AppRuntime.runPromise(
|
||||||
await SessionRevert.cleanup(session)
|
Effect.gen(function* () {
|
||||||
const msgs = await Session.messages({ sessionID })
|
const session = yield* Session.Service
|
||||||
let currentAgent = await Agent.defaultAgent()
|
const revert = yield* SessionRevert.Service
|
||||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
const compact = yield* SessionCompaction.Service
|
||||||
const info = msgs[i].info
|
const prompt = yield* SessionPrompt.Service
|
||||||
if (info.role === "user") {
|
const agent = yield* Agent.Service
|
||||||
currentAgent = info.agent || (await Agent.defaultAgent())
|
|
||||||
break
|
yield* revert.cleanup(yield* session.get(sessionID))
|
||||||
}
|
const msgs = yield* session.messages({ sessionID })
|
||||||
}
|
const defaultAgent = yield* agent.defaultAgent()
|
||||||
await SessionCompaction.create({
|
let currentAgent = defaultAgent
|
||||||
sessionID,
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||||
agent: currentAgent,
|
const info = msgs[i].info
|
||||||
model: {
|
if (info.role === "user") {
|
||||||
providerID: body.providerID,
|
currentAgent = info.agent || defaultAgent
|
||||||
modelID: body.modelID,
|
break
|
||||||
},
|
}
|
||||||
auto: body.auto,
|
}
|
||||||
})
|
|
||||||
await SessionPrompt.loop({ sessionID })
|
yield* compact.create({
|
||||||
|
sessionID,
|
||||||
|
agent: currentAgent,
|
||||||
|
model: {
|
||||||
|
providerID: body.providerID,
|
||||||
|
modelID: body.modelID,
|
||||||
|
},
|
||||||
|
auto: body.auto,
|
||||||
|
})
|
||||||
|
yield* prompt.loop({ sessionID })
|
||||||
|
}),
|
||||||
|
)
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -985,10 +1000,14 @@ export const SessionRoutes = lazy(() =>
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
log.info("revert", c.req.valid("json"))
|
log.info("revert", c.req.valid("json"))
|
||||||
const session = await SessionRevert.revert({
|
const session = await AppRuntime.runPromise(
|
||||||
sessionID,
|
SessionRevert.Service.use((svc) =>
|
||||||
...c.req.valid("json"),
|
svc.revert({
|
||||||
})
|
sessionID,
|
||||||
|
...c.req.valid("json"),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -1018,7 +1037,7 @@ export const SessionRoutes = lazy(() =>
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
const session = await SessionRevert.unrevert({ sessionID })
|
const session = await AppRuntime.runPromise(SessionRevert.Service.use((svc) => svc.unrevert({ sessionID })))
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Cause, Deferred, Effect, Layer, Context } from "effect"
|
import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect"
|
||||||
import * as Stream from "effect/Stream"
|
import * as Stream from "effect/Stream"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent/agent"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
|
|
@ -89,6 +89,7 @@ export namespace SessionProcessor {
|
||||||
| LLM.Service
|
| LLM.Service
|
||||||
| Permission.Service
|
| Permission.Service
|
||||||
| Plugin.Service
|
| Plugin.Service
|
||||||
|
| SessionSummary.Service
|
||||||
| SessionStatus.Service
|
| SessionStatus.Service
|
||||||
> = Layer.effect(
|
> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
|
|
@ -101,6 +102,8 @@ export namespace SessionProcessor {
|
||||||
const llm = yield* LLM.Service
|
const llm = yield* LLM.Service
|
||||||
const permission = yield* Permission.Service
|
const permission = yield* Permission.Service
|
||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
|
const summary = yield* SessionSummary.Service
|
||||||
|
const scope = yield* Scope.Scope
|
||||||
const status = yield* SessionStatus.Service
|
const status = yield* SessionStatus.Service
|
||||||
|
|
||||||
const create = Effect.fn("SessionProcessor.create")(function* (input: Input) {
|
const create = Effect.fn("SessionProcessor.create")(function* (input: Input) {
|
||||||
|
|
@ -385,10 +388,12 @@ export namespace SessionProcessor {
|
||||||
}
|
}
|
||||||
ctx.snapshot = undefined
|
ctx.snapshot = undefined
|
||||||
}
|
}
|
||||||
SessionSummary.summarize({
|
yield* summary
|
||||||
sessionID: ctx.sessionID,
|
.summarize({
|
||||||
messageID: ctx.assistantMessage.parentID,
|
sessionID: ctx.sessionID,
|
||||||
})
|
messageID: ctx.assistantMessage.parentID,
|
||||||
|
})
|
||||||
|
.pipe(Effect.ignore, Effect.forkIn(scope))
|
||||||
if (
|
if (
|
||||||
!ctx.assistantMessage.summary &&
|
!ctx.assistantMessage.summary &&
|
||||||
isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model })
|
isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model })
|
||||||
|
|
@ -603,6 +608,7 @@ export namespace SessionProcessor {
|
||||||
Layer.provide(LLM.defaultLayer),
|
Layer.provide(LLM.defaultLayer),
|
||||||
Layer.provide(Permission.defaultLayer),
|
Layer.provide(Permission.defaultLayer),
|
||||||
Layer.provide(Plugin.defaultLayer),
|
Layer.provide(Plugin.defaultLayer),
|
||||||
|
Layer.provide(SessionSummary.defaultLayer),
|
||||||
Layer.provide(SessionStatus.defaultLayer),
|
Layer.provide(SessionStatus.defaultLayer),
|
||||||
Layer.provide(Bus.layer),
|
Layer.provide(Bus.layer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { NotFoundError, eq, and } from "../storage/db"
|
import { NotFoundError, eq, and, sql } from "../storage/db"
|
||||||
import { SyncEvent } from "@/sync"
|
import { SyncEvent } from "@/sync"
|
||||||
import { Session } from "./index"
|
import { Session } from "./index"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
import { SessionTable, MessageTable, PartTable } from "./session.sql"
|
import { SessionTable, MessageTable, PartTable, SessionEntryTable } from "./session.sql"
|
||||||
import { ProjectTable } from "../project/project.sql"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
import { DateTime } from "effect"
|
||||||
|
import { SessionEntry } from "@/v2/session-entry"
|
||||||
|
|
||||||
const log = Log.create({ service: "session.projector" })
|
const log = Log.create({ service: "session.projector" })
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ export namespace SessionPrompt {
|
||||||
const instruction = yield* Instruction.Service
|
const instruction = yield* Instruction.Service
|
||||||
const state = yield* SessionRunState.Service
|
const state = yield* SessionRunState.Service
|
||||||
const revert = yield* SessionRevert.Service
|
const revert = yield* SessionRevert.Service
|
||||||
|
const summary = yield* SessionSummary.Service
|
||||||
const sys = yield* SystemPrompt.Service
|
const sys = yield* SystemPrompt.Service
|
||||||
const llm = yield* LLM.Service
|
const llm = yield* LLM.Service
|
||||||
|
|
||||||
|
|
@ -1444,7 +1445,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step === 1) SessionSummary.summarize({ sessionID, messageID: lastUser.id })
|
if (step === 1)
|
||||||
|
yield* summary
|
||||||
|
.summarize({ sessionID, messageID: lastUser.id })
|
||||||
|
.pipe(Effect.ignore, Effect.forkIn(scope))
|
||||||
|
|
||||||
if (step > 1 && lastFinished) {
|
if (step > 1 && lastFinished) {
|
||||||
for (const m of msgs) {
|
for (const m of msgs) {
|
||||||
|
|
@ -1692,6 +1696,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||||
Layer.provide(Plugin.defaultLayer),
|
Layer.provide(Plugin.defaultLayer),
|
||||||
Layer.provide(Session.defaultLayer),
|
Layer.provide(Session.defaultLayer),
|
||||||
Layer.provide(SessionRevert.defaultLayer),
|
Layer.provide(SessionRevert.defaultLayer),
|
||||||
|
Layer.provide(SessionSummary.defaultLayer),
|
||||||
Layer.provide(
|
Layer.provide(
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(
|
||||||
Agent.defaultLayer,
|
Agent.defaultLayer,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { Snapshot } from "../snapshot"
|
import { Snapshot } from "../snapshot"
|
||||||
import { Storage } from "@/storage/storage"
|
import { Storage } from "@/storage/storage"
|
||||||
|
|
@ -160,18 +159,4 @@ export namespace SessionRevert {
|
||||||
Layer.provide(SessionSummary.defaultLayer),
|
Layer.provide(SessionSummary.defaultLayer),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
||||||
|
|
||||||
export async function revert(input: RevertInput) {
|
|
||||||
return runPromise((svc) => svc.revert(input))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function unrevert(input: { sessionID: SessionID }) {
|
|
||||||
return runPromise((svc) => svc.unrevert(input))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cleanup(session: Session.Info) {
|
|
||||||
return runPromise((svc) => svc.cleanup(session))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core"
|
import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core"
|
||||||
import { ProjectTable } from "../project/project.sql"
|
import { ProjectTable } from "../project/project.sql"
|
||||||
import type { MessageV2 } from "./message-v2"
|
import type { MessageV2 } from "./message-v2"
|
||||||
|
import type { SessionEntry } from "../v2/session-entry"
|
||||||
import type { Snapshot } from "../snapshot"
|
import type { Snapshot } from "../snapshot"
|
||||||
import type { Permission } from "../permission"
|
import type { Permission } from "../permission"
|
||||||
import type { ProjectID } from "../project/schema"
|
import type { ProjectID } from "../project/schema"
|
||||||
|
|
@ -10,6 +11,7 @@ import { Timestamps } from "../storage/schema.sql"
|
||||||
|
|
||||||
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
|
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
|
||||||
type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
|
type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
|
||||||
|
type EntryData = Omit<SessionEntry.Entry, "id" | "type">
|
||||||
|
|
||||||
export const SessionTable = sqliteTable(
|
export const SessionTable = sqliteTable(
|
||||||
"session",
|
"session",
|
||||||
|
|
@ -94,6 +96,25 @@ export const TodoTable = sqliteTable(
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const SessionEntryTable = sqliteTable(
|
||||||
|
"session_entry",
|
||||||
|
{
|
||||||
|
id: text().$type<SessionEntry.ID>().primaryKey(),
|
||||||
|
session_id: text()
|
||||||
|
.$type<SessionID>()
|
||||||
|
.notNull()
|
||||||
|
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||||
|
type: text().notNull(),
|
||||||
|
...Timestamps,
|
||||||
|
data: text({ mode: "json" }).notNull().$type<Omit<SessionEntry.Entry, "type" | "id">>(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("session_entry_session_idx").on(table.session_id),
|
||||||
|
index("session_entry_session_type_idx").on(table.session_id, table.type),
|
||||||
|
index("session_entry_time_created_idx").on(table.time_created),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
export const PermissionTable = sqliteTable("permission", {
|
export const PermissionTable = sqliteTable("permission", {
|
||||||
project_id: text()
|
project_id: text()
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { Storage } from "@/storage/storage"
|
import { Storage } from "@/storage/storage"
|
||||||
|
|
@ -159,17 +158,8 @@ export namespace SessionSummary {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
||||||
|
|
||||||
export const summarize = (input: { sessionID: SessionID; messageID: MessageID }) =>
|
|
||||||
void runPromise((svc) => svc.summarize(input)).catch(() => {})
|
|
||||||
|
|
||||||
export const DiffInput = z.object({
|
export const DiffInput = z.object({
|
||||||
sessionID: SessionID.zod,
|
sessionID: SessionID.zod,
|
||||||
messageID: MessageID.zod.optional(),
|
messageID: MessageID.zod.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function diff(input: z.infer<typeof DiffInput>) {
|
|
||||||
return runPromise((svc) => svc.diff(input))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@ import z from "zod"
|
||||||
import type { ZodObject } from "zod"
|
import type { ZodObject } from "zod"
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
import { Database, eq } from "@/storage/db"
|
import { Database, eq } from "@/storage/db"
|
||||||
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { Bus as ProjectBus } from "@/bus"
|
import { Bus as ProjectBus } from "@/bus"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Instance } from "@/project/instance"
|
||||||
import { EventSequenceTable, EventTable } from "./event.sql"
|
import { EventSequenceTable, EventTable } from "./event.sql"
|
||||||
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
import { EventID } from "./schema"
|
import { EventID } from "./schema"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
|
|
||||||
|
|
@ -37,8 +40,6 @@ export namespace SyncEvent {
|
||||||
let frozen = false
|
let frozen = false
|
||||||
let convertEvent: (type: string, event: Event["data"]) => Promise<Record<string, unknown>> | Record<string, unknown>
|
let convertEvent: (type: string, event: Event["data"]) => Promise<Record<string, unknown>> | Record<string, unknown>
|
||||||
|
|
||||||
const Bus = new EventEmitter<{ event: [{ def: Definition; event: Event }] }>()
|
|
||||||
|
|
||||||
export function reset() {
|
export function reset() {
|
||||||
frozen = false
|
frozen = false
|
||||||
projectors = undefined
|
projectors = undefined
|
||||||
|
|
@ -140,11 +141,6 @@ export namespace SyncEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
Database.effect(() => {
|
Database.effect(() => {
|
||||||
Bus.emit("event", {
|
|
||||||
def,
|
|
||||||
event,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (options?.publish) {
|
if (options?.publish) {
|
||||||
const result = convertEvent(def.type, event.data)
|
const result = convertEvent(def.type, event.data)
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
|
|
@ -154,6 +150,17 @@ export namespace SyncEvent {
|
||||||
} else {
|
} else {
|
||||||
ProjectBus.publish({ type: def.type, properties: def.schema }, result)
|
ProjectBus.publish({ type: def.type, properties: def.schema }, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory: Instance.directory,
|
||||||
|
project: Instance.project.id,
|
||||||
|
workspace: WorkspaceContext.workspaceID,
|
||||||
|
payload: {
|
||||||
|
type: "sync",
|
||||||
|
name: versionedType(def.type, def.version),
|
||||||
|
...event,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -235,31 +242,23 @@ export namespace SyncEvent {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribeAll(handler: (event: { def: Definition; event: Event }) => void) {
|
|
||||||
Bus.on("event", handler)
|
|
||||||
return () => Bus.off("event", handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function payloads() {
|
export function payloads() {
|
||||||
return z
|
return registry
|
||||||
.union(
|
.entries()
|
||||||
registry
|
.map(([type, def]) => {
|
||||||
.entries()
|
return z
|
||||||
.map(([type, def]) => {
|
.object({
|
||||||
return z
|
type: z.literal("sync"),
|
||||||
.object({
|
name: z.literal(type),
|
||||||
type: z.literal(type),
|
id: z.string(),
|
||||||
aggregate: z.literal(def.aggregate),
|
seq: z.number(),
|
||||||
data: def.schema,
|
aggregateID: z.literal(def.aggregate),
|
||||||
})
|
data: def.schema,
|
||||||
.meta({
|
})
|
||||||
ref: "SyncEvent" + "." + def.type,
|
.meta({
|
||||||
})
|
ref: "SyncEvent" + "." + def.type,
|
||||||
})
|
})
|
||||||
.toArray() as any,
|
|
||||||
)
|
|
||||||
.meta({
|
|
||||||
ref: "SyncEvent",
|
|
||||||
})
|
})
|
||||||
|
.toArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ export namespace ToolRegistry {
|
||||||
const greptool = yield* GrepTool
|
const greptool = yield* GrepTool
|
||||||
const patchtool = yield* ApplyPatchTool
|
const patchtool = yield* ApplyPatchTool
|
||||||
const skilltool = yield* SkillTool
|
const skilltool = yield* SkillTool
|
||||||
|
const agent = yield* Agent.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("ToolRegistry.state")(function* (ctx) {
|
Effect.fn("ToolRegistry.state")(function* (ctx) {
|
||||||
|
|
@ -140,8 +141,8 @@ export namespace ToolRegistry {
|
||||||
worktree: ctx.worktree,
|
worktree: ctx.worktree,
|
||||||
}
|
}
|
||||||
const result = yield* Effect.promise(() => def.execute(args as any, pluginCtx))
|
const result = yield* Effect.promise(() => def.execute(args as any, pluginCtx))
|
||||||
const agent = yield* Effect.promise(() => Agent.get(toolCtx.agent))
|
const info = yield* agent.get(toolCtx.agent)
|
||||||
const out = yield* truncate.output(result, {}, agent)
|
const out = yield* truncate.output(result, {}, info)
|
||||||
return {
|
return {
|
||||||
title: "",
|
title: "",
|
||||||
output: out.truncated ? out.content : result,
|
output: out.truncated ? out.content : result,
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
import { Identifier } from "@/id/id"
|
|
||||||
import { withStatics } from "@/util/schema"
|
|
||||||
import { DateTime, Effect, Schema } from "effect"
|
|
||||||
|
|
||||||
export namespace Message {
|
|
||||||
export const ID = Schema.String.pipe(Schema.brand("Message.ID")).pipe(
|
|
||||||
withStatics((s) => ({
|
|
||||||
create: () => s.make(Identifier.ascending("message")),
|
|
||||||
prefix: "msg",
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
export class Source extends Schema.Class<Source>("Message.Source")({
|
|
||||||
start: Schema.Number,
|
|
||||||
end: Schema.Number,
|
|
||||||
text: Schema.String,
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class FileAttachment extends Schema.Class<FileAttachment>("Message.File.Attachment")({
|
|
||||||
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(url: string) {
|
|
||||||
return new FileAttachment({
|
|
||||||
uri: url,
|
|
||||||
mime: "text/plain",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AgentAttachment extends Schema.Class<AgentAttachment>("Message.Agent.Attachment")({
|
|
||||||
name: Schema.String,
|
|
||||||
source: Source.pipe(Schema.optional),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class User extends Schema.Class<User>("Message.User")({
|
|
||||||
id: ID,
|
|
||||||
type: Schema.Literal("user"),
|
|
||||||
text: Schema.String,
|
|
||||||
files: Schema.Array(FileAttachment).pipe(Schema.optional),
|
|
||||||
agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
|
|
||||||
time: Schema.Struct({
|
|
||||||
created: Schema.DateTimeUtc,
|
|
||||||
}),
|
|
||||||
}) {
|
|
||||||
static create(input: { text: User["text"]; files?: User["files"]; agents?: User["agents"] }) {
|
|
||||||
const msg = new User({
|
|
||||||
id: ID.create(),
|
|
||||||
type: "user",
|
|
||||||
...input,
|
|
||||||
time: {
|
|
||||||
created: Effect.runSync(DateTime.now),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Synthetic extends Schema.Class<Synthetic>("Message.Synthetic")({
|
|
||||||
id: ID,
|
|
||||||
type: Schema.Literal("synthetic"),
|
|
||||||
text: Schema.String,
|
|
||||||
time: Schema.Struct({
|
|
||||||
created: Schema.DateTimeUtc,
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class Request extends Schema.Class<Request>("Message.Request")({
|
|
||||||
id: ID,
|
|
||||||
type: Schema.Literal("start"),
|
|
||||||
model: Schema.Struct({
|
|
||||||
id: Schema.String,
|
|
||||||
providerID: Schema.String,
|
|
||||||
variant: Schema.String.pipe(Schema.optional),
|
|
||||||
}),
|
|
||||||
time: Schema.Struct({
|
|
||||||
created: Schema.DateTimeUtc,
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class Text extends Schema.Class<Text>("Message.Text")({
|
|
||||||
id: ID,
|
|
||||||
type: Schema.Literal("text"),
|
|
||||||
text: Schema.String,
|
|
||||||
time: Schema.Struct({
|
|
||||||
created: Schema.DateTimeUtc,
|
|
||||||
completed: Schema.DateTimeUtc.pipe(Schema.optional),
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class Complete extends Schema.Class<Complete>("Message.Complete")({
|
|
||||||
id: ID,
|
|
||||||
type: Schema.Literal("complete"),
|
|
||||||
time: Schema.Struct({
|
|
||||||
created: Schema.DateTimeUtc,
|
|
||||||
}),
|
|
||||||
cost: Schema.Number,
|
|
||||||
tokens: Schema.Struct({
|
|
||||||
total: Schema.Number,
|
|
||||||
input: Schema.Number,
|
|
||||||
output: Schema.Number,
|
|
||||||
reasoning: Schema.Number,
|
|
||||||
cache: Schema.Struct({
|
|
||||||
read: Schema.Number,
|
|
||||||
write: Schema.Number,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export const Info = Schema.Union([User, Text])
|
|
||||||
export type Info = Schema.Schema.Type<typeof Info>
|
|
||||||
}
|
|
||||||
186
packages/opencode/src/v2/session-entry.ts
Normal file
186
packages/opencode/src/v2/session-entry.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
import { Identifier } from "@/id/id"
|
||||||
|
import { withStatics } from "@/util/schema"
|
||||||
|
import { DateTime, Effect, Schema } from "effect"
|
||||||
|
|
||||||
|
export namespace SessionEntry {
|
||||||
|
export const ID = Schema.String.pipe(Schema.brand("Session.Entry.ID")).pipe(
|
||||||
|
withStatics((s) => ({
|
||||||
|
create: () => s.make(Identifier.ascending("entry")),
|
||||||
|
prefix: "ent",
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
export type ID = Schema.Schema.Type<typeof ID>
|
||||||
|
|
||||||
|
const Base = {
|
||||||
|
id: ID,
|
||||||
|
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||||
|
time: Schema.Struct({
|
||||||
|
created: Schema.DateTimeUtc,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Source extends Schema.Class<Source>("Session.Entry.Source")({
|
||||||
|
start: Schema.Number,
|
||||||
|
end: Schema.Number,
|
||||||
|
text: Schema.String,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class FileAttachment extends Schema.Class<FileAttachment>("Session.Entry.File.Attachment")({
|
||||||
|
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(url: string) {
|
||||||
|
return new FileAttachment({
|
||||||
|
uri: url,
|
||||||
|
mime: "text/plain",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AgentAttachment extends Schema.Class<AgentAttachment>("Session.Entry.Agent.Attachment")({
|
||||||
|
name: Schema.String,
|
||||||
|
source: Source.pipe(Schema.optional),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class User extends Schema.Class<User>("Session.Entry.User")({
|
||||||
|
...Base,
|
||||||
|
type: Schema.Literal("user"),
|
||||||
|
text: Schema.String,
|
||||||
|
files: Schema.Array(FileAttachment).pipe(Schema.optional),
|
||||||
|
agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
|
||||||
|
}) {
|
||||||
|
static create(input: { text: User["text"]; files?: User["files"]; agents?: User["agents"] }) {
|
||||||
|
const msg = new User({
|
||||||
|
id: ID.create(),
|
||||||
|
type: "user",
|
||||||
|
...input,
|
||||||
|
time: {
|
||||||
|
created: Effect.runSync(DateTime.now),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Synthetic extends Schema.Class<Synthetic>("Session.Entry.Synthetic")({
|
||||||
|
...Base,
|
||||||
|
type: Schema.Literal("synthetic"),
|
||||||
|
text: Schema.String,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class Request extends Schema.Class<Request>("Session.Entry.Request")({
|
||||||
|
...Base,
|
||||||
|
type: Schema.Literal("start"),
|
||||||
|
model: Schema.Struct({
|
||||||
|
id: Schema.String,
|
||||||
|
providerID: Schema.String,
|
||||||
|
variant: Schema.String.pipe(Schema.optional),
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class Text extends Schema.Class<Text>("Session.Entry.Text")({
|
||||||
|
...Base,
|
||||||
|
type: Schema.Literal("text"),
|
||||||
|
text: Schema.String,
|
||||||
|
time: Schema.Struct({
|
||||||
|
...Base.time.fields,
|
||||||
|
completed: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class Reasoning extends Schema.Class<Reasoning>("Session.Entry.Reasoning")({
|
||||||
|
...Base,
|
||||||
|
type: Schema.Literal("reasoning"),
|
||||||
|
text: Schema.String,
|
||||||
|
time: Schema.Struct({
|
||||||
|
...Base.time.fields,
|
||||||
|
completed: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class ToolStatePending extends Schema.Class<ToolStatePending>("Session.Entry.ToolState.Pending")({
|
||||||
|
status: Schema.Literal("pending"),
|
||||||
|
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||||
|
raw: Schema.String,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class ToolStateRunning extends Schema.Class<ToolStateRunning>("Session.Entry.ToolState.Running")({
|
||||||
|
status: Schema.Literal("running"),
|
||||||
|
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||||
|
title: Schema.String.pipe(Schema.optional),
|
||||||
|
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class ToolStateCompleted extends Schema.Class<ToolStateCompleted>("Session.Entry.ToolState.Completed")({
|
||||||
|
status: Schema.Literal("completed"),
|
||||||
|
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||||
|
output: Schema.String,
|
||||||
|
title: Schema.String,
|
||||||
|
metadata: Schema.Record(Schema.String, Schema.Unknown),
|
||||||
|
attachments: Schema.Array(FileAttachment).pipe(Schema.optional),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class ToolStateError extends Schema.Class<ToolStateError>("Session.Entry.ToolState.Error")({
|
||||||
|
status: Schema.Literal("error"),
|
||||||
|
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||||
|
error: Schema.String,
|
||||||
|
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||||
|
time: Schema.Struct({
|
||||||
|
start: Schema.Number,
|
||||||
|
end: Schema.Number,
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
|
||||||
|
export type ToolState = Schema.Schema.Type<typeof ToolState>
|
||||||
|
|
||||||
|
export class Tool extends Schema.Class<Tool>("Session.Entry.Tool")({
|
||||||
|
...Base,
|
||||||
|
type: Schema.Literal("tool"),
|
||||||
|
callID: Schema.String,
|
||||||
|
name: Schema.String,
|
||||||
|
state: ToolState,
|
||||||
|
time: Schema.Struct({
|
||||||
|
...Base.time.fields,
|
||||||
|
ran: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||||
|
completed: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||||
|
pruned: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class Complete extends Schema.Class<Complete>("Session.Entry.Complete")({
|
||||||
|
...Base,
|
||||||
|
type: Schema.Literal("complete"),
|
||||||
|
cost: Schema.Number,
|
||||||
|
reason: Schema.String,
|
||||||
|
tokens: Schema.Struct({
|
||||||
|
input: Schema.Number,
|
||||||
|
output: Schema.Number,
|
||||||
|
reasoning: Schema.Number,
|
||||||
|
cache: Schema.Struct({
|
||||||
|
read: Schema.Number,
|
||||||
|
write: Schema.Number,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class Retry extends Schema.Class<Retry>("Session.Entry.Retry")({
|
||||||
|
...Base,
|
||||||
|
type: Schema.Literal("retry"),
|
||||||
|
attempt: Schema.Number,
|
||||||
|
error: Schema.String,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class Compaction extends Schema.Class<Compaction>("Session.Entry.Compaction")({
|
||||||
|
...Base,
|
||||||
|
type: Schema.Literal("compaction"),
|
||||||
|
auto: Schema.Boolean,
|
||||||
|
overflow: Schema.Boolean.pipe(Schema.optional),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export const Entry = Schema.Union([User, Synthetic, Request, Tool, Text, Reasoning, Complete, Retry, Compaction])
|
||||||
|
export type Entry = Schema.Schema.Type<typeof Entry>
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Context, Layer, Schema, Effect } from "effect"
|
import { Context, Layer, Schema, Effect } from "effect"
|
||||||
import { Message } from "./message"
|
import { SessionEntry } from "./session-entry"
|
||||||
import { Struct } from "effect"
|
import { Struct } from "effect"
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id/id"
|
||||||
import { withStatics } from "@/util/schema"
|
import { withStatics } from "@/util/schema"
|
||||||
|
|
@ -12,8 +12,8 @@ export namespace SessionV2 {
|
||||||
export type ID = Schema.Schema.Type<typeof ID>
|
export type ID = Schema.Schema.Type<typeof ID>
|
||||||
|
|
||||||
export class PromptInput extends Schema.Class<PromptInput>("Session.PromptInput")({
|
export class PromptInput extends Schema.Class<PromptInput>("Session.PromptInput")({
|
||||||
...Struct.omit(Message.User.fields, ["time", "type"]),
|
...Struct.omit(SessionEntry.User.fields, ["time", "type"]),
|
||||||
id: Schema.optionalKey(Message.ID),
|
id: Schema.optionalKey(SessionEntry.ID),
|
||||||
sessionID: SessionV2.ID,
|
sessionID: SessionV2.ID,
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ export namespace SessionV2 {
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
fromID: (id: SessionV2.ID) => Effect.Effect<Info>
|
fromID: (id: SessionV2.ID) => Effect.Effect<Info>
|
||||||
create: (input: CreateInput) => Effect.Effect<Info>
|
create: (input: CreateInput) => Effect.Effect<Info>
|
||||||
prompt: (input: PromptInput) => Effect.Effect<Message.User>
|
prompt: (input: PromptInput) => Effect.Effect<SessionEntry.User>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("Session.Service") {}
|
export class Service extends Context.Service<Service, Interface>()("Session.Service") {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { afterEach, test, expect } from "bun:test"
|
import { afterEach, test, expect } from "bun:test"
|
||||||
|
import { Effect } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent/agent"
|
||||||
import { Permission } from "../../src/permission"
|
import { Permission } from "../../src/permission"
|
||||||
|
|
@ -11,6 +12,10 @@ function evalPerm(agent: Agent.Info | undefined, permission: string): Permission
|
||||||
return Permission.evaluate(permission, "*", agent.permission).action
|
return Permission.evaluate(permission, "*", agent.permission).action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function load<A>(dir: string, fn: (svc: Agent.Interface) => Effect.Effect<A>) {
|
||||||
|
return Effect.runPromise(provideInstance(dir)(Agent.Service.use(fn)).pipe(Effect.provide(Agent.defaultLayer)))
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await Instance.disposeAll()
|
await Instance.disposeAll()
|
||||||
})
|
})
|
||||||
|
|
@ -20,7 +25,7 @@ test("returns default native agents when no config", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const agents = await Agent.list()
|
const agents = await load(tmp.path, (svc) => svc.list())
|
||||||
const names = agents.map((a) => a.name)
|
const names = agents.map((a) => a.name)
|
||||||
expect(names).toContain("build")
|
expect(names).toContain("build")
|
||||||
expect(names).toContain("plan")
|
expect(names).toContain("plan")
|
||||||
|
|
@ -38,7 +43,7 @@ test("build agent has correct default properties", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(build).toBeDefined()
|
expect(build).toBeDefined()
|
||||||
expect(build?.mode).toBe("primary")
|
expect(build?.mode).toBe("primary")
|
||||||
expect(build?.native).toBe(true)
|
expect(build?.native).toBe(true)
|
||||||
|
|
@ -53,7 +58,7 @@ test("plan agent denies edits except .opencode/plans/*", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const plan = await Agent.get("plan")
|
const plan = await load(tmp.path, (svc) => svc.get("plan"))
|
||||||
expect(plan).toBeDefined()
|
expect(plan).toBeDefined()
|
||||||
// Wildcard is denied
|
// Wildcard is denied
|
||||||
expect(evalPerm(plan, "edit")).toBe("deny")
|
expect(evalPerm(plan, "edit")).toBe("deny")
|
||||||
|
|
@ -68,7 +73,7 @@ test("explore agent denies edit and write", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const explore = await Agent.get("explore")
|
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
||||||
expect(explore).toBeDefined()
|
expect(explore).toBeDefined()
|
||||||
expect(explore?.mode).toBe("subagent")
|
expect(explore?.mode).toBe("subagent")
|
||||||
expect(evalPerm(explore, "edit")).toBe("deny")
|
expect(evalPerm(explore, "edit")).toBe("deny")
|
||||||
|
|
@ -84,7 +89,7 @@ test("explore agent asks for external directories and allows Truncate.GLOB", asy
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const explore = await Agent.get("explore")
|
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
||||||
expect(explore).toBeDefined()
|
expect(explore).toBeDefined()
|
||||||
expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
|
expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
|
||||||
expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
|
||||||
|
|
@ -97,7 +102,7 @@ test("general agent denies todo tools", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const general = await Agent.get("general")
|
const general = await load(tmp.path, (svc) => svc.get("general"))
|
||||||
expect(general).toBeDefined()
|
expect(general).toBeDefined()
|
||||||
expect(general?.mode).toBe("subagent")
|
expect(general?.mode).toBe("subagent")
|
||||||
expect(general?.hidden).toBeUndefined()
|
expect(general?.hidden).toBeUndefined()
|
||||||
|
|
@ -111,7 +116,7 @@ test("compaction agent denies all permissions", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const compaction = await Agent.get("compaction")
|
const compaction = await load(tmp.path, (svc) => svc.get("compaction"))
|
||||||
expect(compaction).toBeDefined()
|
expect(compaction).toBeDefined()
|
||||||
expect(compaction?.hidden).toBe(true)
|
expect(compaction?.hidden).toBe(true)
|
||||||
expect(evalPerm(compaction, "bash")).toBe("deny")
|
expect(evalPerm(compaction, "bash")).toBe("deny")
|
||||||
|
|
@ -137,7 +142,7 @@ test("custom agent from config creates new agent", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const custom = await Agent.get("my_custom_agent")
|
const custom = await load(tmp.path, (svc) => svc.get("my_custom_agent"))
|
||||||
expect(custom).toBeDefined()
|
expect(custom).toBeDefined()
|
||||||
expect(String(custom?.model?.providerID)).toBe("openai")
|
expect(String(custom?.model?.providerID)).toBe("openai")
|
||||||
expect(String(custom?.model?.modelID)).toBe("gpt-4")
|
expect(String(custom?.model?.modelID)).toBe("gpt-4")
|
||||||
|
|
@ -166,7 +171,7 @@ test("custom agent config overrides native agent properties", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(build).toBeDefined()
|
expect(build).toBeDefined()
|
||||||
expect(String(build?.model?.providerID)).toBe("anthropic")
|
expect(String(build?.model?.providerID)).toBe("anthropic")
|
||||||
expect(String(build?.model?.modelID)).toBe("claude-3")
|
expect(String(build?.model?.modelID)).toBe("claude-3")
|
||||||
|
|
@ -189,9 +194,9 @@ test("agent disable removes agent from list", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const explore = await Agent.get("explore")
|
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
||||||
expect(explore).toBeUndefined()
|
expect(explore).toBeUndefined()
|
||||||
const agents = await Agent.list()
|
const agents = await load(tmp.path, (svc) => svc.list())
|
||||||
const names = agents.map((a) => a.name)
|
const names = agents.map((a) => a.name)
|
||||||
expect(names).not.toContain("explore")
|
expect(names).not.toContain("explore")
|
||||||
},
|
},
|
||||||
|
|
@ -215,7 +220,7 @@ test("agent permission config merges with defaults", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(build).toBeDefined()
|
expect(build).toBeDefined()
|
||||||
// Specific pattern is denied
|
// Specific pattern is denied
|
||||||
expect(Permission.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny")
|
expect(Permission.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny")
|
||||||
|
|
@ -236,7 +241,7 @@ test("global permission config applies to all agents", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(build).toBeDefined()
|
expect(build).toBeDefined()
|
||||||
expect(evalPerm(build, "bash")).toBe("deny")
|
expect(evalPerm(build, "bash")).toBe("deny")
|
||||||
},
|
},
|
||||||
|
|
@ -255,8 +260,8 @@ test("agent steps/maxSteps config sets steps property", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
const plan = await Agent.get("plan")
|
const plan = await load(tmp.path, (svc) => svc.get("plan"))
|
||||||
expect(build?.steps).toBe(50)
|
expect(build?.steps).toBe(50)
|
||||||
expect(plan?.steps).toBe(100)
|
expect(plan?.steps).toBe(100)
|
||||||
},
|
},
|
||||||
|
|
@ -274,7 +279,7 @@ test("agent mode can be overridden", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const explore = await Agent.get("explore")
|
const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
||||||
expect(explore?.mode).toBe("primary")
|
expect(explore?.mode).toBe("primary")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -291,7 +296,7 @@ test("agent name can be overridden", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(build?.name).toBe("Builder")
|
expect(build?.name).toBe("Builder")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -308,7 +313,7 @@ test("agent prompt can be set from config", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(build?.prompt).toBe("Custom system prompt")
|
expect(build?.prompt).toBe("Custom system prompt")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -328,7 +333,7 @@ test("unknown agent properties are placed into options", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(build?.options.random_property).toBe("hello")
|
expect(build?.options.random_property).toBe("hello")
|
||||||
expect(build?.options.another_random).toBe(123)
|
expect(build?.options.another_random).toBe(123)
|
||||||
},
|
},
|
||||||
|
|
@ -351,7 +356,7 @@ test("agent options merge correctly", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(build?.options.custom_option).toBe(true)
|
expect(build?.options.custom_option).toBe(true)
|
||||||
expect(build?.options.another_option).toBe("value")
|
expect(build?.options.another_option).toBe("value")
|
||||||
},
|
},
|
||||||
|
|
@ -376,8 +381,8 @@ test("multiple custom agents can be defined", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const agentA = await Agent.get("agent_a")
|
const agentA = await load(tmp.path, (svc) => svc.get("agent_a"))
|
||||||
const agentB = await Agent.get("agent_b")
|
const agentB = await load(tmp.path, (svc) => svc.get("agent_b"))
|
||||||
expect(agentA?.description).toBe("Agent A")
|
expect(agentA?.description).toBe("Agent A")
|
||||||
expect(agentA?.mode).toBe("subagent")
|
expect(agentA?.mode).toBe("subagent")
|
||||||
expect(agentB?.description).toBe("Agent B")
|
expect(agentB?.description).toBe("Agent B")
|
||||||
|
|
@ -405,7 +410,7 @@ test("Agent.list keeps the default agent first and sorts the rest by name", asyn
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const names = (await Agent.list()).map((a) => a.name)
|
const names = (await load(tmp.path, (svc) => svc.list())).map((a) => a.name)
|
||||||
expect(names[0]).toBe("plan")
|
expect(names[0]).toBe("plan")
|
||||||
expect(names.slice(1)).toEqual(names.slice(1).toSorted((a, b) => a.localeCompare(b)))
|
expect(names.slice(1)).toEqual(names.slice(1).toSorted((a, b) => a.localeCompare(b)))
|
||||||
},
|
},
|
||||||
|
|
@ -417,7 +422,7 @@ test("Agent.get returns undefined for non-existent agent", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const nonExistent = await Agent.get("does_not_exist")
|
const nonExistent = await load(tmp.path, (svc) => svc.get("does_not_exist"))
|
||||||
expect(nonExistent).toBeUndefined()
|
expect(nonExistent).toBeUndefined()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -428,7 +433,7 @@ test("default permission includes doom_loop and external_directory as ask", asyn
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(evalPerm(build, "doom_loop")).toBe("ask")
|
expect(evalPerm(build, "doom_loop")).toBe("ask")
|
||||||
expect(evalPerm(build, "external_directory")).toBe("ask")
|
expect(evalPerm(build, "external_directory")).toBe("ask")
|
||||||
},
|
},
|
||||||
|
|
@ -440,7 +445,7 @@ test("webfetch is allowed by default", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(evalPerm(build, "webfetch")).toBe("allow")
|
expect(evalPerm(build, "webfetch")).toBe("allow")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -462,7 +467,7 @@ test("legacy tools config converts to permissions", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(evalPerm(build, "bash")).toBe("deny")
|
expect(evalPerm(build, "bash")).toBe("deny")
|
||||||
expect(evalPerm(build, "read")).toBe("deny")
|
expect(evalPerm(build, "read")).toBe("deny")
|
||||||
},
|
},
|
||||||
|
|
@ -484,7 +489,7 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(evalPerm(build, "edit")).toBe("deny")
|
expect(evalPerm(build, "edit")).toBe("deny")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -502,7 +507,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
|
||||||
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
||||||
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
|
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
|
||||||
|
|
@ -526,7 +531,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory per-agen
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
|
||||||
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
||||||
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
|
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
|
||||||
|
|
@ -549,7 +554,7 @@ test("explicit Truncate.GLOB deny is respected", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny")
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny")
|
||||||
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
||||||
},
|
},
|
||||||
|
|
@ -581,7 +586,7 @@ description: Permission skill.
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
const skillDir = path.join(tmp.path, ".opencode", "skill", "perm-skill")
|
const skillDir = path.join(tmp.path, ".opencode", "skill", "perm-skill")
|
||||||
const target = path.join(skillDir, "reference", "notes.md")
|
const target = path.join(skillDir, "reference", "notes.md")
|
||||||
expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")
|
expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")
|
||||||
|
|
@ -597,7 +602,7 @@ test("defaultAgent returns build when no default_agent config", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const agent = await Agent.defaultAgent()
|
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
||||||
expect(agent).toBe("build")
|
expect(agent).toBe("build")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -612,7 +617,7 @@ test("defaultAgent respects default_agent config set to plan", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const agent = await Agent.defaultAgent()
|
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
||||||
expect(agent).toBe("plan")
|
expect(agent).toBe("plan")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -632,7 +637,7 @@ test("defaultAgent respects default_agent config set to custom agent with mode a
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const agent = await Agent.defaultAgent()
|
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
||||||
expect(agent).toBe("my_custom")
|
expect(agent).toBe("my_custom")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -647,7 +652,7 @@ test("defaultAgent throws when default_agent points to subagent", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
await expect(Agent.defaultAgent()).rejects.toThrow('default agent "explore" is a subagent')
|
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow('default agent "explore" is a subagent')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -661,7 +666,7 @@ test("defaultAgent throws when default_agent points to hidden agent", async () =
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
await expect(Agent.defaultAgent()).rejects.toThrow('default agent "compaction" is hidden')
|
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow('default agent "compaction" is hidden')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -675,7 +680,9 @@ test("defaultAgent throws when default_agent points to non-existent agent", asyn
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
await expect(Agent.defaultAgent()).rejects.toThrow('default agent "does_not_exist" not found')
|
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow(
|
||||||
|
'default agent "does_not_exist" not found',
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -691,7 +698,7 @@ test("defaultAgent returns plan when build is disabled and default_agent not set
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const agent = await Agent.defaultAgent()
|
const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
||||||
// build is disabled, so it should return plan (next primary agent)
|
// build is disabled, so it should return plan (next primary agent)
|
||||||
expect(agent).toBe("plan")
|
expect(agent).toBe("plan")
|
||||||
},
|
},
|
||||||
|
|
@ -711,7 +718,7 @@ test("defaultAgent throws when all primary agents are disabled", async () => {
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
// build and plan are disabled, no primary-capable agents remain
|
// build and plan are disabled, no primary-capable agents remain
|
||||||
await expect(Agent.defaultAgent()).rejects.toThrow("no primary visible agent found")
|
await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow("no primary visible agent found")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { test, expect } from "bun:test"
|
import { test, expect } from "bun:test"
|
||||||
|
import { Effect } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Config } from "../../src/config/config"
|
import { Config } from "../../src/config/config"
|
||||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
import { Agent as AgentSvc } from "../../src/agent/agent"
|
||||||
|
|
@ -8,6 +9,8 @@ import { Color } from "../../src/util/color"
|
||||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||||
|
|
||||||
const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get()))
|
const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get()))
|
||||||
|
const agent = <A>(dir: string, fn: (svc: AgentSvc.Interface) => Effect.Effect<A>) =>
|
||||||
|
Effect.runPromise(provideInstance(dir)(AgentSvc.Service.use(fn)).pipe(Effect.provide(AgentSvc.defaultLayer)))
|
||||||
|
|
||||||
test("agent color parsed from project config", async () => {
|
test("agent color parsed from project config", async () => {
|
||||||
await using tmp = await tmpdir({
|
await using tmp = await tmpdir({
|
||||||
|
|
@ -52,9 +55,9 @@ test("Agent.get includes color from config", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const plan = await AgentSvc.get("plan")
|
const plan = await agent(tmp.path, (svc) => svc.get("plan"))
|
||||||
expect(plan?.color).toBe("#A855F7")
|
expect(plan?.color).toBe("#A855F7")
|
||||||
const build = await AgentSvc.get("build")
|
const build = await agent(tmp.path, (svc) => svc.get("build"))
|
||||||
expect(build?.color).toBe("accent")
|
expect(build?.color).toBe("accent")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -154,15 +154,22 @@ test("state() generates a new state when none is saved", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
|
const auth = await Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
return yield* McpAuth.Service
|
||||||
|
}).pipe(Effect.provide(McpAuth.defaultLayer)),
|
||||||
|
)
|
||||||
const provider = new McpOAuthProvider(
|
const provider = new McpOAuthProvider(
|
||||||
"test-state-gen",
|
"test-state-gen",
|
||||||
"https://example.com/mcp",
|
"https://example.com/mcp",
|
||||||
{},
|
{},
|
||||||
{ onRedirect: async () => {} },
|
{ onRedirect: async () => {} },
|
||||||
|
auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ensure no state exists
|
const entryBefore = await Effect.runPromise(
|
||||||
const entryBefore = await McpAuth.get("test-state-gen")
|
McpAuth.Service.use((auth) => auth.get("test-state-gen")).pipe(Effect.provide(McpAuth.defaultLayer)),
|
||||||
|
)
|
||||||
expect(entryBefore?.oauthState).toBeUndefined()
|
expect(entryBefore?.oauthState).toBeUndefined()
|
||||||
|
|
||||||
// state() should generate and return a new state, not throw
|
// state() should generate and return a new state, not throw
|
||||||
|
|
@ -171,7 +178,9 @@ test("state() generates a new state when none is saved", async () => {
|
||||||
expect(state.length).toBe(64) // 32 bytes as hex
|
expect(state.length).toBe(64) // 32 bytes as hex
|
||||||
|
|
||||||
// The generated state should be persisted
|
// The generated state should be persisted
|
||||||
const entryAfter = await McpAuth.get("test-state-gen")
|
const entryAfter = await Effect.runPromise(
|
||||||
|
McpAuth.Service.use((auth) => auth.get("test-state-gen")).pipe(Effect.provide(McpAuth.defaultLayer)),
|
||||||
|
)
|
||||||
expect(entryAfter?.oauthState).toBe(state)
|
expect(entryAfter?.oauthState).toBe(state)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -186,16 +195,26 @@ test("state() returns existing state when one is saved", async () => {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
|
const auth = await Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
return yield* McpAuth.Service
|
||||||
|
}).pipe(Effect.provide(McpAuth.defaultLayer)),
|
||||||
|
)
|
||||||
const provider = new McpOAuthProvider(
|
const provider = new McpOAuthProvider(
|
||||||
"test-state-existing",
|
"test-state-existing",
|
||||||
"https://example.com/mcp",
|
"https://example.com/mcp",
|
||||||
{},
|
{},
|
||||||
{ onRedirect: async () => {} },
|
{ onRedirect: async () => {} },
|
||||||
|
auth,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pre-save a state
|
// Pre-save a state
|
||||||
const existingState = "pre-saved-state-value"
|
const existingState = "pre-saved-state-value"
|
||||||
await McpAuth.updateOAuthState("test-state-existing", existingState)
|
await Effect.runPromise(
|
||||||
|
McpAuth.Service.use((auth) => auth.updateOAuthState("test-state-existing", existingState)).pipe(
|
||||||
|
Effect.provide(McpAuth.defaultLayer),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// state() should return the existing state
|
// state() should return the existing state
|
||||||
const state = await provider.state()
|
const state = await provider.state()
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { Session } from "../../src/session"
|
||||||
import { MessageV2 } from "../../src/session/message-v2"
|
import { MessageV2 } from "../../src/session/message-v2"
|
||||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||||
import { SessionStatus } from "../../src/session/status"
|
import { SessionStatus } from "../../src/session/status"
|
||||||
|
import { SessionSummary } from "../../src/session/summary"
|
||||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||||
import type { Provider } from "../../src/provider/provider"
|
import type { Provider } from "../../src/provider/provider"
|
||||||
import * as SessionProcessorModule from "../../src/session/processor"
|
import * as SessionProcessorModule from "../../src/session/processor"
|
||||||
|
|
@ -26,6 +27,15 @@ import { ProviderTest } from "../fake/provider"
|
||||||
|
|
||||||
Log.init({ print: false })
|
Log.init({ print: false })
|
||||||
|
|
||||||
|
const summary = Layer.succeed(
|
||||||
|
SessionSummary.Service,
|
||||||
|
SessionSummary.Service.of({
|
||||||
|
summarize: () => Effect.void,
|
||||||
|
diff: () => Effect.succeed([]),
|
||||||
|
computeDiff: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const ref = {
|
const ref = {
|
||||||
providerID: ProviderID.make("test"),
|
providerID: ProviderID.make("test"),
|
||||||
modelID: ModelID.make("test-model"),
|
modelID: ModelID.make("test-model"),
|
||||||
|
|
@ -194,7 +204,7 @@ function llm() {
|
||||||
function liveRuntime(layer: Layer.Layer<LLM.Service>, provider = ProviderTest.fake()) {
|
function liveRuntime(layer: Layer.Layer<LLM.Service>, provider = ProviderTest.fake()) {
|
||||||
const bus = Bus.layer
|
const bus = Bus.layer
|
||||||
const status = SessionStatus.layer.pipe(Layer.provide(bus))
|
const status = SessionStatus.layer.pipe(Layer.provide(bus))
|
||||||
const processor = SessionProcessorModule.SessionProcessor.layer
|
const processor = SessionProcessorModule.SessionProcessor.layer.pipe(Layer.provide(summary))
|
||||||
return ManagedRuntime.make(
|
return ManagedRuntime.make(
|
||||||
Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, bus, status).pipe(
|
Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, bus, status).pipe(
|
||||||
Layer.provide(provider.layer),
|
Layer.provide(provider.layer),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { MessageV2 } from "../../src/session/message-v2"
|
||||||
import { SessionProcessor } from "../../src/session/processor"
|
import { SessionProcessor } from "../../src/session/processor"
|
||||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||||
import { SessionStatus } from "../../src/session/status"
|
import { SessionStatus } from "../../src/session/status"
|
||||||
|
import { SessionSummary } from "../../src/session/summary"
|
||||||
import { Snapshot } from "../../src/snapshot"
|
import { Snapshot } from "../../src/snapshot"
|
||||||
import { Log } from "../../src/util/log"
|
import { Log } from "../../src/util/log"
|
||||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
|
|
@ -25,6 +26,15 @@ import { raw, reply, TestLLMServer } from "../lib/llm-server"
|
||||||
|
|
||||||
Log.init({ print: false })
|
Log.init({ print: false })
|
||||||
|
|
||||||
|
const summary = Layer.succeed(
|
||||||
|
SessionSummary.Service,
|
||||||
|
SessionSummary.Service.of({
|
||||||
|
summarize: () => Effect.void,
|
||||||
|
diff: () => Effect.succeed([]),
|
||||||
|
computeDiff: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const ref = {
|
const ref = {
|
||||||
providerID: ProviderID.make("test"),
|
providerID: ProviderID.make("test"),
|
||||||
modelID: ModelID.make("test-model"),
|
modelID: ModelID.make("test-model"),
|
||||||
|
|
@ -156,7 +166,10 @@ const deps = Layer.mergeAll(
|
||||||
Provider.defaultLayer,
|
Provider.defaultLayer,
|
||||||
status,
|
status,
|
||||||
).pipe(Layer.provideMerge(infra))
|
).pipe(Layer.provideMerge(infra))
|
||||||
const env = Layer.mergeAll(TestLLMServer.layer, SessionProcessor.layer.pipe(Layer.provideMerge(deps)))
|
const env = Layer.mergeAll(
|
||||||
|
TestLLMServer.layer,
|
||||||
|
SessionProcessor.layer.pipe(Layer.provide(summary), Layer.provideMerge(deps)),
|
||||||
|
)
|
||||||
|
|
||||||
const it = testEffect(env)
|
const it = testEffect(env)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { LLM } from "../../src/session/llm"
|
||||||
import { MessageV2 } from "../../src/session/message-v2"
|
import { MessageV2 } from "../../src/session/message-v2"
|
||||||
import { AppFileSystem } from "../../src/filesystem"
|
import { AppFileSystem } from "../../src/filesystem"
|
||||||
import { SessionCompaction } from "../../src/session/compaction"
|
import { SessionCompaction } from "../../src/session/compaction"
|
||||||
|
import { SessionSummary } from "../../src/session/summary"
|
||||||
import { Instruction } from "../../src/session/instruction"
|
import { Instruction } from "../../src/session/instruction"
|
||||||
import { SessionProcessor } from "../../src/session/processor"
|
import { SessionProcessor } from "../../src/session/processor"
|
||||||
import { SessionPrompt } from "../../src/session/prompt"
|
import { SessionPrompt } from "../../src/session/prompt"
|
||||||
|
|
@ -46,6 +47,15 @@ import { reply, TestLLMServer } from "../lib/llm-server"
|
||||||
|
|
||||||
Log.init({ print: false })
|
Log.init({ print: false })
|
||||||
|
|
||||||
|
const summary = Layer.succeed(
|
||||||
|
SessionSummary.Service,
|
||||||
|
SessionSummary.Service.of({
|
||||||
|
summarize: () => Effect.void,
|
||||||
|
diff: () => Effect.succeed([]),
|
||||||
|
computeDiff: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const ref = {
|
const ref = {
|
||||||
providerID: ProviderID.make("test"),
|
providerID: ProviderID.make("test"),
|
||||||
modelID: ModelID.make("test-model"),
|
modelID: ModelID.make("test-model"),
|
||||||
|
|
@ -182,12 +192,13 @@ function makeHttp() {
|
||||||
Layer.provideMerge(deps),
|
Layer.provideMerge(deps),
|
||||||
)
|
)
|
||||||
const trunc = Truncate.layer.pipe(Layer.provideMerge(deps))
|
const trunc = Truncate.layer.pipe(Layer.provideMerge(deps))
|
||||||
const proc = SessionProcessor.layer.pipe(Layer.provideMerge(deps))
|
const proc = SessionProcessor.layer.pipe(Layer.provide(summary), Layer.provideMerge(deps))
|
||||||
const compact = SessionCompaction.layer.pipe(Layer.provideMerge(proc), Layer.provideMerge(deps))
|
const compact = SessionCompaction.layer.pipe(Layer.provideMerge(proc), Layer.provideMerge(deps))
|
||||||
return Layer.mergeAll(
|
return Layer.mergeAll(
|
||||||
TestLLMServer.layer,
|
TestLLMServer.layer,
|
||||||
SessionPrompt.layer.pipe(
|
SessionPrompt.layer.pipe(
|
||||||
Layer.provide(SessionRevert.defaultLayer),
|
Layer.provide(SessionRevert.defaultLayer),
|
||||||
|
Layer.provide(summary),
|
||||||
Layer.provideMerge(run),
|
Layer.provideMerge(run),
|
||||||
Layer.provideMerge(compact),
|
Layer.provideMerge(compact),
|
||||||
Layer.provideMerge(proc),
|
Layer.provideMerge(proc),
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -146,12 +146,14 @@ function makeHttp() {
|
||||||
Layer.provideMerge(deps),
|
Layer.provideMerge(deps),
|
||||||
)
|
)
|
||||||
const trunc = Truncate.layer.pipe(Layer.provideMerge(deps))
|
const trunc = Truncate.layer.pipe(Layer.provideMerge(deps))
|
||||||
const proc = SessionProcessor.layer.pipe(Layer.provideMerge(deps))
|
const proc = SessionProcessor.layer.pipe(Layer.provide(SessionSummary.defaultLayer), Layer.provideMerge(deps))
|
||||||
const compact = SessionCompaction.layer.pipe(Layer.provideMerge(proc), Layer.provideMerge(deps))
|
const compact = SessionCompaction.layer.pipe(Layer.provideMerge(proc), Layer.provideMerge(deps))
|
||||||
return Layer.mergeAll(
|
return Layer.mergeAll(
|
||||||
TestLLMServer.layer,
|
TestLLMServer.layer,
|
||||||
|
SessionSummary.defaultLayer,
|
||||||
SessionPrompt.layer.pipe(
|
SessionPrompt.layer.pipe(
|
||||||
Layer.provide(SessionRevert.defaultLayer),
|
Layer.provide(SessionRevert.defaultLayer),
|
||||||
|
Layer.provide(SessionSummary.defaultLayer),
|
||||||
Layer.provideMerge(run),
|
Layer.provideMerge(run),
|
||||||
Layer.provideMerge(compact),
|
Layer.provideMerge(compact),
|
||||||
Layer.provideMerge(proc),
|
Layer.provideMerge(proc),
|
||||||
|
|
@ -200,6 +202,7 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () =>
|
||||||
Effect.fnUntraced(function* ({ dir, llm }) {
|
Effect.fnUntraced(function* ({ dir, llm }) {
|
||||||
const prompt = yield* SessionPrompt.Service
|
const prompt = yield* SessionPrompt.Service
|
||||||
const sessions = yield* Session.Service
|
const sessions = yield* Session.Service
|
||||||
|
const summary = yield* SessionSummary.Service
|
||||||
|
|
||||||
const session = yield* sessions.create({
|
const session = yield* sessions.create({
|
||||||
title: "snapshot race test",
|
title: "snapshot race test",
|
||||||
|
|
@ -244,9 +247,9 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () =>
|
||||||
expect(tool?.state.status).toBe("completed")
|
expect(tool?.state.status).toBe("completed")
|
||||||
|
|
||||||
// Poll for diff — summarize() is fire-and-forget
|
// Poll for diff — summarize() is fire-and-forget
|
||||||
let diff: Awaited<ReturnType<typeof SessionSummary.diff>> = []
|
let diff: Array<{ file: string }> = []
|
||||||
for (let i = 0; i < 50; i++) {
|
for (let i = 0; i < 50; i++) {
|
||||||
diff = yield* Effect.promise(() => SessionSummary.diff({ sessionID: session.id }))
|
diff = yield* summary.diff({ sessionID: session.id })
|
||||||
if (diff.length > 0) break
|
if (diff.length > 0) break
|
||||||
yield* Effect.sleep("100 millis")
|
yield* Effect.sleep("100 millis")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import { Effect } from "effect"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent/agent"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { SystemPrompt } from "../../src/session/system"
|
import { SystemPrompt } from "../../src/session/system"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||||
|
|
||||||
|
function load<A>(dir: string, fn: (svc: Agent.Interface) => Effect.Effect<A>) {
|
||||||
|
return Effect.runPromise(provideInstance(dir)(Agent.Service.use(fn)).pipe(Effect.provide(Agent.defaultLayer)))
|
||||||
|
}
|
||||||
|
|
||||||
describe("session.system", () => {
|
describe("session.system", () => {
|
||||||
test("skills output is sorted by name and stable across calls", async () => {
|
test("skills output is sorted by name and stable across calls", async () => {
|
||||||
|
|
@ -38,7 +42,7 @@ description: ${description}
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const build = await Agent.get("build")
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
const runSkills = Effect.gen(function* () {
|
const runSkills = Effect.gen(function* () {
|
||||||
const svc = yield* SystemPrompt.Service
|
const svc = yield* SystemPrompt.Service
|
||||||
return yield* svc.skills(build!)
|
return yield* svc.skills(build!)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ import type {
|
||||||
GlobalDisposeResponses,
|
GlobalDisposeResponses,
|
||||||
GlobalEventResponses,
|
GlobalEventResponses,
|
||||||
GlobalHealthResponses,
|
GlobalHealthResponses,
|
||||||
GlobalSyncEventSubscribeResponses,
|
|
||||||
GlobalUpgradeErrors,
|
GlobalUpgradeErrors,
|
||||||
GlobalUpgradeResponses,
|
GlobalUpgradeResponses,
|
||||||
InstanceDisposeResponses,
|
InstanceDisposeResponses,
|
||||||
|
|
@ -237,20 +236,6 @@ class HeyApiRegistry<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SyncEvent extends HeyApiClient {
|
|
||||||
/**
|
|
||||||
* Subscribe to global sync events
|
|
||||||
*
|
|
||||||
* Get global sync events
|
|
||||||
*/
|
|
||||||
public subscribe<ThrowOnError extends boolean = false>(options?: Options<never, ThrowOnError>) {
|
|
||||||
return (options?.client ?? this.client).sse.get<GlobalSyncEventSubscribeResponses, unknown, ThrowOnError>({
|
|
||||||
url: "/global/sync-event",
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Config extends HeyApiClient {
|
export class Config extends HeyApiClient {
|
||||||
/**
|
/**
|
||||||
* Get global configuration
|
* Get global configuration
|
||||||
|
|
@ -350,11 +335,6 @@ export class Global extends HeyApiClient {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _syncEvent?: SyncEvent
|
|
||||||
get syncEvent(): SyncEvent {
|
|
||||||
return (this._syncEvent ??= new SyncEvent({ client: this.client }))
|
|
||||||
}
|
|
||||||
|
|
||||||
private _config?: Config
|
private _config?: Config
|
||||||
get config(): Config {
|
get config(): Config {
|
||||||
return (this._config ??= new Config({ client: this.client }))
|
return (this._config ??= new Config({ client: this.client }))
|
||||||
|
|
|
||||||
|
|
@ -971,64 +971,12 @@ export type EventSessionDeleted = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Event =
|
|
||||||
| EventProjectUpdated
|
|
||||||
| EventServerInstanceDisposed
|
|
||||||
| EventInstallationUpdated
|
|
||||||
| EventInstallationUpdateAvailable
|
|
||||||
| EventServerConnected
|
|
||||||
| EventGlobalDisposed
|
|
||||||
| EventFileEdited
|
|
||||||
| EventFileWatcherUpdated
|
|
||||||
| EventLspClientDiagnostics
|
|
||||||
| EventLspUpdated
|
|
||||||
| EventMessagePartDelta
|
|
||||||
| EventPermissionAsked
|
|
||||||
| EventPermissionReplied
|
|
||||||
| EventSessionDiff
|
|
||||||
| EventSessionError
|
|
||||||
| EventQuestionAsked
|
|
||||||
| EventQuestionReplied
|
|
||||||
| EventQuestionRejected
|
|
||||||
| EventTodoUpdated
|
|
||||||
| EventSessionStatus
|
|
||||||
| EventSessionIdle
|
|
||||||
| EventSessionCompacted
|
|
||||||
| EventTuiPromptAppend
|
|
||||||
| EventTuiCommandExecute
|
|
||||||
| EventTuiToastShow
|
|
||||||
| EventTuiSessionSelect
|
|
||||||
| EventMcpToolsChanged
|
|
||||||
| EventMcpBrowserOpenFailed
|
|
||||||
| EventCommandExecuted
|
|
||||||
| EventVcsBranchUpdated
|
|
||||||
| EventWorktreeReady
|
|
||||||
| EventWorktreeFailed
|
|
||||||
| EventPtyCreated
|
|
||||||
| EventPtyUpdated
|
|
||||||
| EventPtyExited
|
|
||||||
| EventPtyDeleted
|
|
||||||
| EventWorkspaceReady
|
|
||||||
| EventWorkspaceFailed
|
|
||||||
| EventWorkspaceStatus
|
|
||||||
| EventMessageUpdated
|
|
||||||
| EventMessageRemoved
|
|
||||||
| EventMessagePartUpdated
|
|
||||||
| EventMessagePartRemoved
|
|
||||||
| EventSessionCreated
|
|
||||||
| EventSessionUpdated
|
|
||||||
| EventSessionDeleted
|
|
||||||
|
|
||||||
export type GlobalEvent = {
|
|
||||||
directory: string
|
|
||||||
project?: string
|
|
||||||
workspace?: string
|
|
||||||
payload: Event
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SyncEventMessageUpdated = {
|
export type SyncEventMessageUpdated = {
|
||||||
type: "message.updated.1"
|
type: "sync"
|
||||||
aggregate: "sessionID"
|
name: "message.updated.1"
|
||||||
|
id: string
|
||||||
|
seq: number
|
||||||
|
aggregateID: "sessionID"
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
info: Message
|
info: Message
|
||||||
|
|
@ -1036,8 +984,11 @@ export type SyncEventMessageUpdated = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncEventMessageRemoved = {
|
export type SyncEventMessageRemoved = {
|
||||||
type: "message.removed.1"
|
type: "sync"
|
||||||
aggregate: "sessionID"
|
name: "message.removed.1"
|
||||||
|
id: string
|
||||||
|
seq: number
|
||||||
|
aggregateID: "sessionID"
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
|
|
@ -1045,8 +996,11 @@ export type SyncEventMessageRemoved = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncEventMessagePartUpdated = {
|
export type SyncEventMessagePartUpdated = {
|
||||||
type: "message.part.updated.1"
|
type: "sync"
|
||||||
aggregate: "sessionID"
|
name: "message.part.updated.1"
|
||||||
|
id: string
|
||||||
|
seq: number
|
||||||
|
aggregateID: "sessionID"
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
part: Part
|
part: Part
|
||||||
|
|
@ -1055,8 +1009,11 @@ export type SyncEventMessagePartUpdated = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncEventMessagePartRemoved = {
|
export type SyncEventMessagePartRemoved = {
|
||||||
type: "message.part.removed.1"
|
type: "sync"
|
||||||
aggregate: "sessionID"
|
name: "message.part.removed.1"
|
||||||
|
id: string
|
||||||
|
seq: number
|
||||||
|
aggregateID: "sessionID"
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
|
|
@ -1065,8 +1022,11 @@ export type SyncEventMessagePartRemoved = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncEventSessionCreated = {
|
export type SyncEventSessionCreated = {
|
||||||
type: "session.created.1"
|
type: "sync"
|
||||||
aggregate: "sessionID"
|
name: "session.created.1"
|
||||||
|
id: string
|
||||||
|
seq: number
|
||||||
|
aggregateID: "sessionID"
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
info: Session
|
info: Session
|
||||||
|
|
@ -1074,8 +1034,11 @@ export type SyncEventSessionCreated = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncEventSessionUpdated = {
|
export type SyncEventSessionUpdated = {
|
||||||
type: "session.updated.1"
|
type: "sync"
|
||||||
aggregate: "sessionID"
|
name: "session.updated.1"
|
||||||
|
id: string
|
||||||
|
seq: number
|
||||||
|
aggregateID: "sessionID"
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
info: {
|
info: {
|
||||||
|
|
@ -1114,16 +1077,75 @@ export type SyncEventSessionUpdated = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncEventSessionDeleted = {
|
export type SyncEventSessionDeleted = {
|
||||||
type: "session.deleted.1"
|
type: "sync"
|
||||||
aggregate: "sessionID"
|
name: "session.deleted.1"
|
||||||
|
id: string
|
||||||
|
seq: number
|
||||||
|
aggregateID: "sessionID"
|
||||||
data: {
|
data: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
info: Session
|
info: Session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncEvent = {
|
export type GlobalEvent = {
|
||||||
payload: SyncEvent
|
directory: string
|
||||||
|
project?: string
|
||||||
|
workspace?: string
|
||||||
|
payload:
|
||||||
|
| EventProjectUpdated
|
||||||
|
| EventServerInstanceDisposed
|
||||||
|
| EventInstallationUpdated
|
||||||
|
| EventInstallationUpdateAvailable
|
||||||
|
| EventServerConnected
|
||||||
|
| EventGlobalDisposed
|
||||||
|
| EventFileEdited
|
||||||
|
| EventFileWatcherUpdated
|
||||||
|
| EventLspClientDiagnostics
|
||||||
|
| EventLspUpdated
|
||||||
|
| EventMessagePartDelta
|
||||||
|
| EventPermissionAsked
|
||||||
|
| EventPermissionReplied
|
||||||
|
| EventSessionDiff
|
||||||
|
| EventSessionError
|
||||||
|
| EventQuestionAsked
|
||||||
|
| EventQuestionReplied
|
||||||
|
| EventQuestionRejected
|
||||||
|
| EventTodoUpdated
|
||||||
|
| EventSessionStatus
|
||||||
|
| EventSessionIdle
|
||||||
|
| EventSessionCompacted
|
||||||
|
| EventTuiPromptAppend
|
||||||
|
| EventTuiCommandExecute
|
||||||
|
| EventTuiToastShow
|
||||||
|
| EventTuiSessionSelect
|
||||||
|
| EventMcpToolsChanged
|
||||||
|
| EventMcpBrowserOpenFailed
|
||||||
|
| EventCommandExecuted
|
||||||
|
| EventVcsBranchUpdated
|
||||||
|
| EventWorktreeReady
|
||||||
|
| EventWorktreeFailed
|
||||||
|
| EventPtyCreated
|
||||||
|
| EventPtyUpdated
|
||||||
|
| EventPtyExited
|
||||||
|
| EventPtyDeleted
|
||||||
|
| EventWorkspaceReady
|
||||||
|
| EventWorkspaceFailed
|
||||||
|
| EventWorkspaceStatus
|
||||||
|
| EventMessageUpdated
|
||||||
|
| EventMessageRemoved
|
||||||
|
| EventMessagePartUpdated
|
||||||
|
| EventMessagePartRemoved
|
||||||
|
| EventSessionCreated
|
||||||
|
| EventSessionUpdated
|
||||||
|
| EventSessionDeleted
|
||||||
|
| SyncEventMessageUpdated
|
||||||
|
| SyncEventMessageRemoved
|
||||||
|
| SyncEventMessagePartUpdated
|
||||||
|
| SyncEventMessagePartRemoved
|
||||||
|
| SyncEventSessionCreated
|
||||||
|
| SyncEventSessionUpdated
|
||||||
|
| SyncEventSessionDeleted
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1982,6 +2004,54 @@ export type File = {
|
||||||
status: "added" | "deleted" | "modified"
|
status: "added" | "deleted" | "modified"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Event =
|
||||||
|
| EventProjectUpdated
|
||||||
|
| EventServerInstanceDisposed
|
||||||
|
| EventInstallationUpdated
|
||||||
|
| EventInstallationUpdateAvailable
|
||||||
|
| EventServerConnected
|
||||||
|
| EventGlobalDisposed
|
||||||
|
| EventFileEdited
|
||||||
|
| EventFileWatcherUpdated
|
||||||
|
| EventLspClientDiagnostics
|
||||||
|
| EventLspUpdated
|
||||||
|
| EventMessagePartDelta
|
||||||
|
| EventPermissionAsked
|
||||||
|
| EventPermissionReplied
|
||||||
|
| EventSessionDiff
|
||||||
|
| EventSessionError
|
||||||
|
| EventQuestionAsked
|
||||||
|
| EventQuestionReplied
|
||||||
|
| EventQuestionRejected
|
||||||
|
| EventTodoUpdated
|
||||||
|
| EventSessionStatus
|
||||||
|
| EventSessionIdle
|
||||||
|
| EventSessionCompacted
|
||||||
|
| EventTuiPromptAppend
|
||||||
|
| EventTuiCommandExecute
|
||||||
|
| EventTuiToastShow
|
||||||
|
| EventTuiSessionSelect
|
||||||
|
| EventMcpToolsChanged
|
||||||
|
| EventMcpBrowserOpenFailed
|
||||||
|
| EventCommandExecuted
|
||||||
|
| EventVcsBranchUpdated
|
||||||
|
| EventWorktreeReady
|
||||||
|
| EventWorktreeFailed
|
||||||
|
| EventPtyCreated
|
||||||
|
| EventPtyUpdated
|
||||||
|
| EventPtyExited
|
||||||
|
| EventPtyDeleted
|
||||||
|
| EventWorkspaceReady
|
||||||
|
| EventWorkspaceFailed
|
||||||
|
| EventWorkspaceStatus
|
||||||
|
| EventMessageUpdated
|
||||||
|
| EventMessageRemoved
|
||||||
|
| EventMessagePartUpdated
|
||||||
|
| EventMessagePartRemoved
|
||||||
|
| EventSessionCreated
|
||||||
|
| EventSessionUpdated
|
||||||
|
| EventSessionDeleted
|
||||||
|
|
||||||
export type McpStatusConnected = {
|
export type McpStatusConnected = {
|
||||||
status: "connected"
|
status: "connected"
|
||||||
}
|
}
|
||||||
|
|
@ -2113,23 +2183,6 @@ export type GlobalEventResponses = {
|
||||||
|
|
||||||
export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses]
|
export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses]
|
||||||
|
|
||||||
export type GlobalSyncEventSubscribeData = {
|
|
||||||
body?: never
|
|
||||||
path?: never
|
|
||||||
query?: never
|
|
||||||
url: "/global/sync-event"
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GlobalSyncEventSubscribeResponses = {
|
|
||||||
/**
|
|
||||||
* Event stream
|
|
||||||
*/
|
|
||||||
200: SyncEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GlobalSyncEventSubscribeResponse =
|
|
||||||
GlobalSyncEventSubscribeResponses[keyof GlobalSyncEventSubscribeResponses]
|
|
||||||
|
|
||||||
export type GlobalConfigGetData = {
|
export type GlobalConfigGetData = {
|
||||||
body?: never
|
body?: never
|
||||||
path?: never
|
path?: never
|
||||||
|
|
|
||||||
|
|
@ -66,31 +66,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/global/sync-event": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "global.sync-event.subscribe",
|
|
||||||
"summary": "Subscribe to global sync events",
|
|
||||||
"description": "Get global sync events",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Event stream",
|
|
||||||
"content": {
|
|
||||||
"text/event-stream": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SyncEvent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-codeSamples": [
|
|
||||||
{
|
|
||||||
"lang": "js",
|
|
||||||
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.sync-event.subscribe({\n ...\n})"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/global/config": {
|
"/global/config": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "global.config.get",
|
"operationId": "global.config.get",
|
||||||
|
|
@ -9925,174 +9900,24 @@
|
||||||
},
|
},
|
||||||
"required": ["type", "properties"]
|
"required": ["type", "properties"]
|
||||||
},
|
},
|
||||||
"Event": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.project.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.server.instance.disposed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.installation.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.installation.update-available"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.server.connected"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.global.disposed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.file.edited"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.file.watcher.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.lsp.client.diagnostics"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.lsp.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.message.part.delta"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.permission.asked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.permission.replied"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.session.diff"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.session.error"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.question.asked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.question.replied"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.question.rejected"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.todo.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.session.status"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.session.idle"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.session.compacted"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.tui.toast.show"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.mcp.tools.changed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.mcp.browser.open.failed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.command.executed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.vcs.branch.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.worktree.ready"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.worktree.failed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.pty.created"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.pty.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.pty.exited"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.pty.deleted"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.ready"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.failed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.workspace.status"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.message.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.message.removed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.message.part.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.message.part.removed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.session.created"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.session.updated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Event.session.deleted"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"GlobalEvent": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"directory": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"project": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"workspace": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"$ref": "#/components/schemas/Event"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["directory", "payload"]
|
|
||||||
},
|
|
||||||
"SyncEvent.message.updated": {
|
"SyncEvent.message.updated": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "sync"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "message.updated.1"
|
"const": "message.updated.1"
|
||||||
},
|
},
|
||||||
"aggregate": {
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"aggregateID": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "sessionID"
|
"const": "sessionID"
|
||||||
},
|
},
|
||||||
|
|
@ -10110,16 +9935,26 @@
|
||||||
"required": ["sessionID", "info"]
|
"required": ["sessionID", "info"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["type", "aggregate", "data"]
|
"required": ["type", "name", "id", "seq", "aggregateID", "data"]
|
||||||
},
|
},
|
||||||
"SyncEvent.message.removed": {
|
"SyncEvent.message.removed": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "sync"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "message.removed.1"
|
"const": "message.removed.1"
|
||||||
},
|
},
|
||||||
"aggregate": {
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"aggregateID": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "sessionID"
|
"const": "sessionID"
|
||||||
},
|
},
|
||||||
|
|
@ -10138,16 +9973,26 @@
|
||||||
"required": ["sessionID", "messageID"]
|
"required": ["sessionID", "messageID"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["type", "aggregate", "data"]
|
"required": ["type", "name", "id", "seq", "aggregateID", "data"]
|
||||||
},
|
},
|
||||||
"SyncEvent.message.part.updated": {
|
"SyncEvent.message.part.updated": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "sync"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "message.part.updated.1"
|
"const": "message.part.updated.1"
|
||||||
},
|
},
|
||||||
"aggregate": {
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"aggregateID": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "sessionID"
|
"const": "sessionID"
|
||||||
},
|
},
|
||||||
|
|
@ -10168,16 +10013,26 @@
|
||||||
"required": ["sessionID", "part", "time"]
|
"required": ["sessionID", "part", "time"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["type", "aggregate", "data"]
|
"required": ["type", "name", "id", "seq", "aggregateID", "data"]
|
||||||
},
|
},
|
||||||
"SyncEvent.message.part.removed": {
|
"SyncEvent.message.part.removed": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "sync"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "message.part.removed.1"
|
"const": "message.part.removed.1"
|
||||||
},
|
},
|
||||||
"aggregate": {
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"aggregateID": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "sessionID"
|
"const": "sessionID"
|
||||||
},
|
},
|
||||||
|
|
@ -10200,16 +10055,26 @@
|
||||||
"required": ["sessionID", "messageID", "partID"]
|
"required": ["sessionID", "messageID", "partID"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["type", "aggregate", "data"]
|
"required": ["type", "name", "id", "seq", "aggregateID", "data"]
|
||||||
},
|
},
|
||||||
"SyncEvent.session.created": {
|
"SyncEvent.session.created": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "sync"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "session.created.1"
|
"const": "session.created.1"
|
||||||
},
|
},
|
||||||
"aggregate": {
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"aggregateID": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "sessionID"
|
"const": "sessionID"
|
||||||
},
|
},
|
||||||
|
|
@ -10227,16 +10092,26 @@
|
||||||
"required": ["sessionID", "info"]
|
"required": ["sessionID", "info"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["type", "aggregate", "data"]
|
"required": ["type", "name", "id", "seq", "aggregateID", "data"]
|
||||||
},
|
},
|
||||||
"SyncEvent.session.updated": {
|
"SyncEvent.session.updated": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "sync"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "session.updated.1"
|
"const": "session.updated.1"
|
||||||
},
|
},
|
||||||
"aggregate": {
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"aggregateID": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "sessionID"
|
"const": "sessionID"
|
||||||
},
|
},
|
||||||
|
|
@ -10479,16 +10354,26 @@
|
||||||
"required": ["sessionID", "info"]
|
"required": ["sessionID", "info"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["type", "aggregate", "data"]
|
"required": ["type", "name", "id", "seq", "aggregateID", "data"]
|
||||||
},
|
},
|
||||||
"SyncEvent.session.deleted": {
|
"SyncEvent.session.deleted": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "sync"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "session.deleted.1"
|
"const": "session.deleted.1"
|
||||||
},
|
},
|
||||||
"aggregate": {
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"aggregateID": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "sessionID"
|
"const": "sessionID"
|
||||||
},
|
},
|
||||||
|
|
@ -10506,16 +10391,185 @@
|
||||||
"required": ["sessionID", "info"]
|
"required": ["sessionID", "info"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["type", "aggregate", "data"]
|
"required": ["type", "name", "id", "seq", "aggregateID", "data"]
|
||||||
},
|
},
|
||||||
"SyncEvent": {
|
"GlobalEvent": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"directory": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"payload": {
|
"payload": {
|
||||||
"$ref": "#/components/schemas/SyncEvent"
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.project.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.server.instance.disposed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.installation.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.installation.update-available"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.server.connected"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.global.disposed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.file.edited"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.file.watcher.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.lsp.client.diagnostics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.lsp.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.part.delta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.permission.asked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.permission.replied"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.diff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.question.asked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.question.replied"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.question.rejected"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.todo.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.idle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.compacted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.tui.toast.show"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.mcp.tools.changed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.mcp.browser.open.failed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.command.executed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.vcs.branch.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.worktree.ready"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.worktree.failed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.pty.created"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.pty.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.pty.exited"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.pty.deleted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.ready"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.failed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.removed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.part.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.part.removed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.created"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.deleted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/SyncEvent.message.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/SyncEvent.message.removed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/SyncEvent.message.part.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/SyncEvent.message.part.removed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/SyncEvent.session.created"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/SyncEvent.session.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/SyncEvent.session.deleted"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["payload"]
|
"required": ["directory", "payload"]
|
||||||
},
|
},
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"description": "Log level",
|
"description": "Log level",
|
||||||
|
|
@ -12608,6 +12662,148 @@
|
||||||
},
|
},
|
||||||
"required": ["path", "added", "removed", "status"]
|
"required": ["path", "added", "removed", "status"]
|
||||||
},
|
},
|
||||||
|
"Event": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.project.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.server.instance.disposed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.installation.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.installation.update-available"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.server.connected"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.global.disposed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.file.edited"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.file.watcher.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.lsp.client.diagnostics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.lsp.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.part.delta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.permission.asked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.permission.replied"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.diff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.question.asked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.question.replied"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.question.rejected"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.todo.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.idle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.compacted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.tui.toast.show"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.mcp.tools.changed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.mcp.browser.open.failed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.command.executed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.vcs.branch.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.worktree.ready"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.worktree.failed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.pty.created"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.pty.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.pty.exited"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.pty.deleted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.ready"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.failed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.workspace.status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.removed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.part.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.message.part.removed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.created"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.deleted"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"MCPStatusConnected": {
|
"MCPStatusConnected": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue