feat(core): attach global native tools (#30832)
This commit is contained in:
parent
17ba5539e7
commit
64dc6d39ab
39 changed files with 647 additions and 71 deletions
|
|
@ -27,7 +27,8 @@ import { RepositoryCache } from "./repository-cache"
|
||||||
import { Pty } from "./pty"
|
import { Pty } from "./pty"
|
||||||
import { SkillV2 } from "./skill"
|
import { SkillV2 } from "./skill"
|
||||||
import { BuiltInTools } from "./tool/builtins"
|
import { BuiltInTools } from "./tool/builtins"
|
||||||
import { ToolRegistry } from "./tool-registry"
|
import { ToolRegistry } from "./tool/registry"
|
||||||
|
import { ApplicationTools } from "./tool/application-tools"
|
||||||
import { ToolOutputStore } from "./tool-output-store"
|
import { ToolOutputStore } from "./tool-output-store"
|
||||||
import { AppProcess } from "./process"
|
import { AppProcess } from "./process"
|
||||||
import { Ripgrep } from "./ripgrep"
|
import { Ripgrep } from "./ripgrep"
|
||||||
|
|
@ -108,5 +109,6 @@ export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("
|
||||||
LLMClient.layer.pipe(Layer.provide(RequestExecutor.defaultLayer)),
|
LLMClient.layer.pipe(Layer.provide(RequestExecutor.defaultLayer)),
|
||||||
FetchHttpClient.layer,
|
FetchHttpClient.layer,
|
||||||
ToolOutputStore.defaultCleanupLayer,
|
ToolOutputStore.defaultCleanupLayer,
|
||||||
|
ApplicationTools.layer,
|
||||||
],
|
],
|
||||||
}) {}
|
}) {}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export { Agent } from "./agent"
|
||||||
export { Model } from "./model"
|
export { Model } from "./model"
|
||||||
export { OpenCode } from "./opencode"
|
export { OpenCode } from "./opencode"
|
||||||
export { Session } from "./session"
|
export { Session } from "./session"
|
||||||
|
export { Tool } from "./tool"
|
||||||
export { Location } from "./location"
|
export { Location } from "./location"
|
||||||
export { Prompt } from "../session/prompt"
|
export { Prompt } from "../session/prompt"
|
||||||
export { AbsolutePath } from "../schema"
|
export { AbsolutePath } from "../schema"
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,13 @@ import { SessionV2 } from "../session"
|
||||||
import * as SessionExecutionLocal from "../session/execution/local"
|
import * as SessionExecutionLocal from "../session/execution/local"
|
||||||
import { SessionProjector } from "../session/projector"
|
import { SessionProjector } from "../session/projector"
|
||||||
import { SessionStore } from "../session/store"
|
import { SessionStore } from "../session/store"
|
||||||
|
import { ApplicationTools } from "../tool/application-tools"
|
||||||
import { Session } from "./session"
|
import { Session } from "./session"
|
||||||
|
import { Tool } from "./tool"
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly sessions: Session.Interface
|
readonly sessions: Session.Interface
|
||||||
|
readonly tools: Tool.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Intentional public native API for Effect applications embedding OpenCode. */
|
/** Intentional public native API for Effect applications embedding OpenCode. */
|
||||||
|
|
@ -28,13 +31,16 @@ const SessionsLayer = SessionV2.layer.pipe(
|
||||||
Layer.provide(ProjectV2.defaultLayer),
|
Layer.provide(ProjectV2.defaultLayer),
|
||||||
Layer.orDie,
|
Layer.orDie,
|
||||||
)
|
)
|
||||||
|
const ApplicationToolsLayer = ApplicationTools.layer
|
||||||
|
|
||||||
// TODO: Accept explicit storage so tests and embeddings can select disposable or application-owned persistence.
|
// TODO: Accept explicit storage so tests and embeddings can select disposable or application-owned persistence.
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const sessions = yield* SessionV2.Service
|
const sessions = yield* SessionV2.Service
|
||||||
|
const tools = yield* ApplicationTools.Service
|
||||||
return Service.of({
|
return Service.of({
|
||||||
|
tools: { attach: tools.attach },
|
||||||
sessions: {
|
sessions: {
|
||||||
create: (input) =>
|
create: (input) =>
|
||||||
sessions.create({
|
sessions.create({
|
||||||
|
|
@ -65,6 +71,6 @@ export const layer = Layer.effect(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.provide(SessionsLayer))
|
).pipe(Layer.provide(Layer.merge(ApplicationToolsLayer, SessionsLayer)))
|
||||||
|
|
||||||
// TODO: Add OpenCode.create(...) as the Promise facade over the same native API semantics.
|
// TODO: Add OpenCode.create(...) as the Promise facade over the same native API semantics.
|
||||||
|
|
|
||||||
19
packages/core/src/public/tool.ts
Normal file
19
packages/core/src/public/tool.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
export * as Tool from "./tool"
|
||||||
|
|
||||||
|
import { Effect, Scope } from "effect"
|
||||||
|
import type { NativeTool } from "../tool/native"
|
||||||
|
|
||||||
|
export { Failure, make } from "../tool/native"
|
||||||
|
export type { Any, Content, Context, Executable } from "../tool/native"
|
||||||
|
|
||||||
|
export interface Service {
|
||||||
|
/**
|
||||||
|
* Attach same-process tools to this OpenCode instance for the current Scope.
|
||||||
|
* Location tools with the same name take precedence where they are installed.
|
||||||
|
* Closing the Scope removes the tools immediately, so calls that have not
|
||||||
|
* started settling may fail because the tool is no longer available.
|
||||||
|
*/
|
||||||
|
readonly attach: (
|
||||||
|
tools: Readonly<Record<string, NativeTool.Any>>,
|
||||||
|
) => Effect.Effect<void, never, Scope.Scope>
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ import { SessionStore } from "../store"
|
||||||
import { Service, StepLimitExceededError } from "./index"
|
import { Service, StepLimitExceededError } from "./index"
|
||||||
import { createLLMEventPublisher } from "./publish-llm-event"
|
import { createLLMEventPublisher } from "./publish-llm-event"
|
||||||
import { toLLMMessages } from "./to-llm-message"
|
import { toLLMMessages } from "./to-llm-message"
|
||||||
import { ToolRegistry } from "../../tool-registry"
|
import { ToolRegistry } from "../../tool/registry"
|
||||||
import { SessionRunnerModel } from "./model"
|
import { SessionRunnerModel } from "./model"
|
||||||
import { Database } from "../../database/database"
|
import { Database } from "../../database/database"
|
||||||
import { SessionInput } from "../input"
|
import { SessionInput } from "../input"
|
||||||
|
|
|
||||||
139
packages/core/src/tool/AGENTS.md
Normal file
139
packages/core/src/tool/AGENTS.md
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
# Core Tool Architecture
|
||||||
|
|
||||||
|
This folder owns Core-native tool definition, contribution, effective lookup, and execution. Keep those concerns distinct even though `ToolRegistry` brings them together at runtime.
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Public Tool.make NativeTool value ApplicationTools Location built-ins Location ToolRegistry Session runner
|
||||||
|
│ │ │ │ │ │
|
||||||
|
├─ construct ─────────▶ │ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ├─ scoped attach ─────▶ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ ├─ scoped contributions ──▶ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ ├─ shared current entries ───────────────────────▶ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ ├─ effective definitions and settlement ──▶
|
||||||
|
│ │ │ │ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
There are three relevant representations:
|
||||||
|
|
||||||
|
- `native.ts` defines the plain Core-native executable value exposed publicly as `Tool.make(...)`. It combines an `@opencode-ai/llm` model-facing definition with a Session-aware handler.
|
||||||
|
- `application-tools.ts` stores process-scoped application contributions. It owns availability and scoped attachment, but it does not execute tools.
|
||||||
|
- `registry.ts` is the single execution registry. Each Location owns one registry, its built-in contributions, effective precedence, input/output validation, permissions, and settlement.
|
||||||
|
|
||||||
|
`ToolRegistry.Entry` is intentionally more powerful than the public native tool value. Internal Location tools may use Core-owned capabilities such as `assertPermission`; embedding applications receive only the narrow public execution context.
|
||||||
|
|
||||||
|
## Placement And Layers
|
||||||
|
|
||||||
|
- `ApplicationTools.Service` is process-scoped and must be shared by current and future Locations.
|
||||||
|
- `ToolRegistry.Service` is Location-scoped because built-in handlers close over Location services such as filesystem, permissions, and tool-output storage.
|
||||||
|
- `LocationServiceMap` constructs fresh Location services while receiving the shared `ApplicationTools.Service` as a dependency.
|
||||||
|
- `OpenCode.layer` exposes the same shared application-tool service through `opencode.tools.attach(...)`.
|
||||||
|
- `ToolRegistry.defaultLayer` creates isolated application-tool state. It is suitable for self-contained consumers and tests, but not when attachments must be shared with a separately constructed `LocationServiceMap`.
|
||||||
|
|
||||||
|
Do not make `ToolRegistry` process-global. Do not move Location resources into `ApplicationTools`. Do not construct independent `ApplicationTools.layer` instances when the caller expects one attachment to appear across Locations.
|
||||||
|
|
||||||
|
## Contribution And Precedence
|
||||||
|
|
||||||
|
Built-in Location tools contribute through `ToolRegistry.contribute(...)`. Application tools attach through `ApplicationTools.attach(...)`, exposed publicly as `opencode.tools.attach(...)`.
|
||||||
|
|
||||||
|
Both contribution mechanisms use `State` scoped transforms:
|
||||||
|
|
||||||
|
- Closing a contribution Scope rebuilds state without that contribution.
|
||||||
|
- A later same-name application attachment wins while active.
|
||||||
|
- Closing that later attachment reveals the earlier active application contribution.
|
||||||
|
- A Location tool always takes precedence over an application tool with the same name.
|
||||||
|
- Application attachment inputs are captured before registering the replayable transform; later caller mutation must not alter a contribution during an unrelated rebuild.
|
||||||
|
|
||||||
|
Do not introduce another application-specific tool type or registry. Plugins should contribute existing native tools or internal registry entries at the lifetime they actually own.
|
||||||
|
|
||||||
|
## Dynamic Removal Semantics
|
||||||
|
|
||||||
|
Definitions and settlement intentionally resolve the current effective tools independently. There is no provider-turn snapshot, attachment lease, or draining detach.
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Embedding App ApplicationTools Location ToolRegistry Session Runner
|
||||||
|
│ │ │ │
|
||||||
|
├─ attach({ opencord_run }) ──▶ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ◀─ definitions() ──────────────────┤
|
||||||
|
│ │ │ │
|
||||||
|
│ ◀─ entries() ────────────┤ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ├─ current effective definitions ──▶
|
||||||
|
│ │ │ │
|
||||||
|
├─ attachment Scope closes ───▶ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ◀─ settle(opencord_run) ───────────┤
|
||||||
|
│ │ │ │
|
||||||
|
│ ◀─ current lookup ───────┤ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ├─ Unknown tool ───────────────────▶
|
||||||
|
│ │ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
Consequences of this choice:
|
||||||
|
|
||||||
|
- Closing an attachment Scope revokes the tool immediately for calls that have not started settling.
|
||||||
|
- A call produced from an earlier advertised definition may fail as unknown.
|
||||||
|
- If a same-name replacement is currently active, a later call may execute that replacement.
|
||||||
|
- An execution that already resolved its entry continues with the handler it captured.
|
||||||
|
- Attachment Scope closure does not wait for already-started executions. Applications whose handlers depend on scoped resources must coordinate graceful shutdown themselves.
|
||||||
|
|
||||||
|
These are deliberate simplifications. Do not add snapshots, semaphores, leases, or deferred finalizers without a concrete requirement for stronger consistency or graceful draining.
|
||||||
|
|
||||||
|
## File Roles
|
||||||
|
|
||||||
|
```txt
|
||||||
|
tool/
|
||||||
|
native.ts plain public/Core-native executable tool value
|
||||||
|
application-tools.ts process-scoped State-backed application contributions
|
||||||
|
registry.ts Location-scoped effective lookup, validation, and execution
|
||||||
|
builtins.ts shipped Location tool layer composition
|
||||||
|
read.ts, bash.ts, ... individual Location-scoped built-in contributions
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep model/provider-neutral tool schemas and output projection in `@opencode-ai/llm`. Keep Session identity, permissions, Location precedence, and settlement in Core.
|
||||||
|
|
||||||
|
## Future Directions
|
||||||
|
|
||||||
|
Tool availability may eventually gain a real third scope, such as Session-specific or plugin-owned contributions:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
╭─────────────────╮
|
||||||
|
│ Tool definition │
|
||||||
|
╰────────┬────────╯
|
||||||
|
╭────────────────────────────────────────╰╮─ ─ ─ ─ ─ ─ ─ ─ future ─ ─ ─ ─ ─ ─ ─ ─ ╮
|
||||||
|
│ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
╭───────────────────────╮ ╭────────────────────────╮ ╭───────────────────────╮
|
||||||
|
│ Process contributions │ │ Location contributions │ │ Session contributions │
|
||||||
|
╰───────────┬───────────╯ ╰────────────┬───────────╯ ╰───────────┬───────────╯
|
||||||
|
│ │ │
|
||||||
|
│ │
|
||||||
|
╰─────────────────────────────────────────◀─ ─ ─ ─ ─ ─ ─ ─ future ─ ─ ─ ─ ─ ─ ─ ─ ╯
|
||||||
|
╭──────────────────────╮
|
||||||
|
│ Effective resolution │
|
||||||
|
╭─────────╰───────────┬──────────╯────────────╮
|
||||||
|
│ │ │
|
||||||
|
▼ ▼
|
||||||
|
╭───────────────────────────────╮ ╭─────────────────────────╮
|
||||||
|
│ Advertise current definitions │ │ Execute current handler │
|
||||||
|
╰───────────────────────────────╯ ╰─────────────────────────╯
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer these directions only when a concrete use requires them:
|
||||||
|
|
||||||
|
- **Contextual availability:** Add Session/agent/plugin filtering at effective resolution. Keep tool definitions independent from where they are enabled.
|
||||||
|
- **Hierarchical overlays:** If a third contribution scope becomes real, consider one registry abstraction with process, Location, and Session overlays rather than adding another special registry service.
|
||||||
|
- **Plugin tools:** Reuse the existing native tool value for restricted handlers and `ToolRegistry.Entry` for trusted Core-owned capabilities. Choose process or Location contribution lifetime explicitly.
|
||||||
|
- **Stale-call rejection:** If executing a same-name replacement is unsafe, attach an identity/version to advertised definitions and reject stale calls without retaining removed handlers.
|
||||||
|
- **Pinned provider turns:** If exact advertisement-to-execution consistency becomes necessary, snapshot effective entries for one provider turn. This weakens immediate revocation.
|
||||||
|
- **Graceful plugin unload:** If attachment-owned resources must outlive started executions, add explicit execution draining. Keep this separate from whether new calls can discover the tool.
|
||||||
|
- **Cluster placement:** `ApplicationTools` is process-global, not cluster-global. Cluster-wide contribution and execution ownership require a separate durable design.
|
||||||
|
|
||||||
|
When choosing stronger semantics, state which property matters: immediate revocation, stale-call rejection, exact handler pinning, or graceful resource draining. They are different guarantees and should not arrive as one bundled lifecycle mechanism.
|
||||||
51
packages/core/src/tool/application-tools.ts
Normal file
51
packages/core/src/tool/application-tools.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
export * as ApplicationTools from "./application-tools"
|
||||||
|
|
||||||
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
|
import { castDraft, enableMapSet } from "immer"
|
||||||
|
import { State } from "../state"
|
||||||
|
import { NativeTool } from "./native"
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
readonly entries: Map<string, NativeTool.Any>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Editor = {
|
||||||
|
readonly set: (name: string, tool: NativeTool.Any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly attach: (tools: Readonly<Record<string, NativeTool.Any>>) => Effect.Effect<void, never, Scope.Scope>
|
||||||
|
readonly entries: () => ReadonlyMap<string, NativeTool.Any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/ApplicationTools") {}
|
||||||
|
|
||||||
|
enableMapSet()
|
||||||
|
|
||||||
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const state = State.create<Data, Editor>({
|
||||||
|
initial: () => ({ entries: new Map() }),
|
||||||
|
editor: (draft) => ({
|
||||||
|
set: (name, tool) => {
|
||||||
|
draft.entries.set(
|
||||||
|
name,
|
||||||
|
castDraft(tool) as typeof draft.entries extends Map<string, infer Value> ? Value : never,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
return Service.of({
|
||||||
|
attach: Effect.fn("ApplicationTools.attach")(function* (tools) {
|
||||||
|
const entries = Object.entries(tools)
|
||||||
|
const transform = yield* state.transform()
|
||||||
|
yield* transform((editor) => {
|
||||||
|
for (const [name, tool] of entries) editor.set(name, tool)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
entries: () => state.get().entries,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
@ -6,7 +6,7 @@ import { FileMutation } from "../file-mutation"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
import { LocationMutation } from "../location-mutation"
|
import { LocationMutation } from "../location-mutation"
|
||||||
import { Patch } from "../patch"
|
import { Patch } from "../patch"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "apply_patch"
|
export const name = "apply_patch"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { LocationMutation } from "../location-mutation"
|
||||||
import { AppProcess } from "../process"
|
import { AppProcess } from "../process"
|
||||||
import { PositiveInt } from "../schema"
|
import { PositiveInt } from "../schema"
|
||||||
import { ToolOutputStore } from "../tool-output-store"
|
import { ToolOutputStore } from "../tool-output-store"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "bash"
|
export const name = "bash"
|
||||||
export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000
|
export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { Cause, Effect, Layer, Schema } from "effect"
|
||||||
import { FileMutation } from "../file-mutation"
|
import { FileMutation } from "../file-mutation"
|
||||||
import { FSUtil } from "../fs-util"
|
import { FSUtil } from "../fs-util"
|
||||||
import { LocationMutation } from "../location-mutation"
|
import { LocationMutation } from "../location-mutation"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "edit"
|
export const name = "edit"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Tool, ToolFailure, toolText } from "@opencode-ai/llm"
|
||||||
import { Cause, Effect, Layer, Schema } from "effect"
|
import { Cause, Effect, Layer, Schema } from "effect"
|
||||||
import { FileSystem } from "../filesystem"
|
import { FileSystem } from "../filesystem"
|
||||||
import { LocationSearch } from "../location-search"
|
import { LocationSearch } from "../location-search"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "glob"
|
export const name = "glob"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Cause, Effect, Layer, Schema } from "effect"
|
||||||
import { FileSystem } from "../filesystem"
|
import { FileSystem } from "../filesystem"
|
||||||
import { LocationSearch } from "../location-search"
|
import { LocationSearch } from "../location-search"
|
||||||
import { Ripgrep } from "../ripgrep"
|
import { Ripgrep } from "../ripgrep"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "grep"
|
export const name = "grep"
|
||||||
|
|
||||||
|
|
|
||||||
73
packages/core/src/tool/native.ts
Normal file
73
packages/core/src/tool/native.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
export * as NativeTool from "./native"
|
||||||
|
|
||||||
|
import { Tool, ToolFailure } from "@opencode-ai/llm"
|
||||||
|
import { Effect, Schema } from "effect"
|
||||||
|
import type { SessionSchema } from "../session/schema"
|
||||||
|
|
||||||
|
export interface Context {
|
||||||
|
readonly sessionID: SessionSchema.ID
|
||||||
|
readonly id: string
|
||||||
|
readonly name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SchemaType<A> = Schema.Codec<A, any, never, never>
|
||||||
|
|
||||||
|
export interface Executable<Parameters extends SchemaType<any>, Success extends SchemaType<any>> {
|
||||||
|
readonly definition: Tool.Tool<Parameters, Success>
|
||||||
|
readonly execute: (
|
||||||
|
parameters: Schema.Schema.Type<Parameters>,
|
||||||
|
context: Context,
|
||||||
|
) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Any = Executable<any, any>
|
||||||
|
|
||||||
|
export const Failure = ToolFailure
|
||||||
|
export type Failure = ToolFailure
|
||||||
|
|
||||||
|
export type Content =
|
||||||
|
| { readonly type: "text"; readonly text: string }
|
||||||
|
| {
|
||||||
|
readonly type: "file"
|
||||||
|
readonly data: string
|
||||||
|
readonly mime: string
|
||||||
|
readonly name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function make<Parameters extends SchemaType<any>, Success extends SchemaType<any>>(config: {
|
||||||
|
readonly description: string
|
||||||
|
readonly parameters: Parameters
|
||||||
|
readonly success: Success
|
||||||
|
readonly execute: (
|
||||||
|
parameters: Schema.Schema.Type<Parameters>,
|
||||||
|
context: Context,
|
||||||
|
) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
|
||||||
|
readonly toModelOutput?: (input: {
|
||||||
|
readonly callID: string
|
||||||
|
readonly parameters: Schema.Schema.Type<Parameters>
|
||||||
|
readonly output: Success["Encoded"]
|
||||||
|
}) => ReadonlyArray<Content>
|
||||||
|
}): Executable<Parameters, Success> {
|
||||||
|
const toModelOutput = config.toModelOutput
|
||||||
|
return {
|
||||||
|
definition: Tool.make({
|
||||||
|
description: config.description,
|
||||||
|
parameters: config.parameters,
|
||||||
|
success: config.success,
|
||||||
|
toModelOutput: toModelOutput
|
||||||
|
? (input) =>
|
||||||
|
toModelOutput(input).map((content) =>
|
||||||
|
content.type === "text"
|
||||||
|
? content
|
||||||
|
: {
|
||||||
|
type: "file",
|
||||||
|
source: { type: "data", data: content.data },
|
||||||
|
mime: content.mime,
|
||||||
|
name: content.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
execute: config.execute,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ export * as QuestionTool from "./question"
|
||||||
import { Tool, toolText } from "@opencode-ai/llm"
|
import { Tool, toolText } from "@opencode-ai/llm"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Layer, Schema } from "effect"
|
||||||
import { QuestionV2 } from "../question"
|
import { QuestionV2 } from "../question"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "question"
|
export const name = "question"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { FileSystem } from "../filesystem"
|
||||||
import { NonNegativeInt, PositiveInt } from "../schema"
|
import { NonNegativeInt, PositiveInt } from "../schema"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { ToolOutputStore } from "../tool-output-store"
|
import { ToolOutputStore } from "../tool-output-store"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "read"
|
export const name = "read"
|
||||||
const LocationInput = Schema.Struct({
|
const LocationInput = Schema.Struct({
|
||||||
|
|
@ -93,7 +93,7 @@ export const layer = Layer.effectDiscard(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
export const locationLayer = layer.pipe(
|
export const locationLayer = layer.pipe(
|
||||||
Layer.provideMerge(ToolRegistry.layer),
|
Layer.provideMerge(ToolRegistry.defaultLayer),
|
||||||
Layer.provideMerge(FileSystem.locationLayer),
|
Layer.provideMerge(FileSystem.locationLayer),
|
||||||
Layer.provideMerge(PermissionV2.locationLayer),
|
Layer.provideMerge(PermissionV2.locationLayer),
|
||||||
Layer.provideMerge(ToolOutputStore.defaultLayer),
|
Layer.provideMerge(ToolOutputStore.defaultLayer),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export * as ToolRegistry from "./tool-registry"
|
export * as ToolRegistry from "./registry"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Tool,
|
Tool,
|
||||||
|
|
@ -13,10 +13,11 @@ import {
|
||||||
} from "@opencode-ai/llm"
|
} from "@opencode-ai/llm"
|
||||||
import { Context, Effect, Layer, Schema, Scope } from "effect"
|
import { Context, Effect, Layer, Schema, Scope } from "effect"
|
||||||
import { castDraft, enableMapSet } from "immer"
|
import { castDraft, enableMapSet } from "immer"
|
||||||
import { PermissionV2 } from "./permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { State } from "./state"
|
import { State } from "../state"
|
||||||
import { SessionSchema } from "./session/schema"
|
import { SessionSchema } from "../session/schema"
|
||||||
import type { SessionV2 } from "./session"
|
import type { SessionV2 } from "../session"
|
||||||
|
import { ApplicationTools } from "./application-tools"
|
||||||
|
|
||||||
export type ExecuteInput = {
|
export type ExecuteInput = {
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
|
|
@ -86,6 +87,7 @@ export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const permission = yield* PermissionV2.Service
|
const permission = yield* PermissionV2.Service
|
||||||
|
const applications = yield* ApplicationTools.Service
|
||||||
const state = State.create<Data, Editor>({
|
const state = State.create<Data, Editor>({
|
||||||
initial: () => ({ entries: new Map() }),
|
initial: () => ({ entries: new Map() }),
|
||||||
editor: (draft) => ({
|
editor: (draft) => ({
|
||||||
|
|
@ -104,19 +106,36 @@ export const layer = Layer.effect(
|
||||||
})
|
})
|
||||||
|
|
||||||
const definitions = Effect.fn("ToolRegistry.definitions")(function* () {
|
const definitions = Effect.fn("ToolRegistry.definitions")(function* () {
|
||||||
return Tool.toDefinitions(
|
const tools = new Map(Array.from(state.get().entries, ([name, entry]) => [name, entry.tool] as const))
|
||||||
Object.fromEntries(Array.from(state.get().entries, ([name, entry]) => [name, entry.tool])),
|
// Location tools own their names. Application tools fill otherwise-unclaimed names.
|
||||||
)
|
for (const [name, tool] of applications.entries()) {
|
||||||
|
if (!tools.has(name)) tools.set(name, tool.definition)
|
||||||
|
}
|
||||||
|
return Tool.toDefinitions(Object.fromEntries(tools))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const entry = (name: string): Entry | undefined => {
|
||||||
|
const local = state.get().entries.get(name)
|
||||||
|
if (local !== undefined) return local
|
||||||
|
const tool = applications.entries().get(name)
|
||||||
|
if (tool === undefined) return
|
||||||
|
return {
|
||||||
|
tool: tool.definition,
|
||||||
|
execute: ({ parameters, sessionID, call }) =>
|
||||||
|
tool.execute(parameters, { sessionID, id: call.id, name: call.name }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const invocation = (input: ExecuteInput): Invocation => ({
|
const invocation = (input: ExecuteInput): Invocation => ({
|
||||||
...input,
|
...input,
|
||||||
// Source needs the durable owning assistant message ID, which the registry does not receive yet.
|
// Source needs the durable owning assistant message ID, which the registry does not receive yet.
|
||||||
assertPermission: (request) => permission.assert({ ...request, sessionID: input.sessionID }),
|
assertPermission: (request) => permission.assert({ ...request, sessionID: input.sessionID }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const settle = Effect.fn("ToolRegistry.settle")(function* (input: ExecuteInput) {
|
const settleEntry = Effect.fn("ToolRegistry.settleEntry")(function* (
|
||||||
const entry = state.get().entries.get(input.call.name)
|
entry: Entry | undefined,
|
||||||
|
input: ExecuteInput,
|
||||||
|
) {
|
||||||
if (!entry) return { result: { type: "error" as const, value: `Unknown tool: ${input.call.name}` } }
|
if (!entry) return { result: { type: "error" as const, value: `Unknown tool: ${input.call.name}` } }
|
||||||
if (!entry.execute && !entry.tool.execute)
|
if (!entry.execute && !entry.tool.execute)
|
||||||
return { result: { type: "error" as const, value: `Tool has no execute handler: ${input.call.name}` } }
|
return { result: { type: "error" as const, value: `Tool has no execute handler: ${input.call.name}` } }
|
||||||
|
|
@ -155,6 +174,7 @@ export const layer = Layer.effect(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const settle = Effect.fn("ToolRegistry.settle")((input: ExecuteInput) => settleEntry(entry(input.call.name), input))
|
||||||
const execute = Effect.fn("ToolRegistry.execute")(function* (input: ExecuteInput) {
|
const execute = Effect.fn("ToolRegistry.execute")(function* (input: ExecuteInput) {
|
||||||
return (yield* settle(input)).result
|
return (yield* settle(input)).result
|
||||||
})
|
})
|
||||||
|
|
@ -171,3 +191,5 @@ export const layer = Layer.effect(
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer.pipe(Layer.provide(ApplicationTools.layer))
|
||||||
|
|
@ -8,7 +8,7 @@ import { FSUtil } from "../fs-util"
|
||||||
import { PluginBoot } from "../plugin/boot"
|
import { PluginBoot } from "../plugin/boot"
|
||||||
import { SkillV2 } from "../skill"
|
import { SkillV2 } from "../skill"
|
||||||
import { ToolOutputStore } from "../tool-output-store"
|
import { ToolOutputStore } from "../tool-output-store"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "skill"
|
export const name = "skill"
|
||||||
const FILE_LIMIT = 10
|
const FILE_LIMIT = 10
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ export * as TodoWriteTool from "./todowrite"
|
||||||
import { Tool, ToolFailure, toolText } from "@opencode-ai/llm"
|
import { Tool, ToolFailure, toolText } from "@opencode-ai/llm"
|
||||||
import { Cause, Effect, Layer, Schema } from "effect"
|
import { Cause, Effect, Layer, Schema } from "effect"
|
||||||
import { SessionTodo } from "../session/todo"
|
import { SessionTodo } from "../session/todo"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "todowrite"
|
export const name = "todowrite"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab
|
||||||
import { Parser } from "htmlparser2"
|
import { Parser } from "htmlparser2"
|
||||||
import TurndownService from "turndown"
|
import TurndownService from "turndown"
|
||||||
import { ToolOutputStore } from "../tool-output-store"
|
import { ToolOutputStore } from "../tool-output-store"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "webfetch"
|
export const name = "webfetch"
|
||||||
export const MAX_RESPONSE_BYTES = 5 * 1024 * 1024
|
export const MAX_RESPONSE_BYTES = 5 * 1024 * 1024
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { truthy } from "../flag/flag"
|
||||||
import { InstallationVersion } from "../installation/version"
|
import { InstallationVersion } from "../installation/version"
|
||||||
import { PositiveInt } from "../schema"
|
import { PositiveInt } from "../schema"
|
||||||
import { ToolOutputStore } from "../tool-output-store"
|
import { ToolOutputStore } from "../tool-output-store"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
import { checksum } from "../util/encode"
|
import { checksum } from "../util/encode"
|
||||||
|
|
||||||
export const name = "websearch"
|
export const name = "websearch"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { Tool, ToolFailure, toolText } from "@opencode-ai/llm"
|
||||||
import { Cause, Effect, Layer, Schema } from "effect"
|
import { Cause, Effect, Layer, Schema } from "effect"
|
||||||
import { FileMutation } from "../file-mutation"
|
import { FileMutation } from "../file-mutation"
|
||||||
import { LocationMutation } from "../location-mutation"
|
import { LocationMutation } from "../location-mutation"
|
||||||
import { ToolRegistry } from "../tool-registry"
|
import { ToolRegistry } from "./registry"
|
||||||
|
|
||||||
export const name = "write"
|
export const name = "write"
|
||||||
|
|
||||||
|
|
|
||||||
184
packages/core/test/application-tools.test.ts
Normal file
184
packages/core/test/application-tools.test.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Tool } from "@opencode-ai/core/public"
|
||||||
|
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
||||||
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
|
import { Effect, Exit, Layer, Schema, Scope } from "effect"
|
||||||
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
|
const permission = Layer.mock(PermissionV2.Service, {
|
||||||
|
assert: () => Effect.void,
|
||||||
|
})
|
||||||
|
const applications = ApplicationTools.layer
|
||||||
|
const registry = ToolRegistry.layer.pipe(Layer.provide(permission), Layer.provide(applications))
|
||||||
|
const it = testEffect(Layer.mergeAll(applications, registry))
|
||||||
|
|
||||||
|
const sessionID = SessionV2.ID.make("ses_application_tool")
|
||||||
|
const contextual = (contexts: Tool.Context[]) =>
|
||||||
|
Tool.make({
|
||||||
|
description: "Read application context",
|
||||||
|
parameters: Schema.Struct({ query: Schema.String }),
|
||||||
|
success: Schema.Struct({ answer: Schema.String }),
|
||||||
|
execute: ({ query }, context) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
contexts.push(context)
|
||||||
|
return { answer: query.toUpperCase() }
|
||||||
|
}),
|
||||||
|
toModelOutput: ({ output }) => [
|
||||||
|
{ type: "text", text: output.answer },
|
||||||
|
{ type: "file", data: "aGVsbG8=", mime: "image/png", name: "result.png" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("ApplicationTools", () => {
|
||||||
|
it.effect("advertises and executes a scoped application tool with Session context", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const applications = yield* ApplicationTools.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const contexts: Tool.Context[] = []
|
||||||
|
|
||||||
|
yield* applications.attach({ application_context: contextual(contexts) })
|
||||||
|
|
||||||
|
expect(yield* registry.definitions()).toMatchObject([
|
||||||
|
{ name: "application_context", description: "Read application context" },
|
||||||
|
])
|
||||||
|
expect(
|
||||||
|
yield* registry.settle({
|
||||||
|
sessionID,
|
||||||
|
call: { type: "tool-call", id: "call-context", name: "application_context", input: { query: "hello" } },
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
result: {
|
||||||
|
type: "content",
|
||||||
|
value: [
|
||||||
|
{ type: "text", text: "HELLO" },
|
||||||
|
{ type: "media", mediaType: "image/png", data: "aGVsbG8=", filename: "result.png" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
structured: { answer: "HELLO" },
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "HELLO" },
|
||||||
|
{ type: "file", source: { type: "data", data: "aGVsbG8=" }, mime: "image/png", name: "result.png" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(contexts).toEqual([{ sessionID, id: "call-context", name: "application_context" }])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("removes an application tool when its attachment scope closes", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const applications = yield* ApplicationTools.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const scope = yield* Scope.make()
|
||||||
|
|
||||||
|
yield* applications.attach({ temporary: contextual([]) }).pipe(Scope.provide(scope))
|
||||||
|
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["temporary"])
|
||||||
|
|
||||||
|
yield* Scope.close(scope, Exit.void)
|
||||||
|
expect(yield* registry.definitions()).toEqual([])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("removes a tool before settling a call produced from an earlier definition", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const applications = yield* ApplicationTools.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const attachmentScope = yield* Scope.make()
|
||||||
|
yield* applications.attach({ contextual: contextual([]) }).pipe(Scope.provide(attachmentScope))
|
||||||
|
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["contextual"])
|
||||||
|
|
||||||
|
yield* Scope.close(attachmentScope, Exit.void)
|
||||||
|
expect(
|
||||||
|
yield* registry.settle({
|
||||||
|
sessionID,
|
||||||
|
call: { type: "tool-call", id: "call-removed", name: "contextual", input: { query: "hello" } },
|
||||||
|
}),
|
||||||
|
).toEqual({ result: { type: "error", value: "Unknown tool: contextual" } })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("does not leak an attachment into an already closed scope", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const applications = yield* ApplicationTools.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const scope = yield* Scope.make()
|
||||||
|
yield* Scope.close(scope, Exit.void)
|
||||||
|
|
||||||
|
yield* applications.attach({ closed: contextual([]) }).pipe(Scope.provide(scope))
|
||||||
|
|
||||||
|
expect(yield* registry.definitions()).toEqual([])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("captures the attached record before later State rebuilds", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const applications = yield* ApplicationTools.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const attached = { stable: contextual([]) }
|
||||||
|
yield* applications.attach(attached)
|
||||||
|
Object.assign(attached, { late: contextual([]) })
|
||||||
|
|
||||||
|
yield* Effect.scoped(applications.attach({ temporary: contextual([]) }))
|
||||||
|
|
||||||
|
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["stable"])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("settles with the current same-name application tool and restores earlier attachments", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const applications = yield* ApplicationTools.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const firstContexts: Tool.Context[] = []
|
||||||
|
const secondContexts: Tool.Context[] = []
|
||||||
|
const scope = yield* Scope.make()
|
||||||
|
yield* applications.attach({ contextual: contextual(firstContexts) })
|
||||||
|
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["contextual"])
|
||||||
|
yield* applications.attach({ contextual: contextual(secondContexts) }).pipe(Scope.provide(scope))
|
||||||
|
|
||||||
|
yield* registry.settle({
|
||||||
|
sessionID,
|
||||||
|
call: { type: "tool-call", id: "call-second", name: "contextual", input: { query: "second" } },
|
||||||
|
})
|
||||||
|
yield* Scope.close(scope, Exit.void)
|
||||||
|
yield* registry.settle({
|
||||||
|
sessionID,
|
||||||
|
call: { type: "tool-call", id: "call-first", name: "contextual", input: { query: "first" } },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(secondContexts).toEqual([{ sessionID, id: "call-second", name: "contextual" }])
|
||||||
|
expect(firstContexts).toEqual([{ sessionID, id: "call-first", name: "contextual" }])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("keeps the Location tool when an application tool has the same name", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const applications = yield* ApplicationTools.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const transform = yield* registry.transform()
|
||||||
|
const locationContexts: Tool.Context[] = []
|
||||||
|
const applicationContexts: Tool.Context[] = []
|
||||||
|
const location = contextual(locationContexts)
|
||||||
|
yield* transform((editor) =>
|
||||||
|
editor.set("shared", {
|
||||||
|
tool: location.definition,
|
||||||
|
execute: ({ parameters, sessionID, call }) =>
|
||||||
|
location.execute(parameters, { sessionID, id: call.id, name: call.name }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* applications.attach({ shared: contextual(applicationContexts) })
|
||||||
|
|
||||||
|
expect((yield* registry.definitions()).map((definition) => definition.name)).toEqual(["shared"])
|
||||||
|
expect(
|
||||||
|
yield* registry.settle({
|
||||||
|
sessionID,
|
||||||
|
call: { type: "tool-call", id: "call-shared", name: "shared", input: { query: "location" } },
|
||||||
|
}),
|
||||||
|
).toMatchObject({ result: { type: "content" } })
|
||||||
|
expect(locationContexts).toEqual([{ sessionID, id: "call-shared", name: "shared" }])
|
||||||
|
expect(applicationContexts).toEqual([])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer, Schema } from "effect"
|
||||||
|
import { Tool } from "@opencode-ai/core/public"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||||
|
|
@ -18,19 +19,24 @@ import { Npm } from "../src/npm"
|
||||||
import { Project } from "../src/project"
|
import { Project } from "../src/project"
|
||||||
import { ProjectReference } from "../src/project-reference"
|
import { ProjectReference } from "../src/project-reference"
|
||||||
import { LocationSearch } from "../src/location-search"
|
import { LocationSearch } from "../src/location-search"
|
||||||
import { ToolRegistry } from "../src/tool-registry"
|
import { ToolRegistry } from "../src/tool/registry"
|
||||||
|
import { ApplicationTools } from "../src/tool/application-tools"
|
||||||
|
|
||||||
|
const applicationTools = ApplicationTools.layer
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
LocationServiceMap.layer.pipe(
|
Layer.merge(
|
||||||
Layer.provide(
|
applicationTools,
|
||||||
Layer.mergeAll(
|
LocationServiceMap.layer.pipe(
|
||||||
Project.defaultLayer,
|
Layer.provide(
|
||||||
EventV2.defaultLayer,
|
Layer.mergeAll(
|
||||||
Auth.defaultLayer,
|
Project.defaultLayer,
|
||||||
Npm.defaultLayer,
|
EventV2.defaultLayer,
|
||||||
ModelsDev.defaultLayer,
|
Auth.defaultLayer,
|
||||||
FSUtil.defaultLayer,
|
Npm.defaultLayer,
|
||||||
Global.defaultLayer,
|
ModelsDev.defaultLayer,
|
||||||
|
FSUtil.defaultLayer,
|
||||||
|
Global.defaultLayer,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -44,6 +50,14 @@ describe("LocationServiceMap", () => {
|
||||||
).pipe(
|
).pipe(
|
||||||
Effect.flatMap(([blocked, allowed]) =>
|
Effect.flatMap(([blocked, allowed]) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
yield* (yield* ApplicationTools.Service).attach({
|
||||||
|
application_context: Tool.make({
|
||||||
|
description: "Read application context",
|
||||||
|
parameters: Schema.Struct({}),
|
||||||
|
success: Schema.Struct({ ok: Schema.Boolean }),
|
||||||
|
execute: () => Effect.succeed({ ok: true }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
yield* Effect.promise(() =>
|
yield* Effect.promise(() =>
|
||||||
fs.writeFile(
|
fs.writeFile(
|
||||||
path.join(blocked.path, "opencode.json"),
|
path.join(blocked.path, "opencode.json"),
|
||||||
|
|
@ -70,6 +84,7 @@ describe("LocationServiceMap", () => {
|
||||||
const blockedState = yield* update(blocked.path)
|
const blockedState = yield* update(blocked.path)
|
||||||
expect(blockedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(false)
|
expect(blockedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(false)
|
||||||
expect(blockedState.tools.map((tool) => tool.name).sort()).toEqual([
|
expect(blockedState.tools.map((tool) => tool.name).sort()).toEqual([
|
||||||
|
"application_context",
|
||||||
"apply_patch",
|
"apply_patch",
|
||||||
"bash",
|
"bash",
|
||||||
"edit",
|
"edit",
|
||||||
|
|
@ -86,6 +101,7 @@ describe("LocationServiceMap", () => {
|
||||||
const allowedState = yield* update(allowed.path)
|
const allowedState = yield* update(allowed.path)
|
||||||
expect(allowedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(true)
|
expect(allowedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(true)
|
||||||
expect(allowedState.tools.map((tool) => tool.name).sort()).toEqual([
|
expect(allowedState.tools.map((tool) => tool.name).sort()).toEqual([
|
||||||
|
"application_context",
|
||||||
"apply_patch",
|
"apply_patch",
|
||||||
"bash",
|
"bash",
|
||||||
"edit",
|
"edit",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { OpenCode, Session } from "@opencode-ai/core/public"
|
import { OpenCode, Session, Tool } from "@opencode-ai/core/public"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const it = testEffect(OpenCode.layer)
|
const it = testEffect(OpenCode.layer)
|
||||||
|
|
@ -10,6 +10,8 @@ describe("public native OpenCode API", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const opencode = yield* OpenCode.Service
|
const opencode = yield* OpenCode.Service
|
||||||
|
|
||||||
|
expect(Object.keys(opencode).sort()).toEqual(["sessions", "tools"])
|
||||||
|
|
||||||
expect(Object.keys(opencode.sessions).sort()).toEqual([
|
expect(Object.keys(opencode.sessions).sort()).toEqual([
|
||||||
"context",
|
"context",
|
||||||
"create",
|
"create",
|
||||||
|
|
@ -23,6 +25,14 @@ describe("public native OpenCode API", () => {
|
||||||
expect(Session.ID.create()).toStartWith("ses_")
|
expect(Session.ID.create()).toStartWith("ses_")
|
||||||
expect(Session.MessageID.create()).toStartWith("msg_")
|
expect(Session.MessageID.create()).toStartWith("msg_")
|
||||||
expect(yield* opencode.sessions.list()).toBeArray()
|
expect(yield* opencode.sessions.list()).toBeArray()
|
||||||
|
yield* opencode.tools.attach({
|
||||||
|
public_tool: Tool.make({
|
||||||
|
description: "Public tool",
|
||||||
|
parameters: Schema.Struct({}),
|
||||||
|
success: Schema.Struct({ ok: Schema.Boolean }),
|
||||||
|
execute: () => Effect.succeed({ ok: true }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||||
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
||||||
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
|
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
|
||||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
|
|
@ -46,7 +46,7 @@ const permission = Layer.succeed(
|
||||||
list: () => Effect.die("unused"),
|
list: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const model = OpenAIChat.route
|
const model = OpenAIChat.route
|
||||||
.with({
|
.with({
|
||||||
endpoint: { baseURL: "https://api.openai.com/v1" },
|
endpoint: { baseURL: "https://api.openai.com/v1" },
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { describe, expect } from "bun:test"
|
||||||
import { Tool, ToolFailure } from "@opencode-ai/llm"
|
import { Tool, ToolFailure } from "@opencode-ai/llm"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { Effect, Exit, Layer, Schema, Scope } from "effect"
|
import { Effect, Exit, Layer, Schema, Scope } from "effect"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ const permission = Layer.succeed(
|
||||||
list: () => Effect.die("unused"),
|
list: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const it = testEffect(Layer.mergeAll(permission, registry))
|
const it = testEffect(Layer.mergeAll(permission, registry))
|
||||||
|
|
||||||
const echo = Tool.make({
|
const echo = Tool.make({
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator
|
||||||
import { SessionRunner } from "@opencode-ai/core/session/runner"
|
import { SessionRunner } from "@opencode-ai/core/session/runner"
|
||||||
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
|
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
|
||||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
|
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
||||||
|
import { NativeTool } from "@opencode-ai/core/tool/native"
|
||||||
import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
|
import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
|
@ -93,7 +95,8 @@ const permission = Layer.succeed(
|
||||||
list: () => Effect.die("unused"),
|
list: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const applications = ApplicationTools.layer
|
||||||
|
const registry = ToolRegistry.layer.pipe(Layer.provide(permission), Layer.provide(applications))
|
||||||
const echo = Layer.effectDiscard(
|
const echo = Layer.effectDiscard(
|
||||||
ToolRegistry.Service.use((registry) =>
|
ToolRegistry.Service.use((registry) =>
|
||||||
registry.contribute((editor) => {
|
registry.contribute((editor) => {
|
||||||
|
|
@ -163,6 +166,7 @@ const it = testEffect(
|
||||||
store,
|
store,
|
||||||
client,
|
client,
|
||||||
permission,
|
permission,
|
||||||
|
applications,
|
||||||
registry,
|
registry,
|
||||||
echo,
|
echo,
|
||||||
models,
|
models,
|
||||||
|
|
@ -414,6 +418,55 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("SessionRunnerLLM", () => {
|
describe("SessionRunnerLLM", () => {
|
||||||
|
it.effect("advertises and executes a globally attached application tool", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
const applicationTools = yield* ApplicationTools.Service
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
const contexts: NativeTool.Context[] = []
|
||||||
|
yield* applicationTools.attach({
|
||||||
|
application_context: NativeTool.make({
|
||||||
|
description: "Read application context",
|
||||||
|
parameters: Schema.Struct({ query: Schema.String }),
|
||||||
|
success: Schema.Struct({ answer: Schema.String }),
|
||||||
|
execute: ({ query }, context) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
contexts.push(context)
|
||||||
|
return { answer: query.toUpperCase() }
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Use application context" }), resume: false })
|
||||||
|
responses = [
|
||||||
|
[
|
||||||
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
|
LLMEvent.toolCall({ id: "call-application", name: "application_context", input: { query: "hello" } }),
|
||||||
|
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
|
||||||
|
LLMEvent.finish({ reason: "tool-calls" }),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
]
|
||||||
|
|
||||||
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
|
expect(requests[0]?.tools.map((tool) => tool.name)).toContain("application_context")
|
||||||
|
expect(contexts).toEqual([{ sessionID, id: "call-application", name: "application_context" }])
|
||||||
|
expect(yield* session.context(sessionID)).toMatchObject([
|
||||||
|
{ type: "user", text: "Use application context" },
|
||||||
|
{
|
||||||
|
type: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool",
|
||||||
|
id: "call-application",
|
||||||
|
state: { status: "completed", structured: { answer: "HELLO" } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("starts a real runner turn after default prompt recording", () =>
|
it.effect("starts a real runner turn after default prompt recording", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { ApplyPatchTool } from "@opencode-ai/core/tool/apply-patch"
|
import { ApplyPatchTool } from "@opencode-ai/core/tool/apply-patch"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
|
@ -86,7 +86,7 @@ const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Inte
|
||||||
)
|
)
|
||||||
const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
|
const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
|
||||||
const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning))
|
const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning))
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const patch = ApplyPatchTool.layer.pipe(
|
const patch = ApplyPatchTool.layer.pipe(
|
||||||
Layer.provide(registry),
|
Layer.provide(registry),
|
||||||
Layer.provide(planning),
|
Layer.provide(planning),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { BashTool } from "@opencode-ai/core/tool/bash"
|
import { BashTool } from "@opencode-ai/core/tool/bash"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
@ -113,7 +113,7 @@ const withTool = <A, E, R>(
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
|
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
|
||||||
)
|
)
|
||||||
const mutation = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
|
const mutation = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const bash = BashTool.layer.pipe(
|
const bash = BashTool.layer.pipe(
|
||||||
Layer.provide(registry),
|
Layer.provide(registry),
|
||||||
Layer.provide(permission),
|
Layer.provide(permission),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { EditTool } from "@opencode-ai/core/tool/edit"
|
import { EditTool } from "@opencode-ai/core/tool/edit"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
|
@ -79,7 +79,7 @@ const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Inte
|
||||||
)
|
)
|
||||||
const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
|
const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
|
||||||
const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning))
|
const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning))
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const edit = EditTool.layer.pipe(
|
const edit = EditTool.layer.pipe(
|
||||||
Layer.provide(registry),
|
Layer.provide(registry),
|
||||||
Layer.provide(planning),
|
Layer.provide(planning),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { RelativePath } from "@opencode-ai/core/schema"
|
import { RelativePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { GlobTool } from "@opencode-ai/core/tool/glob"
|
import { GlobTool } from "@opencode-ai/core/tool/glob"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const sessionID = SessionV2.ID.make("ses_glob_tool_test")
|
const sessionID = SessionV2.ID.make("ses_glob_tool_test")
|
||||||
|
|
@ -81,7 +81,7 @@ const search = Layer.succeed(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const glob = GlobTool.layer.pipe(
|
const glob = GlobTool.layer.pipe(
|
||||||
Layer.provide(registry),
|
Layer.provide(registry),
|
||||||
Layer.provide(permission),
|
Layer.provide(permission),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||||
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { GrepTool } from "@opencode-ai/core/tool/grep"
|
import { GrepTool } from "@opencode-ai/core/tool/grep"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { it as runtimeIt } from "./lib/effect"
|
import { it as runtimeIt } from "./lib/effect"
|
||||||
|
|
@ -86,7 +86,7 @@ const permission = Layer.succeed(
|
||||||
list: () => Effect.die("unused"),
|
list: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const grep = GrepTool.layer.pipe(
|
const grep = GrepTool.layer.pipe(
|
||||||
Layer.provide(registry),
|
Layer.provide(registry),
|
||||||
Layer.provide(filesystem),
|
Layer.provide(filesystem),
|
||||||
|
|
@ -140,7 +140,7 @@ function provideLive(directory: string, projectReferences = references({})) {
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
Layer.provide(FSUtil.defaultLayer),
|
||||||
Layer.provide(dependencies),
|
Layer.provide(dependencies),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const grep = GrepTool.layer.pipe(
|
const grep = GrepTool.layer.pipe(
|
||||||
Layer.provide(registry),
|
Layer.provide(registry),
|
||||||
Layer.provide(filesystem),
|
Layer.provide(filesystem),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Effect, Exit, Fiber, Layer } from "effect"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { QuestionV2 } from "@opencode-ai/core/question"
|
import { QuestionV2 } from "@opencode-ai/core/question"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { QuestionTool } from "@opencode-ai/core/tool/question"
|
import { QuestionTool } from "@opencode-ai/core/tool/question"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ const permission = Layer.succeed(
|
||||||
list: () => Effect.die("unused"),
|
list: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const question = Layer.succeed(
|
const question = Layer.succeed(
|
||||||
QuestionV2.Service,
|
QuestionV2.Service,
|
||||||
QuestionV2.Service.of({
|
QuestionV2.Service.of({
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Effect, Layer } from "effect"
|
||||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { ReadTool } from "@opencode-ai/core/tool/read"
|
import { ReadTool } from "@opencode-ai/core/tool/read"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { RelativePath } from "@opencode-ai/core/schema"
|
import { RelativePath } from "@opencode-ai/core/schema"
|
||||||
|
|
@ -127,7 +127,7 @@ const permission = Layer.succeed(
|
||||||
list: () => Effect.die("unused"),
|
list: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const resources = Layer.succeed(
|
const resources = Layer.succeed(
|
||||||
ToolOutputStore.Service,
|
ToolOutputStore.Service,
|
||||||
ToolOutputStore.Service.of({
|
ToolOutputStore.Service.of({
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||||
import { SkillTool } from "@opencode-ai/core/tool/skill"
|
import { SkillTool } from "@opencode-ai/core/tool/skill"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { it } from "./lib/effect"
|
import { it } from "./lib/effect"
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ describe("SkillTool", () => {
|
||||||
forAgent: () => Effect.die("unused"),
|
forAgent: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const resources = Layer.succeed(
|
const resources = Layer.succeed(
|
||||||
ToolOutputStore.Service,
|
ToolOutputStore.Service,
|
||||||
ToolOutputStore.Service.of({
|
ToolOutputStore.Service.of({
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||||
import { SessionTodo } from "@opencode-ai/core/session/todo"
|
import { SessionTodo } from "@opencode-ai/core/session/todo"
|
||||||
import { TodoWriteTool } from "@opencode-ai/core/tool/todowrite"
|
import { TodoWriteTool } from "@opencode-ai/core/tool/todowrite"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const sessionID = SessionV2.ID.make("ses_todowrite_tool_test")
|
const sessionID = SessionV2.ID.make("ses_todowrite_tool_test")
|
||||||
|
|
@ -34,7 +34,7 @@ const permission = Layer.succeed(
|
||||||
const database = Database.layerFromPath(":memory:")
|
const database = Database.layerFromPath(":memory:")
|
||||||
const events = EventV2.layer.pipe(Layer.provide(database))
|
const events = EventV2.layer.pipe(Layer.provide(database))
|
||||||
const todos = SessionTodo.layer.pipe(Layer.provide(database), Layer.provide(events))
|
const todos = SessionTodo.layer.pipe(Layer.provide(database), Layer.provide(events))
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const tool = TodoWriteTool.layer.pipe(Layer.provide(registry), Layer.provide(todos))
|
const tool = TodoWriteTool.layer.pipe(Layer.provide(registry), Layer.provide(todos))
|
||||||
const it = testEffect(Layer.mergeAll(database, events, todos, permission, registry, tool))
|
const it = testEffect(Layer.mergeAll(database, events, todos, permission, registry, tool))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } fr
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { WebFetchTool } from "@opencode-ai/core/tool/webfetch"
|
import { WebFetchTool } from "@opencode-ai/core/tool/webfetch"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ const resources = Layer.succeed(
|
||||||
cleanup: () => Effect.die("unused"),
|
cleanup: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const webfetch = WebFetchTool.layer.pipe(Layer.provide(registry), Layer.provide(http), Layer.provide(resources))
|
const webfetch = WebFetchTool.layer.pipe(Layer.provide(registry), Layer.provide(http), Layer.provide(resources))
|
||||||
const it = testEffect(Layer.mergeAll(registry, permission, http, resources, webfetch))
|
const it = testEffect(Layer.mergeAll(registry, permission, http, resources, webfetch))
|
||||||
const fetchWebfetch = WebFetchTool.layer.pipe(
|
const fetchWebfetch = WebFetchTool.layer.pipe(
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Effect, Layer, Schema } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { WebSearchTool } from "@opencode-ai/core/tool/websearch"
|
import { WebSearchTool } from "@opencode-ai/core/tool/websearch"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
@ -96,7 +96,7 @@ const permission = Layer.succeed(
|
||||||
list: () => Effect.die("unused"),
|
list: () => Effect.die("unused"),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const websearchConfig = Layer.succeed(
|
const websearchConfig = Layer.succeed(
|
||||||
WebSearchTool.ConfigService,
|
WebSearchTool.ConfigService,
|
||||||
WebSearchTool.ConfigService.of({
|
WebSearchTool.ConfigService.of({
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool-registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { WriteTool } from "@opencode-ai/core/tool/write"
|
import { WriteTool } from "@opencode-ai/core/tool/write"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
|
@ -67,7 +67,7 @@ const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Inte
|
||||||
)
|
)
|
||||||
const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
|
const planning = LocationMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(activeLocation))
|
||||||
const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning))
|
const commits = FileMutation.layer.pipe(Layer.provide(filesystem), Layer.provide(planning))
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
const write = WriteTool.layer.pipe(Layer.provide(registry), Layer.provide(planning), Layer.provide(commits))
|
const write = WriteTool.layer.pipe(Layer.provide(registry), Layer.provide(planning), Layer.provide(commits))
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
return yield* body(yield* ToolRegistry.Service)
|
return yield* body(yield* ToolRegistry.Service)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue