T14 — Effect-based lazy tool init với service injection
Tool.define(id, Effect.gen(...)) resolve service dependencies (Truncate, Agent) một lần tại define-time, sau đó wrap execute để gắn truncation + OTel span tự động.Tổng quan Tool Design
Tại sao quan trọng. Mỗi tool cần dùng các service:
Truncate để giới hạn output, Agent config để biết model đang dùng, logger, tracer... Nếu inject tại mỗi lần execute() → overhead mỗi call + code lặp lại ở mọi tool. Ngược lại, dùng global singleton → không testable. Effect's service resolution giải quyết: resolve một lần tại define-time, sau đó wrap() intercept execute để gắn cross-cutting concerns (truncation, tracing) — tool author chỉ viết business logic thuần.
Effect-TS specific: Đây là ứng dụng của Reader Monad pattern — khả năng "inject dependencies qua type channel" thay vì function argument. R channel của Effect type encode dependencies cần thiết, compiler enforce phải resolve trước khi run.
Phân tích code chi tiết Anatomy
Tool.define — resolve services once at definition time
tool/tool.ts — Tool.define + wrap
TS
{`
export namespace Tool {
export const define = <P extends z.ZodType, R>(
id: string,
// init là Effect, R channel = services cần thiết (Truncate, Agent)
init: Effect.Effect<Definition<P, R>, never, Truncate | Agent>,
) => {
return Effect.gen(function* () {
// Resolve services MỘT LẦN tại define-time
const def = yield* init
const truncate = yield* Truncate
const agent = yield* Agent
// Trả về tool đã được wrap — execute luôn có truncation + tracing
return wrap(id, def, truncate, agent)
})
}
// wrap intercepts execute cho mọi tool
function wrap<P, R>(
id: string,
def: Definition<P, R>,
truncate: Truncate,
agent: Agent,
) {
return {
...def,
execute: (args: z.infer<P>, ctx: ExecuteCtx) =>
Effect.withSpan(`Tool.execute`, {
attributes: { "tool.name": id, "session.id": ctx.sessionId },
})(
def.execute(args, ctx).pipe(
Effect.tap((result) => truncate.apply(id, result)),
),
),
}
}
}
`}Cách tool author dùng Tool.define
tool/bash.ts — tool sử dụng Tool.define
TS
{`
export const BashTool = Tool.define(
"bash",
Effect.gen(function* () {
// Tool author chỉ cần yield services mà TOOL CỤ THỂ cần
const session = yield* Session.Ops
return {
description: renderTemplate(DESCRIPTION, { PLATFORM: process.platform }),
parameters: z.object({ command: z.string(), timeout: z.number().optional() }),
// execute KHÔNG cần nghĩ về truncation hay tracing — wrap lo hết
async execute(args, ctx) {
const result = await runBash(args.command, args.timeout)
await session.setToolStatus(ctx.callId, "completed")
return result // ← sẽ tự động bị truncate bởi wrap()
},
}
})
)
`}
Tool.define("bash", Effect.gen(...))
│
▼
[Effect resolve services]
│
├─► Truncate service
├─► Agent service
└─► Tool-specific services (Session.Ops, etc.)
│
▼
wrap(id, def, truncate, agent)
│
▼
{ ...def, execute: (args, ctx) => withSpan(truncate(def.execute(args, ctx))) }
──────── ──────────────────────────────
OTel truncation auto-applied
Generic equivalent không dùng Effect
Pattern tương đương dùng factory function
TS
{`
function defineTool<A>(id: string, factory: (deps: Deps) => ToolDef<A>) {
// Resolve deps một lần
const deps = resolveDeps() // Truncate, Agent, etc.
const base = factory(deps)
return {
...base,
execute: async (args: A, ctx: Ctx) => {
const span = tracer.startSpan(`tool.${id}`)
try {
const raw = await base.execute(args, ctx)
return deps.truncate(raw) // auto truncate
} finally {
span.end()
}
},
}
}
`}Tương tác với kỹ thuật khác Interaction
- T15 (Output truncation):
truncate.apply(id, result)trong wrap là trực tiếp invoke T15. Tool author không cần biết truncation tồn tại. - T13 (Tool description .txt): Description được render và truyền vào definition object bên trong Effect.gen block — cùng scope với service resolution.
- T21 (OTel tracing):
Effect.withSpantrong wrap đặt span tự động cho mọi tool. Metadata (tool.name, session.id, call.id) được attach mà không cần tool author làm gì. - T4 (Interruption-safe cleanup): Effect scope propagation đảm bảo nếu tool bị interrupt, Effect sẽ finalize span và log abort.
Failure modes Failure
1. Effect learning curve
Dev mới gặp Effect.gen yield* syntax lần đầu thường bị bối rối. Error message của Effect khi service không được provide có thể cryptic. Onboarding cần time để hiểu R channel và service layer.
2. Vô tình bỏ qua truncation
Nếu tool trả về stream thay vì string, truncate.apply có thể không hoạt động đúng hoặc bị bypass. Cần escape hatch cho streaming tools.
3. Service không được provide
Nếu run Effect mà không provide Truncate layer, Effect sẽ throw type error (tốt — compile-time) hoặc runtime error (tệ — nếu dùng Effect.runPromise không qua layer). Cần đảm bảo bootstrap layer đầy đủ.
Quan trọng: Không bao giờ dùng
Effect.runPromise trực tiếp trong tool execute — luôn để Effect runtime của opencode handle. Nếu cần gọi async, dùng Effect.tryPromise.
So sánh với các harness khác Compare
| Harness | DI cho tool | Auto truncate | Auto trace |
|---|---|---|---|
| opencode (Effect) | Service layer R channel, resolve once at define-time | ✅ wrap() tự động | ✅ withSpan tự động |
| Claude Code | Module-level singleton hoặc param injection | ✅ có nhưng explicit | ✅ OpenTelemetry manual |
| Aider | Global config object | ⚠️ manual trong từng tool | ❌ không |
| Cline | Constructor injection | ✅ có wrapper | ⚠️ partial |
| OpenHarness | Python DI đơn giản qua constructor | ⚠️ manual | ❌ không |
Implementation recipe Recipe
Nếu không dùng Effect, đây là cách implement pattern tương đương với factory + wrapper:
TS
{`
// 1. Định nghĩa interface Deps
interface ToolDeps {
truncate: (toolId: string, output: string) => string
tracer: Tracer
config: AgentConfig
}
// 2. Factory function nhận deps một lần
function createTools(deps: ToolDeps) {
function define<Args>(id: string, def: ToolDef<Args>) {
return {
...def,
execute: async (args: Args, ctx: Ctx) => {
const span = deps.tracer.startSpan(`tool.${id}`, { sessionId: ctx.sessionId })
try {
const raw = await def.execute(args, ctx)
return deps.truncate(id, raw)
} catch (err) {
span.recordException(err)
throw err
} finally {
span.end()
}
},
}
}
return {
bash: define("bash", { description: "...", execute: bashHandler }),
edit: define("edit", { description: "...", execute: editHandler }),
}
}
// 3. Bootstrap một lần khi start
const tools = createTools({
truncate: createTruncate({ maxLines: 2000, maxBytes: 50_000 }),
tracer: opentelemetry.trace.getTracer("opencode"),
config: await loadAgentConfig(),
})
`} Key insight: Mục tiêu là tool author không phải nghĩ đến cross-cutting concerns. Dù dùng Effect hay factory pattern, nguyên tắc là: resolve dependencies một lần, wrap execute để inject behavior, tool author chỉ viết business logic.