feat(acp): promote next implementation (#29929)
This commit is contained in:
@@ -1,95 +0,0 @@
|
||||
import {
|
||||
RequestError,
|
||||
type Agent as ACPAgent,
|
||||
type AgentSideConnection,
|
||||
type AuthenticateRequest,
|
||||
type CancelNotification,
|
||||
type CloseSessionRequest,
|
||||
type ForkSessionRequest,
|
||||
type InitializeRequest,
|
||||
type ListSessionsRequest,
|
||||
type LoadSessionRequest,
|
||||
type NewSessionRequest,
|
||||
type PromptRequest,
|
||||
type ResumeSessionRequest,
|
||||
type SetSessionConfigOptionRequest,
|
||||
type SetSessionModelRequest,
|
||||
type SetSessionModeRequest,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
import { Effect } from "effect"
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import * as ACPNextError from "./error"
|
||||
import * as ACPNextService from "./service"
|
||||
|
||||
export function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
|
||||
return {
|
||||
create: (connection: AgentSideConnection) => {
|
||||
return new Agent(ACPNextService.make({ sdk: _sdk, connection }))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export class Agent implements ACPAgent {
|
||||
constructor(private readonly service: ACPNextService.Interface) {}
|
||||
|
||||
initialize(params: InitializeRequest) {
|
||||
return run(this.service.initialize(params))
|
||||
}
|
||||
|
||||
authenticate(params: AuthenticateRequest) {
|
||||
return run(this.service.authenticate(params))
|
||||
}
|
||||
|
||||
newSession(params: NewSessionRequest) {
|
||||
return run(this.service.newSession(params))
|
||||
}
|
||||
|
||||
loadSession(params: LoadSessionRequest) {
|
||||
return run(this.service.loadSession(params))
|
||||
}
|
||||
|
||||
listSessions(params: ListSessionsRequest) {
|
||||
return run(this.service.listSessions(params))
|
||||
}
|
||||
|
||||
resumeSession(params: ResumeSessionRequest) {
|
||||
return run(this.service.resumeSession(params))
|
||||
}
|
||||
|
||||
closeSession(params: CloseSessionRequest) {
|
||||
return run(this.service.closeSession(params))
|
||||
}
|
||||
|
||||
unstable_forkSession(params: ForkSessionRequest) {
|
||||
return run(this.service.forkSession(params))
|
||||
}
|
||||
|
||||
setSessionConfigOption(params: SetSessionConfigOptionRequest) {
|
||||
return run(this.service.setSessionConfigOption(params))
|
||||
}
|
||||
|
||||
setSessionMode(params: SetSessionModeRequest) {
|
||||
return run(this.service.setSessionMode(params))
|
||||
}
|
||||
|
||||
unstable_setSessionModel(params: SetSessionModelRequest) {
|
||||
return run(this.service.setSessionModel(params))
|
||||
}
|
||||
|
||||
prompt(params: PromptRequest) {
|
||||
return run(this.service.prompt(params))
|
||||
}
|
||||
|
||||
cancel(params: CancelNotification) {
|
||||
return run(this.service.cancel(params))
|
||||
}
|
||||
}
|
||||
|
||||
function run<A>(effect: Effect.Effect<A, ACPNextService.Error>) {
|
||||
return Effect.runPromise(effect.pipe(Effect.mapError(ACPNextError.toRequestError))).catch((defect: unknown) => {
|
||||
if (defect instanceof RequestError) throw defect
|
||||
throw ACPNextError.toRequestError(ACPNextError.fromUnknownDefect(defect))
|
||||
})
|
||||
}
|
||||
|
||||
export * as ACPNext from "./agent"
|
||||
@@ -1,232 +0,0 @@
|
||||
import type { McpServer } from "@agentclientprotocol/sdk"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2"
|
||||
import { Context, Effect, Layer, Ref } from "effect"
|
||||
import type { ModelID, ProviderID } from "../provider/schema"
|
||||
import * as ACPNextError from "./error"
|
||||
|
||||
export type SelectedModel = {
|
||||
providerID: ProviderID
|
||||
modelID: ModelID
|
||||
}
|
||||
|
||||
export type KnownMessagePartMetadata = {
|
||||
messageId: string
|
||||
partId: string
|
||||
partType?: Part["type"]
|
||||
role?: Message["role"]
|
||||
ignored?: boolean
|
||||
toolCallId?: string
|
||||
metadata?: unknown
|
||||
}
|
||||
|
||||
export type Info = {
|
||||
id: string
|
||||
cwd: string
|
||||
mcpServers: readonly McpServer[]
|
||||
createdAt: Date
|
||||
model?: SelectedModel
|
||||
variant?: string
|
||||
modeId?: string
|
||||
knownParts: ReadonlyMap<string, KnownMessagePartMetadata>
|
||||
}
|
||||
|
||||
export type StoreInput = {
|
||||
id: string
|
||||
cwd: string
|
||||
mcpServers?: readonly McpServer[]
|
||||
createdAt?: Date
|
||||
model?: SelectedModel
|
||||
variant?: string
|
||||
modeId?: string
|
||||
}
|
||||
|
||||
export type RecordPartMetadataInput = {
|
||||
sessionId: string
|
||||
messageId: string
|
||||
partId: string
|
||||
partType?: Part["type"]
|
||||
role?: Message["role"]
|
||||
ignored?: boolean
|
||||
toolCallId?: string
|
||||
metadata?: unknown
|
||||
}
|
||||
|
||||
export type PartMetadataLookupInput = {
|
||||
sessionId: string
|
||||
messageId: string
|
||||
partId: string
|
||||
}
|
||||
|
||||
export type Interface = {
|
||||
readonly create: (input: StoreInput) => Effect.Effect<Info>
|
||||
readonly load: (input: StoreInput) => Effect.Effect<Info>
|
||||
readonly list: (cwd?: string) => Effect.Effect<readonly Info[]>
|
||||
readonly get: (sessionId: string) => Effect.Effect<Info, ACPNextError.SessionNotFoundError>
|
||||
readonly tryGet: (sessionId: string) => Effect.Effect<Info | undefined>
|
||||
readonly remove: (sessionId: string) => Effect.Effect<Info | undefined>
|
||||
readonly setModel: (
|
||||
sessionId: string,
|
||||
model: SelectedModel | undefined,
|
||||
) => Effect.Effect<Info, ACPNextError.SessionNotFoundError>
|
||||
readonly getModel: (sessionId: string) => Effect.Effect<SelectedModel | undefined, ACPNextError.SessionNotFoundError>
|
||||
readonly setVariant: (
|
||||
sessionId: string,
|
||||
variant: string | undefined,
|
||||
) => Effect.Effect<Info, ACPNextError.SessionNotFoundError>
|
||||
readonly getVariant: (sessionId: string) => Effect.Effect<string | undefined, ACPNextError.SessionNotFoundError>
|
||||
readonly setMode: (
|
||||
sessionId: string,
|
||||
modeId: string | undefined,
|
||||
) => Effect.Effect<Info, ACPNextError.SessionNotFoundError>
|
||||
readonly getMode: (sessionId: string) => Effect.Effect<string | undefined, ACPNextError.SessionNotFoundError>
|
||||
readonly recordPartMetadata: (
|
||||
input: RecordPartMetadataInput,
|
||||
) => Effect.Effect<KnownMessagePartMetadata, ACPNextError.SessionNotFoundError>
|
||||
readonly getPartMetadata: (
|
||||
input: PartMetadataLookupInput,
|
||||
) => Effect.Effect<KnownMessagePartMetadata | undefined, ACPNextError.SessionNotFoundError>
|
||||
readonly tryGetPartMetadata: (input: PartMetadataLookupInput) => Effect.Effect<KnownMessagePartMetadata | undefined>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ACPNext/Session") {}
|
||||
|
||||
type State = Map<string, Info>
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Ref.make<State>(new Map())
|
||||
|
||||
const store = Effect.fn("ACPNext.Session.store")(function* (input: StoreInput) {
|
||||
const session = makeSession(input)
|
||||
yield* Ref.update(sessions, (state) => new Map(state).set(session.id, session))
|
||||
return snapshot(session)
|
||||
})
|
||||
|
||||
const tryGet = Effect.fn("ACPNext.Session.tryGet")(function* (sessionId: string) {
|
||||
const session = (yield* Ref.get(sessions)).get(sessionId)
|
||||
if (!session) return
|
||||
return snapshot(session)
|
||||
})
|
||||
|
||||
const get = Effect.fn("ACPNext.Session.get")(function* (sessionId: string) {
|
||||
const session = yield* tryGet(sessionId)
|
||||
if (session) return session
|
||||
return yield* new ACPNextError.SessionNotFoundError({ sessionId })
|
||||
})
|
||||
|
||||
const update = Effect.fn("ACPNext.Session.update")(function* (sessionId: string, fn: (session: Info) => Info) {
|
||||
const result = yield* Ref.modify(sessions, (state) => {
|
||||
const session = state.get(sessionId)
|
||||
if (!session) return [undefined, state] as const
|
||||
const next = fn(session)
|
||||
return [snapshot(next), new Map(state).set(sessionId, next)] as const
|
||||
})
|
||||
if (result) return result
|
||||
return yield* new ACPNextError.SessionNotFoundError({ sessionId })
|
||||
})
|
||||
|
||||
const remove = Effect.fn("ACPNext.Session.remove")(function* (sessionId: string) {
|
||||
return yield* Ref.modify(sessions, (state) => {
|
||||
const session = state.get(sessionId)
|
||||
if (!session) return [undefined, state] as const
|
||||
const next = new Map(state)
|
||||
next.delete(sessionId)
|
||||
return [snapshot(session), next] as const
|
||||
})
|
||||
})
|
||||
|
||||
const setModel: Interface["setModel"] = Effect.fn("ACPNext.Session.setModel")((sessionId, model) =>
|
||||
update(sessionId, (session) => ({ ...session, model })),
|
||||
)
|
||||
|
||||
const setVariant: Interface["setVariant"] = Effect.fn("ACPNext.Session.setVariant")((sessionId, variant) =>
|
||||
update(sessionId, (session) => ({ ...session, variant })),
|
||||
)
|
||||
|
||||
const setMode: Interface["setMode"] = Effect.fn("ACPNext.Session.setMode")((sessionId, modeId) =>
|
||||
update(sessionId, (session) => ({ ...session, modeId })),
|
||||
)
|
||||
|
||||
const recordPartMetadata: Interface["recordPartMetadata"] = Effect.fn("ACPNext.Session.recordPartMetadata")((
|
||||
input,
|
||||
) => {
|
||||
const metadata = {
|
||||
messageId: input.messageId,
|
||||
partId: input.partId,
|
||||
partType: input.partType,
|
||||
role: input.role,
|
||||
ignored: input.ignored,
|
||||
toolCallId: input.toolCallId,
|
||||
metadata: input.metadata,
|
||||
}
|
||||
return update(input.sessionId, (session) => ({
|
||||
...session,
|
||||
knownParts: new Map(session.knownParts).set(partMetadataKey(input), metadata),
|
||||
})).pipe(Effect.as(metadata))
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
create: store,
|
||||
load: store,
|
||||
list: Effect.fn("ACPNext.Session.list")(function* (cwd?: string) {
|
||||
return [...(yield* Ref.get(sessions)).values()]
|
||||
.filter((session) => !cwd || session.cwd === cwd)
|
||||
.map(snapshot)
|
||||
.toSorted((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
}),
|
||||
get,
|
||||
tryGet,
|
||||
remove,
|
||||
setModel,
|
||||
getModel: Effect.fn("ACPNext.Session.getModel")(function* (sessionId) {
|
||||
return (yield* get(sessionId)).model
|
||||
}),
|
||||
setVariant,
|
||||
getVariant: Effect.fn("ACPNext.Session.getVariant")(function* (sessionId) {
|
||||
return (yield* get(sessionId)).variant
|
||||
}),
|
||||
setMode,
|
||||
getMode: Effect.fn("ACPNext.Session.getMode")(function* (sessionId) {
|
||||
return (yield* get(sessionId)).modeId
|
||||
}),
|
||||
recordPartMetadata,
|
||||
getPartMetadata: Effect.fn("ACPNext.Session.getPartMetadata")(function* (input) {
|
||||
return (yield* get(input.sessionId)).knownParts.get(partMetadataKey(input))
|
||||
}),
|
||||
tryGetPartMetadata: Effect.fn("ACPNext.Session.tryGetPartMetadata")(function* (input) {
|
||||
return (yield* tryGet(input.sessionId))?.knownParts.get(partMetadataKey(input))
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
function makeSession(input: StoreInput): Info {
|
||||
return {
|
||||
id: input.id,
|
||||
cwd: input.cwd,
|
||||
mcpServers: [...(input.mcpServers ?? [])],
|
||||
createdAt: input.createdAt ? new Date(input.createdAt) : new Date(),
|
||||
model: input.model,
|
||||
variant: input.variant,
|
||||
modeId: input.modeId,
|
||||
knownParts: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
function snapshot(session: Info): Info {
|
||||
return {
|
||||
...session,
|
||||
mcpServers: [...session.mcpServers],
|
||||
createdAt: new Date(session.createdAt),
|
||||
knownParts: new Map(session.knownParts),
|
||||
}
|
||||
}
|
||||
|
||||
function partMetadataKey(input: { messageId: string; partId: string }) {
|
||||
return `${input.messageId}:${input.partId}`
|
||||
}
|
||||
|
||||
export * as ACPNextSession from "./session"
|
||||
@@ -1,174 +0,0 @@
|
||||
# ACP (Agent Client Protocol) Implementation
|
||||
|
||||
This directory contains a clean, protocol-compliant implementation of the [Agent Client Protocol](https://agentclientprotocol.com/) for opencode.
|
||||
|
||||
## Architecture
|
||||
|
||||
The implementation follows a clean separation of concerns:
|
||||
|
||||
### Core Components
|
||||
|
||||
- **`agent.ts`** - Implements the `Agent` interface from `@agentclientprotocol/sdk`
|
||||
- Handles initialization and capability negotiation
|
||||
- Manages session lifecycle (`session/new`, `session/load`)
|
||||
- Processes prompts and returns responses
|
||||
- Properly implements ACP protocol v1
|
||||
|
||||
- **`client.ts`** - Implements the `Client` interface for client-side capabilities
|
||||
- File operations (`readTextFile`, `writeTextFile`)
|
||||
- Permission requests (auto-approves for now)
|
||||
- Terminal support (stub implementation)
|
||||
|
||||
- **`session.ts`** - Session state management
|
||||
- Creates and tracks ACP sessions
|
||||
- Maps ACP sessions to internal opencode sessions
|
||||
- Maintains working directory context
|
||||
- Handles MCP server configurations
|
||||
|
||||
- **`server.ts`** - ACP server startup and lifecycle
|
||||
- Sets up JSON-RPC over stdio using the official library
|
||||
- Manages graceful shutdown on SIGTERM/SIGINT
|
||||
- Provides Instance context for the agent
|
||||
|
||||
- **`types.ts`** - Type definitions for internal use
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line
|
||||
|
||||
```bash
|
||||
# Start the ACP server in the current directory
|
||||
opencode acp
|
||||
|
||||
# Start in a specific directory
|
||||
opencode acp --cwd /path/to/project
|
||||
```
|
||||
|
||||
### Question Tool Opt-In
|
||||
|
||||
ACP excludes `QuestionTool` by default.
|
||||
|
||||
```bash
|
||||
OPENCODE_ENABLE_QUESTION_TOOL=1 opencode acp
|
||||
```
|
||||
|
||||
Enable this only for ACP clients that support interactive question prompts.
|
||||
|
||||
### Programmatic
|
||||
|
||||
```typescript
|
||||
import { ACPServer } from "./acp/server"
|
||||
|
||||
await ACPServer.start()
|
||||
```
|
||||
|
||||
### Integration with Zed
|
||||
|
||||
Add to your Zed configuration (`~/.config/zed/settings.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_servers": {
|
||||
"OpenCode": {
|
||||
"command": "opencode",
|
||||
"args": ["acp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol Compliance
|
||||
|
||||
This implementation follows the ACP specification v1:
|
||||
|
||||
✅ **Initialization**
|
||||
|
||||
- Proper `initialize` request/response with protocol version negotiation
|
||||
- Capability advertisement (`agentCapabilities`)
|
||||
- Authentication support (stub)
|
||||
|
||||
✅ **Session Management**
|
||||
|
||||
- `session/new` - Create new conversation sessions
|
||||
- `session/load` - Resume existing sessions (basic support)
|
||||
- Working directory context (`cwd`)
|
||||
- MCP server configuration support
|
||||
|
||||
✅ **Prompting**
|
||||
|
||||
- `session/prompt` - Process user messages
|
||||
- Content block handling (text, resources)
|
||||
- Response with stop reasons
|
||||
|
||||
✅ **Client Capabilities**
|
||||
|
||||
- File read/write operations
|
||||
- Permission requests
|
||||
- Terminal support (stub for future)
|
||||
|
||||
## Current Limitations
|
||||
|
||||
### Not Yet Implemented
|
||||
|
||||
1. **Streaming Responses** - Currently returns complete responses instead of streaming via `session/update` notifications
|
||||
2. **Tool Call Reporting** - Doesn't report tool execution progress
|
||||
3. **Session Modes** - No mode switching support yet
|
||||
4. **Authentication** - No actual auth implementation
|
||||
5. **Terminal Support** - Placeholder only
|
||||
6. **Session Persistence** - `session/load` doesn't restore actual conversation history
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
- **Real-time Streaming**: Implement `session/update` notifications for progressive responses
|
||||
- **Tool Call Visibility**: Report tool executions as they happen
|
||||
- **Session Persistence**: Save and restore full conversation history
|
||||
- **Mode Support**: Implement different operational modes (ask, code, etc.)
|
||||
- **Enhanced Permissions**: More sophisticated permission handling
|
||||
- **Terminal Integration**: Full terminal support via opencode's bash tool
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run ACP tests
|
||||
bun test test/acp.test.ts
|
||||
|
||||
# Test manually with stdio
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}' | opencode acp
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Why the Official Library?
|
||||
|
||||
We use `@agentclientprotocol/sdk` instead of implementing JSON-RPC ourselves because:
|
||||
|
||||
- Ensures protocol compliance
|
||||
- Handles edge cases and future protocol versions
|
||||
- Reduces maintenance burden
|
||||
- Works with other ACP clients automatically
|
||||
|
||||
### Clean Architecture
|
||||
|
||||
Each component has a single responsibility:
|
||||
|
||||
- **Agent** = Protocol interface
|
||||
- **Client** = Client-side operations
|
||||
- **Session** = State management
|
||||
- **Server** = Lifecycle and I/O
|
||||
|
||||
This makes the codebase maintainable and testable.
|
||||
|
||||
### Mapping to OpenCode
|
||||
|
||||
ACP sessions map cleanly to opencode's internal session model:
|
||||
|
||||
- ACP `session/new` → creates internal Session
|
||||
- ACP `session/prompt` → uses SessionPrompt.prompt()
|
||||
- Working directory context preserved per-session
|
||||
- Tool execution uses existing ToolRegistry
|
||||
|
||||
## References
|
||||
|
||||
- [ACP Specification](https://agentclientprotocol.com/)
|
||||
- [TypeScript Library](https://github.com/agentclientprotocol/typescript-sdk)
|
||||
- [Protocol Examples](https://github.com/agentclientprotocol/typescript-sdk/tree/main/src/examples)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ import { InstanceStore } from "@/project/instance-store"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Context, Effect, Layer, SynchronizedRef } from "effect"
|
||||
import type * as ACPNextError from "./error"
|
||||
import type * as ACPError from "./error"
|
||||
|
||||
export type ModelOption = {
|
||||
readonly providerID: ProviderID
|
||||
@@ -39,18 +39,18 @@ export type Snapshot = {
|
||||
}
|
||||
|
||||
export interface LoaderInterface {
|
||||
readonly load: (directory: string) => Effect.Effect<Snapshot, ACPNextError.Error>
|
||||
readonly load: (directory: string) => Effect.Effect<Snapshot, ACPError.Error>
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (directory: string) => Effect.Effect<Snapshot, ACPNextError.Error>
|
||||
readonly refresh: (directory: string) => Effect.Effect<Snapshot, ACPNextError.Error>
|
||||
readonly get: (directory: string) => Effect.Effect<Snapshot, ACPError.Error>
|
||||
readonly refresh: (directory: string) => Effect.Effect<Snapshot, ACPError.Error>
|
||||
readonly variants: (snapshot: Snapshot, model: DefaultModel) => ModelVariants | undefined
|
||||
}
|
||||
|
||||
export class Loader extends Context.Service<Loader, LoaderInterface>()("@opencode/ACPNextDirectoryLoader") {}
|
||||
export class Loader extends Context.Service<Loader, LoaderInterface>()("@opencode/ACPDirectoryLoader") {}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ACPNextDirectory") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ACPDirectory") {}
|
||||
|
||||
export const modelKey = (model: DefaultModel) => `${model.providerID}/${model.modelID}`
|
||||
|
||||
@@ -110,7 +110,7 @@ export const loaderLayer = Layer.effect(
|
||||
const command = yield* Command.Service
|
||||
|
||||
return Loader.of({
|
||||
load: Effect.fn("ACPNextDirectoryLoader.load")(function* (directory) {
|
||||
load: Effect.fn("ACPDirectoryLoader.load")(function* (directory) {
|
||||
const ctx = yield* store.load({ directory })
|
||||
return yield* Effect.gen(function* () {
|
||||
const providers = yield* provider.list()
|
||||
@@ -142,7 +142,7 @@ export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const loader = yield* Loader
|
||||
const snapshots = yield* SynchronizedRef.make(new Map<string, Effect.Effect<Snapshot, ACPNextError.Error>>())
|
||||
const snapshots = yield* SynchronizedRef.make(new Map<string, Effect.Effect<Snapshot, ACPError.Error>>())
|
||||
|
||||
const cached = Effect.fnUntraced(function* (directory: string) {
|
||||
return yield* SynchronizedRef.modifyEffect(
|
||||
@@ -166,11 +166,11 @@ export const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const get = Effect.fn("ACPNextDirectory.get")(function* (directory: string) {
|
||||
const get = Effect.fn("ACPDirectory.get")(function* (directory: string) {
|
||||
return yield* yield* cached(directory)
|
||||
})
|
||||
|
||||
const refresh = Effect.fn("ACPNextDirectory.refresh")(function* (directory: string) {
|
||||
const refresh = Effect.fn("ACPDirectory.refresh")(function* (directory: string) {
|
||||
return yield* SynchronizedRef.modifyEffect(
|
||||
snapshots,
|
||||
Effect.fnUntraced(function* (items) {
|
||||
@@ -2,51 +2,51 @@ import { RequestError } from "@agentclientprotocol/sdk"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class SessionNotFoundError extends Schema.TaggedErrorClass<SessionNotFoundError>()(
|
||||
"ACPNextSessionNotFoundError",
|
||||
"ACPSessionNotFoundError",
|
||||
{
|
||||
sessionId: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class InvalidConfigOptionError extends Schema.TaggedErrorClass<InvalidConfigOptionError>()(
|
||||
"ACPNextInvalidConfigOptionError",
|
||||
"ACPInvalidConfigOptionError",
|
||||
{
|
||||
configId: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class InvalidModelError extends Schema.TaggedErrorClass<InvalidModelError>()("ACPNextInvalidModelError", {
|
||||
export class InvalidModelError extends Schema.TaggedErrorClass<InvalidModelError>()("ACPInvalidModelError", {
|
||||
modelId: Schema.String,
|
||||
providerId: Schema.optional(Schema.String),
|
||||
}) {}
|
||||
|
||||
export class InvalidEffortError extends Schema.TaggedErrorClass<InvalidEffortError>()("ACPNextInvalidEffortError", {
|
||||
export class InvalidEffortError extends Schema.TaggedErrorClass<InvalidEffortError>()("ACPInvalidEffortError", {
|
||||
effort: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class InvalidModeError extends Schema.TaggedErrorClass<InvalidModeError>()("ACPNextInvalidModeError", {
|
||||
export class InvalidModeError extends Schema.TaggedErrorClass<InvalidModeError>()("ACPInvalidModeError", {
|
||||
mode: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class AuthRequiredError extends Schema.TaggedErrorClass<AuthRequiredError>()("ACPNextAuthRequiredError", {
|
||||
export class AuthRequiredError extends Schema.TaggedErrorClass<AuthRequiredError>()("ACPAuthRequiredError", {
|
||||
providerId: Schema.optional(Schema.String),
|
||||
}) {}
|
||||
|
||||
export class UnknownAuthMethodError extends Schema.TaggedErrorClass<UnknownAuthMethodError>()(
|
||||
"ACPNextUnknownAuthMethodError",
|
||||
"ACPUnknownAuthMethodError",
|
||||
{
|
||||
methodId: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class UnsupportedOperationError extends Schema.TaggedErrorClass<UnsupportedOperationError>()(
|
||||
"ACPNextUnsupportedOperationError",
|
||||
"ACPUnsupportedOperationError",
|
||||
{
|
||||
method: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class ServiceFailureError extends Schema.TaggedErrorClass<ServiceFailureError>()("ACPNextServiceFailureError", {
|
||||
export class ServiceFailureError extends Schema.TaggedErrorClass<ServiceFailureError>()("ACPServiceFailureError", {
|
||||
safeMessage: Schema.String,
|
||||
service: Schema.optional(Schema.String),
|
||||
}) {}
|
||||
@@ -64,26 +64,26 @@ export type Error =
|
||||
|
||||
export function toRequestError(error: Error) {
|
||||
switch (error._tag) {
|
||||
case "ACPNextSessionNotFoundError":
|
||||
case "ACPSessionNotFoundError":
|
||||
return RequestError.invalidParams({ sessionId: error.sessionId }, `session not found: ${error.sessionId}`)
|
||||
case "ACPNextInvalidConfigOptionError":
|
||||
case "ACPInvalidConfigOptionError":
|
||||
return RequestError.invalidParams({ configId: error.configId }, `unknown config option: ${error.configId}`)
|
||||
case "ACPNextInvalidModelError":
|
||||
case "ACPInvalidModelError":
|
||||
return RequestError.invalidParams(
|
||||
{ providerId: error.providerId, modelId: error.modelId },
|
||||
`model not found: ${error.modelId}`,
|
||||
)
|
||||
case "ACPNextInvalidEffortError":
|
||||
case "ACPInvalidEffortError":
|
||||
return RequestError.invalidParams({ effort: error.effort }, `effort not found: ${error.effort}`)
|
||||
case "ACPNextInvalidModeError":
|
||||
case "ACPInvalidModeError":
|
||||
return RequestError.invalidParams({ mode: error.mode }, `mode not found: ${error.mode}`)
|
||||
case "ACPNextAuthRequiredError":
|
||||
case "ACPAuthRequiredError":
|
||||
return RequestError.authRequired({ providerId: error.providerId }, "provider authentication required")
|
||||
case "ACPNextUnknownAuthMethodError":
|
||||
case "ACPUnknownAuthMethodError":
|
||||
return RequestError.invalidParams({ methodId: error.methodId }, `unknown auth method: ${error.methodId}`)
|
||||
case "ACPNextUnsupportedOperationError":
|
||||
case "ACPUnsupportedOperationError":
|
||||
return RequestError.methodNotFound(error.method)
|
||||
case "ACPNextServiceFailureError":
|
||||
case "ACPServiceFailureError":
|
||||
return RequestError.internalError({ service: error.service }, error.safeMessage)
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import type {
|
||||
ToolPart,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { Effect } from "effect"
|
||||
import { ACPNextSession } from "./session"
|
||||
import { ACPNextPermission } from "./permission"
|
||||
import { ACPSession } from "./session"
|
||||
import { ACPPermission } from "./permission"
|
||||
import {
|
||||
duplicateRunningToolUpdate,
|
||||
errorToolUpdate,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
completedToolUpdate,
|
||||
} from "./tool"
|
||||
|
||||
const log = Log.create({ service: "acp-next-event" })
|
||||
const log = Log.create({ service: "acp-event" })
|
||||
|
||||
type Connection = Pick<AgentSideConnection, "sessionUpdate"> &
|
||||
Partial<Pick<AgentSideConnection, "requestPermission" | "writeTextFile">>
|
||||
@@ -32,7 +32,7 @@ type GlobalEventStream = {
|
||||
stream: AsyncIterable<GlobalEventEnvelope>
|
||||
}
|
||||
|
||||
export function start(input: { sdk: OpencodeClient; connection: Connection; session: ACPNextSession.Interface }) {
|
||||
export function start(input: { sdk: OpencodeClient; connection: Connection; session: ACPSession.Interface }) {
|
||||
const subscription = new Subscription(input)
|
||||
subscription.start()
|
||||
return subscription
|
||||
@@ -42,17 +42,17 @@ export class Subscription {
|
||||
private readonly abort = new AbortController()
|
||||
private readonly shellSnapshots = new Map<string, string>()
|
||||
private readonly toolStarts = new Set<string>()
|
||||
private readonly permission: ACPNextPermission.Handler
|
||||
private readonly permission: ACPPermission.Handler
|
||||
private started = false
|
||||
|
||||
constructor(
|
||||
private readonly input: {
|
||||
sdk: OpencodeClient
|
||||
connection: Connection
|
||||
session: ACPNextSession.Interface
|
||||
session: ACPSession.Interface
|
||||
},
|
||||
) {
|
||||
this.permission = new ACPNextPermission.Handler(input)
|
||||
this.permission = new ACPPermission.Handler(input)
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -316,4 +316,4 @@ export class Subscription {
|
||||
}
|
||||
}
|
||||
|
||||
export * as ACPNextEvent from "./event"
|
||||
export * as ACPEvent from "./event"
|
||||
@@ -3,11 +3,11 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||
import type { Event, OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { applyPatch } from "diff"
|
||||
import { exists, readText } from "@/util/filesystem"
|
||||
import type { ACPNextSession } from "./session"
|
||||
import type { ACPSession } from "./session"
|
||||
import { toLocations, toToolKind, type ToolInput } from "./tool"
|
||||
import { Effect } from "effect"
|
||||
|
||||
const log = Log.create({ service: "acp-next-permission" })
|
||||
const log = Log.create({ service: "acp-permission" })
|
||||
|
||||
type PermissionEvent = Extract<Event, { type: "permission.asked" }>
|
||||
type Reply = "once" | "always" | "reject"
|
||||
@@ -26,7 +26,7 @@ export class Handler {
|
||||
private readonly input: {
|
||||
sdk: OpencodeClient
|
||||
connection: Connection
|
||||
session: ACPNextSession.Interface
|
||||
session: ACPSession.Interface
|
||||
},
|
||||
) {}
|
||||
|
||||
@@ -142,4 +142,4 @@ function stringValue(value: unknown) {
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
export * as ACPNextPermission from "./permission"
|
||||
export * as ACPPermission from "./permission"
|
||||
@@ -39,4 +39,4 @@ function write(name: string, durationMs: number, fields?: Record<string, string
|
||||
console.error(`[acp-profile] ${name} ${Math.round(durationMs)}ms${extra ? ` ${extra}` : ""}`)
|
||||
}
|
||||
|
||||
export * as ACPNextProfile from "./profile"
|
||||
export * as ACPProfile from "./profile"
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { AppRuntime, type AppServices } from "@/effect/app-runtime"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { InstanceRuntime } from "@/project/instance-runtime"
|
||||
import { Effect } from "effect"
|
||||
|
||||
// Global ACP Effect re-entry: no project InstanceRef is provided.
|
||||
export const runGlobal = AppRuntime.runPromise
|
||||
|
||||
// Directory-scoped ACP Effect re-entry: load the project instance and provide InstanceRef.
|
||||
export async function runDirectory<A, E>(input: { directory: string; effect: Effect.Effect<A, E, AppServices> }) {
|
||||
const ctx = await InstanceRuntime.load({ directory: input.directory })
|
||||
return AppRuntime.runPromise(input.effect.pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
}
|
||||
|
||||
export const defaultAgentInfo = (directory: string) =>
|
||||
runDirectory({
|
||||
directory,
|
||||
effect: Agent.Service.use((svc) => svc.defaultInfo()),
|
||||
})
|
||||
|
||||
export * as ACPRuntime from "./runtime"
|
||||
@@ -33,22 +33,22 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import type { Message, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
|
||||
import { Context, Effect, Layer, ManagedRuntime } from "effect"
|
||||
import * as ACPNextError from "./error"
|
||||
import * as ACPError from "./error"
|
||||
import { buildConfigOptions, parseModelSelection } from "./config-option"
|
||||
import { promptContentToParts } from "./content"
|
||||
import { Directory } from "./directory"
|
||||
import { ACPNextEvent } from "./event"
|
||||
import { ACPNextSession } from "./session"
|
||||
import { ACPEvent } from "./event"
|
||||
import { ACPSession } from "./session"
|
||||
import { UsageService } from "./usage"
|
||||
import { ACPNextProfile } from "./profile"
|
||||
import { ACPProfile } from "./profile"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import type { Command } from "@/command"
|
||||
|
||||
export const AuthMethodID = "opencode-login"
|
||||
const log = Log.create({ service: "acp-next-service" })
|
||||
const log = Log.create({ service: "acp-service" })
|
||||
|
||||
export type Error = ACPNextError.Error
|
||||
export type Error = ACPError.Error
|
||||
type ServiceConnection = Pick<AgentSideConnection, "sessionUpdate"> &
|
||||
Partial<Pick<AgentSideConnection, "requestPermission" | "writeTextFile">>
|
||||
|
||||
@@ -70,26 +70,26 @@ export type Interface = {
|
||||
readonly cancel: (input: CancelNotification) => Effect.Effect<void, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ACPNext/Service") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ACP/Service") {}
|
||||
|
||||
export function make(input: {
|
||||
sdk: OpencodeClient
|
||||
connection?: ServiceConnection
|
||||
directory?: Directory.Interface
|
||||
session?: ACPNextSession.Interface
|
||||
session?: ACPSession.Interface
|
||||
usage?: UsageService.Interface
|
||||
eventSubscription?: (subscription: ACPNextEvent.Subscription) => void
|
||||
eventSubscription?: (subscription: ACPEvent.Subscription) => void
|
||||
}): Interface {
|
||||
const session = input.session ?? makeSessionService()
|
||||
const directoryService = input.directory ?? makeDirectoryService(input.sdk)
|
||||
const registeredMcp = new Map<string, Set<string>>()
|
||||
const sessionSnapshots = new Map<string, Directory.Snapshot>()
|
||||
const events = input.connection
|
||||
? ACPNextEvent.start({ sdk: input.sdk, connection: input.connection, session })
|
||||
? ACPEvent.start({ sdk: input.sdk, connection: input.connection, session })
|
||||
: undefined
|
||||
if (events) input.eventSubscription?.(events)
|
||||
|
||||
const initialize = Effect.fn("ACPNext.initialize")(function* (params: InitializeRequest) {
|
||||
const initialize = Effect.fn("ACP.initialize")(function* (params: InitializeRequest) {
|
||||
const started = performance.now()
|
||||
const authMethod: AuthMethod = {
|
||||
description: "Run `opencode auth login` in the terminal",
|
||||
@@ -132,25 +132,25 @@ export function make(input: {
|
||||
version: InstallationVersion,
|
||||
},
|
||||
}
|
||||
ACPNextProfile.duration("acp.initialize", started)
|
||||
ACPProfile.duration("acp.initialize", started)
|
||||
return response
|
||||
})
|
||||
|
||||
const authenticate = Effect.fn("ACPNext.authenticate")(function* (params: AuthenticateRequest) {
|
||||
const authenticate = Effect.fn("ACP.authenticate")(function* (params: AuthenticateRequest) {
|
||||
if (params.methodId !== AuthMethodID) {
|
||||
return yield* new ACPNextError.UnknownAuthMethodError({ methodId: params.methodId })
|
||||
return yield* new ACPError.UnknownAuthMethodError({ methodId: params.methodId })
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const directorySnapshot = Effect.fn("ACPNext.directorySnapshot")(function* (cwd: string) {
|
||||
const directorySnapshot = Effect.fn("ACP.directorySnapshot")(function* (cwd: string) {
|
||||
const started = performance.now()
|
||||
const snapshot = yield* directoryService.get(cwd)
|
||||
ACPNextProfile.duration("acp.directory.snapshot", started)
|
||||
ACPProfile.duration("acp.directory.snapshot", started)
|
||||
return snapshot
|
||||
})
|
||||
|
||||
const configSnapshot = Effect.fn("ACPNext.configSnapshot")(function* (state: ACPNextSession.Info) {
|
||||
const configSnapshot = Effect.fn("ACP.configSnapshot")(function* (state: ACPSession.Info) {
|
||||
const snapshot = sessionSnapshots.get(state.id)
|
||||
if (snapshot) return snapshot
|
||||
const loaded = yield* directorySnapshot(state.cwd)
|
||||
@@ -158,7 +158,7 @@ export function make(input: {
|
||||
return loaded
|
||||
})
|
||||
|
||||
const newSession = Effect.fn("ACPNext.newSession")(function* (params: NewSessionRequest) {
|
||||
const newSession = Effect.fn("ACP.newSession")(function* (params: NewSessionRequest) {
|
||||
const started = performance.now()
|
||||
const snapshot = yield* directorySnapshot(params.cwd)
|
||||
const selected = selectDefaultModel(snapshot)
|
||||
@@ -202,11 +202,11 @@ export function make(input: {
|
||||
modeId: state.modeId,
|
||||
}),
|
||||
}
|
||||
ACPNextProfile.duration("acp.newSession", started)
|
||||
ACPProfile.duration("acp.newSession", started)
|
||||
return response
|
||||
})
|
||||
|
||||
const loadSession = Effect.fn("ACPNext.loadSession")(function* (params: LoadSessionRequest) {
|
||||
const loadSession = Effect.fn("ACP.loadSession")(function* (params: LoadSessionRequest) {
|
||||
const snapshot = yield* directorySnapshot(params.cwd)
|
||||
yield* request(
|
||||
() => input.sdk.session.get({ directory: params.cwd, sessionID: params.sessionId }, { throwOnError: true }),
|
||||
@@ -245,7 +245,7 @@ export function make(input: {
|
||||
}
|
||||
})
|
||||
|
||||
const listSessions = Effect.fn("ACPNext.listSessions")(function* (params: ListSessionsRequest) {
|
||||
const listSessions = Effect.fn("ACP.listSessions")(function* (params: ListSessionsRequest) {
|
||||
const cursor = params.cursor ? Number(params.cursor) : undefined
|
||||
const limit = 100
|
||||
const sessions = yield* request(
|
||||
@@ -291,7 +291,7 @@ export function make(input: {
|
||||
}
|
||||
})
|
||||
|
||||
const resumeSession = Effect.fn("ACPNext.resumeSession")(function* (params: ResumeSessionRequest) {
|
||||
const resumeSession = Effect.fn("ACP.resumeSession")(function* (params: ResumeSessionRequest) {
|
||||
const snapshot = yield* directorySnapshot(params.cwd)
|
||||
yield* request(
|
||||
() => input.sdk.session.get({ directory: params.cwd, sessionID: params.sessionId }, { throwOnError: true }),
|
||||
@@ -330,7 +330,7 @@ export function make(input: {
|
||||
}
|
||||
})
|
||||
|
||||
const closeSession = Effect.fn("ACPNext.closeSession")(function* (params: CloseSessionRequest) {
|
||||
const closeSession = Effect.fn("ACP.closeSession")(function* (params: CloseSessionRequest) {
|
||||
const removed = yield* session.remove(params.sessionId)
|
||||
registeredMcp.delete(params.sessionId)
|
||||
sessionSnapshots.delete(params.sessionId)
|
||||
@@ -349,7 +349,7 @@ export function make(input: {
|
||||
return {}
|
||||
})
|
||||
|
||||
const forkSession = Effect.fn("ACPNext.forkSession")(function* (params: ForkSessionRequest) {
|
||||
const forkSession = Effect.fn("ACP.forkSession")(function* (params: ForkSessionRequest) {
|
||||
const snapshot = yield* directorySnapshot(params.cwd)
|
||||
const forked = yield* request(
|
||||
() =>
|
||||
@@ -393,13 +393,13 @@ export function make(input: {
|
||||
}
|
||||
})
|
||||
|
||||
const setSessionConfigOption = Effect.fn("ACPNext.setSessionConfigOption")(function* (
|
||||
const setSessionConfigOption = Effect.fn("ACP.setSessionConfigOption")(function* (
|
||||
params: SetSessionConfigOptionRequest,
|
||||
) {
|
||||
const current = yield* session.get(params.sessionId)
|
||||
const snapshot = yield* configSnapshot(current)
|
||||
if (typeof params.value !== "string") {
|
||||
return yield* new ACPNextError.InvalidConfigOptionError({ configId: params.configId })
|
||||
return yield* new ACPError.InvalidConfigOptionError({ configId: params.configId })
|
||||
}
|
||||
|
||||
if (params.configId === "model") {
|
||||
@@ -421,7 +421,7 @@ export function make(input: {
|
||||
const model = current.model ?? selectDefaultModel(snapshot)
|
||||
const variants = Directory.variants(snapshot, model)
|
||||
if (!variants || !Object.keys(variants).includes(params.value)) {
|
||||
return yield* new ACPNextError.InvalidEffortError({ effort: params.value })
|
||||
return yield* new ACPError.InvalidEffortError({ effort: params.value })
|
||||
}
|
||||
const state = yield* session.setVariant(params.sessionId, params.value)
|
||||
return {
|
||||
@@ -435,7 +435,7 @@ export function make(input: {
|
||||
|
||||
if (params.configId === "mode") {
|
||||
if (!snapshot.availableModes.some((mode) => mode.id === params.value)) {
|
||||
return yield* new ACPNextError.InvalidModeError({ mode: params.value })
|
||||
return yield* new ACPError.InvalidModeError({ mode: params.value })
|
||||
}
|
||||
const state = yield* session.setMode(params.sessionId, params.value)
|
||||
return {
|
||||
@@ -447,20 +447,20 @@ export function make(input: {
|
||||
}
|
||||
}
|
||||
|
||||
return yield* new ACPNextError.InvalidConfigOptionError({ configId: params.configId })
|
||||
return yield* new ACPError.InvalidConfigOptionError({ configId: params.configId })
|
||||
})
|
||||
|
||||
const setSessionMode = Effect.fn("ACPNext.setSessionMode")(function* (params: SetSessionModeRequest) {
|
||||
const setSessionMode = Effect.fn("ACP.setSessionMode")(function* (params: SetSessionModeRequest) {
|
||||
const current = yield* session.get(params.sessionId)
|
||||
const snapshot = yield* configSnapshot(current)
|
||||
if (!snapshot.availableModes.some((mode) => mode.id === params.modeId)) {
|
||||
return yield* new ACPNextError.InvalidModeError({ mode: params.modeId })
|
||||
return yield* new ACPError.InvalidModeError({ mode: params.modeId })
|
||||
}
|
||||
yield* session.setMode(params.sessionId, params.modeId)
|
||||
return {}
|
||||
})
|
||||
|
||||
const setSessionModel = Effect.fn("ACPNext.setSessionModel")(function* (params: SetSessionModelRequest) {
|
||||
const setSessionModel = Effect.fn("ACP.setSessionModel")(function* (params: SetSessionModelRequest) {
|
||||
const current = yield* session.get(params.sessionId)
|
||||
const snapshot = yield* configSnapshot(current)
|
||||
const selected = yield* parseSelectedModel(snapshot, params.modelId)
|
||||
@@ -487,7 +487,7 @@ export function make(input: {
|
||||
setSessionConfigOption,
|
||||
setSessionMode,
|
||||
setSessionModel,
|
||||
prompt: Effect.fn("ACPNext.prompt")(function* (params: PromptRequest) {
|
||||
prompt: Effect.fn("ACP.prompt")(function* (params: PromptRequest) {
|
||||
const current = yield* session.get(params.sessionId)
|
||||
const snapshot = yield* directorySnapshot(current.cwd)
|
||||
const selected = current.model ?? selectDefaultModel(snapshot)
|
||||
@@ -563,15 +563,15 @@ export function make(input: {
|
||||
yield* sendUsageUpdate(input.usage, input.sdk, input.connection, current.id, current.cwd)
|
||||
return promptResponse(undefined, params.messageId)
|
||||
}),
|
||||
cancel: Effect.fn("ACPNext.cancel")(function* (_input: CancelNotification) {
|
||||
return yield* new ACPNextError.UnsupportedOperationError({ method: "session/cancel" })
|
||||
cancel: Effect.fn("ACP.cancel")(function* (_input: CancelNotification) {
|
||||
return yield* new ACPError.UnsupportedOperationError({ method: "session/cancel" })
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function makeSessionService() {
|
||||
return ManagedRuntime.make(ACPNextSession.defaultLayer).runSync(
|
||||
ACPNextSession.Service.use((service) => Effect.succeed(service)),
|
||||
return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
|
||||
ACPSession.Service.use((service) => Effect.succeed(service)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@ function makeDirectoryService(sdk: OpencodeClient) {
|
||||
|
||||
function makeUsageService(sdk: OpencodeClient) {
|
||||
const limits = new Map<string, Promise<number | undefined>>()
|
||||
const contextLimit: UsageService.Interface["contextLimit"] = Effect.fn("ACPNext.promptUsage.contextLimit")(
|
||||
const contextLimit: UsageService.Interface["contextLimit"] = Effect.fn("ACP.promptUsage.contextLimit")(
|
||||
function* (params) {
|
||||
const key = `${params.directory}\u0000${params.providerID}\u0000${params.modelID}`
|
||||
const current = limits.get(key)
|
||||
@@ -615,7 +615,7 @@ function makeUsageService(sdk: OpencodeClient) {
|
||||
},
|
||||
)
|
||||
|
||||
const sendUpdate: UsageService.Interface["sendUpdate"] = Effect.fn("ACPNext.promptUsage.sendUpdate")(
|
||||
const sendUpdate: UsageService.Interface["sendUpdate"] = Effect.fn("ACP.promptUsage.sendUpdate")(
|
||||
function* (params) {
|
||||
const messages = yield* request(
|
||||
() =>
|
||||
@@ -675,7 +675,7 @@ function makeUsageService(sdk: OpencodeClient) {
|
||||
})
|
||||
}
|
||||
|
||||
function replayMessages(subscription: ACPNextEvent.Subscription | undefined, messages: SessionMessageResponse[]) {
|
||||
function replayMessages(subscription: ACPEvent.Subscription | undefined, messages: SessionMessageResponse[]) {
|
||||
if (!subscription) return Effect.void
|
||||
return Effect.promise(async () => {
|
||||
for (const message of messages) {
|
||||
@@ -724,23 +724,23 @@ function request<T>(fn: () => Promise<T | SdkResponse<T>>, service?: string) {
|
||||
}
|
||||
|
||||
function profiledRequest<T>(name: string, fn: () => Promise<T | SdkResponse<T>>, service?: string) {
|
||||
return request(() => ACPNextProfile.measure(name, fn), service)
|
||||
return request(() => ACPProfile.measure(name, fn), service)
|
||||
}
|
||||
|
||||
async function loadDirectorySnapshot(sdk: OpencodeClient, directory: string) {
|
||||
return ACPNextProfile.measure("acp.directory.load", async () => {
|
||||
return ACPProfile.measure("acp.directory.load", async () => {
|
||||
const [providersResponse, agentsResponse, commandsResponse, skillsResponse, configResponse] = await Promise.all([
|
||||
ACPNextProfile.measure("acp.directory.provider.list", () =>
|
||||
ACPProfile.measure("acp.directory.provider.list", () =>
|
||||
sdk.config.providers({ directory }, { throwOnError: true }),
|
||||
),
|
||||
ACPNextProfile.measure("acp.directory.mode.defaultAgent.load", () =>
|
||||
ACPProfile.measure("acp.directory.mode.defaultAgent.load", () =>
|
||||
sdk.app.agents({ directory }, { throwOnError: true }),
|
||||
),
|
||||
ACPNextProfile.measure("acp.directory.command.list", () =>
|
||||
ACPProfile.measure("acp.directory.command.list", () =>
|
||||
sdk.command.list({ directory }, { throwOnError: true }),
|
||||
),
|
||||
ACPNextProfile.measure("acp.directory.skill.list", () => sdk.app.skills({ directory }, { throwOnError: true })),
|
||||
ACPNextProfile.measure("acp.directory.defaultModel.config", () =>
|
||||
ACPProfile.measure("acp.directory.skill.list", () => sdk.app.skills({ directory }, { throwOnError: true })),
|
||||
ACPProfile.measure("acp.directory.defaultModel.config", () =>
|
||||
sdk.config.get({ directory }, { throwOnError: true }).catch(() => undefined),
|
||||
),
|
||||
])
|
||||
@@ -754,7 +754,7 @@ async function loadDirectorySnapshot(sdk: OpencodeClient, directory: string) {
|
||||
>
|
||||
const defaultModelStarted = performance.now()
|
||||
const defaultModel = defaultModelFromConfig(configResponse?.data?.model, providers)
|
||||
ACPNextProfile.duration("acp.directory.defaultModel.resolve", defaultModelStarted, { configured: !!defaultModel })
|
||||
ACPProfile.duration("acp.directory.defaultModel.resolve", defaultModelStarted, { configured: !!defaultModel })
|
||||
const modes = agents
|
||||
.filter((agent) => agent.mode !== "subagent" && agent.hidden !== true)
|
||||
.map((agent) => ({
|
||||
@@ -872,14 +872,14 @@ function parseSelectedModel(snapshot: Directory.Snapshot, modelId: string) {
|
||||
const model = provider?.models[ModelID.make(selected.model.modelID)]
|
||||
if (!model) {
|
||||
return Effect.fail(
|
||||
new ACPNextError.InvalidModelError({
|
||||
new ACPError.InvalidModelError({
|
||||
providerId: selected.model.providerID,
|
||||
modelId,
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (selected.variant && !model.variants?.[selected.variant]) {
|
||||
return Effect.fail(new ACPNextError.InvalidEffortError({ effort: selected.variant }))
|
||||
return Effect.fail(new ACPError.InvalidEffortError({ effort: selected.variant }))
|
||||
}
|
||||
return Effect.succeed({
|
||||
model: {
|
||||
@@ -954,7 +954,7 @@ function registerMcpServers(
|
||||
).pipe(
|
||||
Effect.tap(() =>
|
||||
Effect.sync(() =>
|
||||
ACPNextProfile.duration("acp.mcp.register", started, {
|
||||
ACPProfile.duration("acp.mcp.register", started, {
|
||||
count: pending.size,
|
||||
}),
|
||||
),
|
||||
@@ -1020,20 +1020,20 @@ function isSdkResponse<T>(value: T | SdkResponse<T>): value is SdkResponse<T> {
|
||||
}
|
||||
|
||||
function fromUnknownError(error: unknown, service?: string): Error {
|
||||
if (isACPNextError(error)) return error
|
||||
if (isACPError(error)) return error
|
||||
if (isAuthRequired(error)) {
|
||||
return new ACPNextError.AuthRequiredError({ providerId: findProviderID(error) })
|
||||
return new ACPError.AuthRequiredError({ providerId: findProviderID(error) })
|
||||
}
|
||||
return new ACPNextError.ServiceFailureError({ safeMessage: "OpenCode service failure", service })
|
||||
return new ACPError.ServiceFailureError({ safeMessage: "OpenCode service failure", service })
|
||||
}
|
||||
|
||||
function isACPNextError(error: unknown): error is Error {
|
||||
function isACPError(error: unknown): error is Error {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"_tag" in error &&
|
||||
typeof error._tag === "string" &&
|
||||
error._tag.startsWith("ACPNext")
|
||||
error._tag.startsWith("ACP")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,122 +1,232 @@
|
||||
import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
|
||||
import type { ACPSessionState } from "./types"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import type { McpServer } from "@agentclientprotocol/sdk"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2"
|
||||
import { Context, Effect, Layer, Ref } from "effect"
|
||||
import type { ModelID, ProviderID } from "../provider/schema"
|
||||
import * as ACPError from "./error"
|
||||
|
||||
const log = Log.create({ service: "acp-session-manager" })
|
||||
export type SelectedModel = {
|
||||
providerID: ProviderID
|
||||
modelID: ModelID
|
||||
}
|
||||
|
||||
export class ACPSessionManager {
|
||||
private sessions = new Map<string, ACPSessionState>()
|
||||
private sdk: OpencodeClient
|
||||
export type KnownMessagePartMetadata = {
|
||||
messageId: string
|
||||
partId: string
|
||||
partType?: Part["type"]
|
||||
role?: Message["role"]
|
||||
ignored?: boolean
|
||||
toolCallId?: string
|
||||
metadata?: unknown
|
||||
}
|
||||
|
||||
constructor(sdk: OpencodeClient) {
|
||||
this.sdk = sdk
|
||||
}
|
||||
export type Info = {
|
||||
id: string
|
||||
cwd: string
|
||||
mcpServers: readonly McpServer[]
|
||||
createdAt: Date
|
||||
model?: SelectedModel
|
||||
variant?: string
|
||||
modeId?: string
|
||||
knownParts: ReadonlyMap<string, KnownMessagePartMetadata>
|
||||
}
|
||||
|
||||
tryGet(sessionId: string): ACPSessionState | undefined {
|
||||
return this.sessions.get(sessionId)
|
||||
}
|
||||
export type StoreInput = {
|
||||
id: string
|
||||
cwd: string
|
||||
mcpServers?: readonly McpServer[]
|
||||
createdAt?: Date
|
||||
model?: SelectedModel
|
||||
variant?: string
|
||||
modeId?: string
|
||||
}
|
||||
|
||||
async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise<ACPSessionState> {
|
||||
const session = await this.sdk.session
|
||||
.create(
|
||||
{
|
||||
directory: cwd,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.then((x) => x.data!)
|
||||
export type RecordPartMetadataInput = {
|
||||
sessionId: string
|
||||
messageId: string
|
||||
partId: string
|
||||
partType?: Part["type"]
|
||||
role?: Message["role"]
|
||||
ignored?: boolean
|
||||
toolCallId?: string
|
||||
metadata?: unknown
|
||||
}
|
||||
|
||||
const sessionId = session.id
|
||||
const resolvedModel = model
|
||||
export type PartMetadataLookupInput = {
|
||||
sessionId: string
|
||||
messageId: string
|
||||
partId: string
|
||||
}
|
||||
|
||||
const state: ACPSessionState = {
|
||||
id: sessionId,
|
||||
cwd,
|
||||
mcpServers,
|
||||
createdAt: new Date(),
|
||||
model: resolvedModel,
|
||||
}
|
||||
log.info("creating_session", { state })
|
||||
|
||||
this.sessions.set(sessionId, state)
|
||||
return state
|
||||
}
|
||||
|
||||
async load(
|
||||
export type Interface = {
|
||||
readonly create: (input: StoreInput) => Effect.Effect<Info>
|
||||
readonly load: (input: StoreInput) => Effect.Effect<Info>
|
||||
readonly list: (cwd?: string) => Effect.Effect<readonly Info[]>
|
||||
readonly get: (sessionId: string) => Effect.Effect<Info, ACPError.SessionNotFoundError>
|
||||
readonly tryGet: (sessionId: string) => Effect.Effect<Info | undefined>
|
||||
readonly remove: (sessionId: string) => Effect.Effect<Info | undefined>
|
||||
readonly setModel: (
|
||||
sessionId: string,
|
||||
cwd: string,
|
||||
mcpServers: McpServer[],
|
||||
model?: ACPSessionState["model"],
|
||||
): Promise<ACPSessionState> {
|
||||
const session = await this.sdk.session
|
||||
.get(
|
||||
{
|
||||
sessionID: sessionId,
|
||||
directory: cwd,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.then((x) => x.data!)
|
||||
model: SelectedModel | undefined,
|
||||
) => Effect.Effect<Info, ACPError.SessionNotFoundError>
|
||||
readonly getModel: (sessionId: string) => Effect.Effect<SelectedModel | undefined, ACPError.SessionNotFoundError>
|
||||
readonly setVariant: (
|
||||
sessionId: string,
|
||||
variant: string | undefined,
|
||||
) => Effect.Effect<Info, ACPError.SessionNotFoundError>
|
||||
readonly getVariant: (sessionId: string) => Effect.Effect<string | undefined, ACPError.SessionNotFoundError>
|
||||
readonly setMode: (
|
||||
sessionId: string,
|
||||
modeId: string | undefined,
|
||||
) => Effect.Effect<Info, ACPError.SessionNotFoundError>
|
||||
readonly getMode: (sessionId: string) => Effect.Effect<string | undefined, ACPError.SessionNotFoundError>
|
||||
readonly recordPartMetadata: (
|
||||
input: RecordPartMetadataInput,
|
||||
) => Effect.Effect<KnownMessagePartMetadata, ACPError.SessionNotFoundError>
|
||||
readonly getPartMetadata: (
|
||||
input: PartMetadataLookupInput,
|
||||
) => Effect.Effect<KnownMessagePartMetadata | undefined, ACPError.SessionNotFoundError>
|
||||
readonly tryGetPartMetadata: (input: PartMetadataLookupInput) => Effect.Effect<KnownMessagePartMetadata | undefined>
|
||||
}
|
||||
|
||||
const resolvedModel = model
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ACP/Session") {}
|
||||
|
||||
const state: ACPSessionState = {
|
||||
id: sessionId,
|
||||
cwd,
|
||||
mcpServers,
|
||||
createdAt: new Date(session.time.created),
|
||||
model: resolvedModel,
|
||||
}
|
||||
log.info("loading_session", { state })
|
||||
type State = Map<string, Info>
|
||||
|
||||
this.sessions.set(sessionId, state)
|
||||
return state
|
||||
}
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Ref.make<State>(new Map())
|
||||
|
||||
get(sessionId: string): ACPSessionState {
|
||||
const session = this.sessions.get(sessionId)
|
||||
if (!session) {
|
||||
log.error("session not found", { sessionId })
|
||||
throw RequestError.invalidParams(JSON.stringify({ error: `Session not found: ${sessionId}` }))
|
||||
}
|
||||
return session
|
||||
}
|
||||
const store = Effect.fn("ACP.Session.store")(function* (input: StoreInput) {
|
||||
const session = makeSession(input)
|
||||
yield* Ref.update(sessions, (state) => new Map(state).set(session.id, session))
|
||||
return snapshot(session)
|
||||
})
|
||||
|
||||
getModel(sessionId: string) {
|
||||
const session = this.get(sessionId)
|
||||
return session.model
|
||||
}
|
||||
const tryGet = Effect.fn("ACP.Session.tryGet")(function* (sessionId: string) {
|
||||
const session = (yield* Ref.get(sessions)).get(sessionId)
|
||||
if (!session) return
|
||||
return snapshot(session)
|
||||
})
|
||||
|
||||
setModel(sessionId: string, model: ACPSessionState["model"]) {
|
||||
const session = this.get(sessionId)
|
||||
session.model = model
|
||||
this.sessions.set(sessionId, session)
|
||||
return session
|
||||
}
|
||||
const get = Effect.fn("ACP.Session.get")(function* (sessionId: string) {
|
||||
const session = yield* tryGet(sessionId)
|
||||
if (session) return session
|
||||
return yield* new ACPError.SessionNotFoundError({ sessionId })
|
||||
})
|
||||
|
||||
getVariant(sessionId: string) {
|
||||
const session = this.get(sessionId)
|
||||
return session.variant
|
||||
}
|
||||
const update = Effect.fn("ACP.Session.update")(function* (sessionId: string, fn: (session: Info) => Info) {
|
||||
const result = yield* Ref.modify(sessions, (state) => {
|
||||
const session = state.get(sessionId)
|
||||
if (!session) return [undefined, state] as const
|
||||
const next = fn(session)
|
||||
return [snapshot(next), new Map(state).set(sessionId, next)] as const
|
||||
})
|
||||
if (result) return result
|
||||
return yield* new ACPError.SessionNotFoundError({ sessionId })
|
||||
})
|
||||
|
||||
setVariant(sessionId: string, variant?: string) {
|
||||
const session = this.get(sessionId)
|
||||
session.variant = variant
|
||||
this.sessions.set(sessionId, session)
|
||||
return session
|
||||
}
|
||||
const remove = Effect.fn("ACP.Session.remove")(function* (sessionId: string) {
|
||||
return yield* Ref.modify(sessions, (state) => {
|
||||
const session = state.get(sessionId)
|
||||
if (!session) return [undefined, state] as const
|
||||
const next = new Map(state)
|
||||
next.delete(sessionId)
|
||||
return [snapshot(session), next] as const
|
||||
})
|
||||
})
|
||||
|
||||
setMode(sessionId: string, modeId: string) {
|
||||
const session = this.get(sessionId)
|
||||
session.modeId = modeId
|
||||
this.sessions.set(sessionId, session)
|
||||
return session
|
||||
}
|
||||
const setModel: Interface["setModel"] = Effect.fn("ACP.Session.setModel")((sessionId, model) =>
|
||||
update(sessionId, (session) => ({ ...session, model })),
|
||||
)
|
||||
|
||||
remove(sessionId: string): ACPSessionState | undefined {
|
||||
const session = this.sessions.get(sessionId)
|
||||
this.sessions.delete(sessionId)
|
||||
return session
|
||||
const setVariant: Interface["setVariant"] = Effect.fn("ACP.Session.setVariant")((sessionId, variant) =>
|
||||
update(sessionId, (session) => ({ ...session, variant })),
|
||||
)
|
||||
|
||||
const setMode: Interface["setMode"] = Effect.fn("ACP.Session.setMode")((sessionId, modeId) =>
|
||||
update(sessionId, (session) => ({ ...session, modeId })),
|
||||
)
|
||||
|
||||
const recordPartMetadata: Interface["recordPartMetadata"] = Effect.fn("ACP.Session.recordPartMetadata")((
|
||||
input,
|
||||
) => {
|
||||
const metadata = {
|
||||
messageId: input.messageId,
|
||||
partId: input.partId,
|
||||
partType: input.partType,
|
||||
role: input.role,
|
||||
ignored: input.ignored,
|
||||
toolCallId: input.toolCallId,
|
||||
metadata: input.metadata,
|
||||
}
|
||||
return update(input.sessionId, (session) => ({
|
||||
...session,
|
||||
knownParts: new Map(session.knownParts).set(partMetadataKey(input), metadata),
|
||||
})).pipe(Effect.as(metadata))
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
create: store,
|
||||
load: store,
|
||||
list: Effect.fn("ACP.Session.list")(function* (cwd?: string) {
|
||||
return [...(yield* Ref.get(sessions)).values()]
|
||||
.filter((session) => !cwd || session.cwd === cwd)
|
||||
.map(snapshot)
|
||||
.toSorted((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
}),
|
||||
get,
|
||||
tryGet,
|
||||
remove,
|
||||
setModel,
|
||||
getModel: Effect.fn("ACP.Session.getModel")(function* (sessionId) {
|
||||
return (yield* get(sessionId)).model
|
||||
}),
|
||||
setVariant,
|
||||
getVariant: Effect.fn("ACP.Session.getVariant")(function* (sessionId) {
|
||||
return (yield* get(sessionId)).variant
|
||||
}),
|
||||
setMode,
|
||||
getMode: Effect.fn("ACP.Session.getMode")(function* (sessionId) {
|
||||
return (yield* get(sessionId)).modeId
|
||||
}),
|
||||
recordPartMetadata,
|
||||
getPartMetadata: Effect.fn("ACP.Session.getPartMetadata")(function* (input) {
|
||||
return (yield* get(input.sessionId)).knownParts.get(partMetadataKey(input))
|
||||
}),
|
||||
tryGetPartMetadata: Effect.fn("ACP.Session.tryGetPartMetadata")(function* (input) {
|
||||
return (yield* tryGet(input.sessionId))?.knownParts.get(partMetadataKey(input))
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
function makeSession(input: StoreInput): Info {
|
||||
return {
|
||||
id: input.id,
|
||||
cwd: input.cwd,
|
||||
mcpServers: [...(input.mcpServers ?? [])],
|
||||
createdAt: input.createdAt ? new Date(input.createdAt) : new Date(),
|
||||
model: input.model,
|
||||
variant: input.variant,
|
||||
modeId: input.modeId,
|
||||
knownParts: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
function snapshot(session: Info): Info {
|
||||
return {
|
||||
...session,
|
||||
mcpServers: [...session.mcpServers],
|
||||
createdAt: new Date(session.createdAt),
|
||||
knownParts: new Map(session.knownParts),
|
||||
}
|
||||
}
|
||||
|
||||
function partMetadataKey(input: { messageId: string; partId: string }) {
|
||||
return `${input.messageId}:${input.partId}`
|
||||
}
|
||||
|
||||
export * as ACPSession from "./session"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { McpServer } from "@agentclientprotocol/sdk"
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import type { ProviderID, ModelID } from "../provider/schema"
|
||||
|
||||
export interface ACPSessionState {
|
||||
id: string
|
||||
cwd: string
|
||||
mcpServers: McpServer[]
|
||||
createdAt: Date
|
||||
model?: {
|
||||
providerID: ProviderID
|
||||
modelID: ModelID
|
||||
}
|
||||
variant?: string
|
||||
modeId?: string
|
||||
}
|
||||
|
||||
export interface ACPConfig {
|
||||
sdk: OpencodeClient
|
||||
defaultModel?: {
|
||||
providerID: ProviderID
|
||||
modelID: ModelID
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Context, Effect, Layer, SynchronizedRef } from "effect"
|
||||
|
||||
const log = Log.create({ service: "acp-next-usage" })
|
||||
const log = Log.create({ service: "acp-usage" })
|
||||
|
||||
export type AssistantTokenCost = Pick<OpenCodeAssistantMessage, "cost" | "tokens">
|
||||
|
||||
@@ -60,14 +60,14 @@ export interface Interface {
|
||||
}
|
||||
|
||||
export class MessageLoader extends Context.Service<MessageLoader, MessageLoaderInterface>()(
|
||||
"@opencode/ACPNextUsageMessageLoader",
|
||||
"@opencode/ACPUsageMessageLoader",
|
||||
) {}
|
||||
|
||||
export class ContextLimitLoader extends Context.Service<ContextLimitLoader, ContextLimitLoaderInterface>()(
|
||||
"@opencode/ACPNextUsageContextLimitLoader",
|
||||
"@opencode/ACPUsageContextLimitLoader",
|
||||
) {}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ACPNextUsage") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ACPUsage") {}
|
||||
|
||||
export function messageLoaderFromSDK(sdk: SDK): MessageLoaderInterface {
|
||||
return MessageLoader.of({
|
||||
@@ -124,7 +124,7 @@ export const contextLimitLoaderLayer = Layer.effect(
|
||||
const provider = yield* Provider.Service
|
||||
|
||||
return ContextLimitLoader.of({
|
||||
providers: Effect.fn("ACPNextUsageContextLimitLoader.providers")(function* (directory) {
|
||||
providers: Effect.fn("ACPUsageContextLimitLoader.providers")(function* (directory) {
|
||||
const ctx = yield* store.load({ directory })
|
||||
return yield* Effect.gen(function* () {
|
||||
return yield* provider.list()
|
||||
@@ -168,7 +168,7 @@ export const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const contextLimit = Effect.fn("ACPNextUsage.contextLimit")(function* (input: {
|
||||
const contextLimit = Effect.fn("ACPUsage.contextLimit")(function* (input: {
|
||||
readonly directory: string
|
||||
readonly providerID: ProviderID
|
||||
readonly modelID: ModelID
|
||||
@@ -176,7 +176,7 @@ export const layer = Layer.effect(
|
||||
return yield* yield* cachedLimit(input)
|
||||
})
|
||||
|
||||
const sendUpdate = Effect.fn("ACPNextUsage.sendUpdate")(function* (input: {
|
||||
const sendUpdate = Effect.fn("ACPUsage.sendUpdate")(function* (input: {
|
||||
readonly connection: UsageConnection
|
||||
readonly sessionID: string
|
||||
readonly directory: string
|
||||
@@ -3,13 +3,11 @@ import { Effect } from "effect"
|
||||
import { effectCmd } from "../effect-cmd"
|
||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
||||
import { ACP } from "@/acp/agent"
|
||||
import { ACPNext } from "@/acp-next/agent"
|
||||
import { Server } from "@/server/server"
|
||||
import { ServerAuth } from "@/server/auth"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ACPNextProfile } from "@/acp-next/profile"
|
||||
import { ACPProfile } from "@/acp/profile"
|
||||
|
||||
const log = Log.create({ service: "acp-command" })
|
||||
|
||||
@@ -24,13 +22,10 @@ export const AcpCommand = effectCmd({
|
||||
})
|
||||
},
|
||||
handler: Effect.fn("Cli.acp")(function* (args) {
|
||||
ACPNextProfile.mark("cli.acp.handler")
|
||||
ACPProfile.mark("cli.acp.handler")
|
||||
process.env.OPENCODE_CLIENT = "acp"
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
const opts = yield* resolveNetworkOptions(args)
|
||||
const server = yield* Effect.promise(() =>
|
||||
ACPNextProfile.measure("cli.acp.server.listen", () => Server.listen(opts)),
|
||||
)
|
||||
const server = yield* Effect.promise(() => ACPProfile.measure("cli.acp.server.listen", () => Server.listen(opts)))
|
||||
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: `http://${server.hostname}:${server.port}`,
|
||||
@@ -61,11 +56,11 @@ export const AcpCommand = effectCmd({
|
||||
})
|
||||
|
||||
const stream = ndJsonStream(input, output)
|
||||
const agent = flags.acpNext ? ACPNext.init({ sdk }) : ACP.init({ sdk })
|
||||
const agent = ACP.init({ sdk })
|
||||
|
||||
new AgentSideConnection((conn) => {
|
||||
ACPNextProfile.mark("cli.acp.connection.create", { acpNext: flags.acpNext })
|
||||
return agent.create(conn, { sdk })
|
||||
ACPProfile.mark("cli.acp.connection.create")
|
||||
return agent.create(conn)
|
||||
}, stream)
|
||||
|
||||
log.info("setup connection")
|
||||
|
||||
@@ -50,7 +50,6 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
|
||||
experimentalEventSystem: enabledByExperimental("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"),
|
||||
experimentalWorkspaces: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),
|
||||
experimentalIconDiscovery: enabledByExperimental("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"),
|
||||
acpNext: bool("OPENCODE_ACP_NEXT"),
|
||||
outputTokenMax: positiveInteger("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"),
|
||||
bashDefaultTimeoutMs: positiveInteger("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"),
|
||||
experimentalNativeLlm: bool("OPENCODE_EXPERIMENTAL_NATIVE_LLM"),
|
||||
|
||||
Reference in New Issue
Block a user