refactor(tool): make Tool.Info init effectful (#21989)
This commit is contained in:
@@ -229,24 +229,24 @@ Still open:
|
|||||||
|
|
||||||
## Tool interface → Effect
|
## Tool interface → Effect
|
||||||
|
|
||||||
Once individual tools are effectified, change `Tool.Info` (`tool/tool.ts`) so `init` and `execute` return `Effect` instead of `Promise`. This lets tool implementations compose natively with the Effect pipeline rather than being wrapped in `Effect.promise()` at the call site. Requires:
|
`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch. Tool definitions should now stay Effect-native all the way through initialization instead of using Promise-returning init callbacks. Tools can still use lazy init callbacks when they need instance-bound state at init time, but those callbacks should return `Effect`, not `Promise`. Remaining work is:
|
||||||
|
|
||||||
1. Migrate each tool to return Effects
|
1. Migrate each tool body to return Effects
|
||||||
2. Update `Tool.define()` factory to work with Effects
|
2. Keep `Tool.define()` inputs Effect-native
|
||||||
3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing
|
3. Update remaining callers to `yield*` tool initialization instead of `await`ing
|
||||||
|
|
||||||
### Tool migration details
|
### Tool migration details
|
||||||
|
|
||||||
Until the tool interface itself returns `Effect`, use this transitional pattern for migrated tools:
|
With `Tool.Info.init()` now effectful, use this transitional pattern for migrated tools that still need Promise-based boundaries internally:
|
||||||
|
|
||||||
- `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
|
- `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
|
||||||
- Keep the bridge at the Promise boundary only. Prefer a single `Effect.runPromise(...)` in the temporary `async execute(...)` implementation, and move the inner logic into `Effect.fn(...)` helpers instead of scattering `runPromise` islands through the tool body.
|
- Keep the bridge at the Promise boundary only inside the tool body when required by external APIs. Do not return Promise-based init callbacks from `Tool.define()`.
|
||||||
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
|
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
|
||||||
|
|
||||||
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
|
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
|
||||||
|
|
||||||
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
|
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
|
||||||
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* Effect.promise(() => info.init())`.
|
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`.
|
||||||
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
|
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
|
||||||
|
|
||||||
This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info` → `Effect` cleanup mostly mechanical later.
|
This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info` → `Effect` cleanup mostly mechanical later.
|
||||||
|
|||||||
@@ -454,52 +454,53 @@ export const BashTool = Tool.define(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return async () => {
|
return () =>
|
||||||
const shell = Shell.acceptable()
|
Effect.sync(() => {
|
||||||
const name = Shell.name(shell)
|
const shell = Shell.acceptable()
|
||||||
const chain =
|
const name = Shell.name(shell)
|
||||||
name === "powershell"
|
const chain =
|
||||||
? "If the commands depend on each other and must run sequentially, avoid '&&' in this shell because Windows PowerShell 5.1 does not support it. Use PowerShell conditionals such as `cmd1; if ($?) { cmd2 }` when later commands must depend on earlier success."
|
name === "powershell"
|
||||||
: "If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead."
|
? "If the commands depend on each other and must run sequentially, avoid '&&' in this shell because Windows PowerShell 5.1 does not support it. Use PowerShell conditionals such as `cmd1; if ($?) { cmd2 }` when later commands must depend on earlier success."
|
||||||
log.info("bash tool using shell", { shell })
|
: "If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead."
|
||||||
|
log.info("bash tool using shell", { shell })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description: DESCRIPTION.replaceAll("${directory}", Instance.directory)
|
description: DESCRIPTION.replaceAll("${directory}", Instance.directory)
|
||||||
.replaceAll("${os}", process.platform)
|
.replaceAll("${os}", process.platform)
|
||||||
.replaceAll("${shell}", name)
|
.replaceAll("${shell}", name)
|
||||||
.replaceAll("${chaining}", chain)
|
.replaceAll("${chaining}", chain)
|
||||||
.replaceAll("${maxLines}", String(Truncate.MAX_LINES))
|
.replaceAll("${maxLines}", String(Truncate.MAX_LINES))
|
||||||
.replaceAll("${maxBytes}", String(Truncate.MAX_BYTES)),
|
.replaceAll("${maxBytes}", String(Truncate.MAX_BYTES)),
|
||||||
parameters: Parameters,
|
parameters: Parameters,
|
||||||
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const cwd = params.workdir
|
const cwd = params.workdir
|
||||||
? yield* resolvePath(params.workdir, Instance.directory, shell)
|
? yield* resolvePath(params.workdir, Instance.directory, shell)
|
||||||
: Instance.directory
|
: Instance.directory
|
||||||
if (params.timeout !== undefined && params.timeout < 0) {
|
if (params.timeout !== undefined && params.timeout < 0) {
|
||||||
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
||||||
}
|
}
|
||||||
const timeout = params.timeout ?? DEFAULT_TIMEOUT
|
const timeout = params.timeout ?? DEFAULT_TIMEOUT
|
||||||
const ps = PS.has(name)
|
const ps = PS.has(name)
|
||||||
const root = yield* parse(params.command, ps)
|
const root = yield* parse(params.command, ps)
|
||||||
const scan = yield* collect(root, cwd, ps, shell)
|
const scan = yield* collect(root, cwd, ps, shell)
|
||||||
if (!Instance.containsPath(cwd)) scan.dirs.add(cwd)
|
if (!Instance.containsPath(cwd)) scan.dirs.add(cwd)
|
||||||
yield* ask(ctx, scan)
|
yield* ask(ctx, scan)
|
||||||
|
|
||||||
return yield* run(
|
return yield* run(
|
||||||
{
|
{
|
||||||
shell,
|
shell,
|
||||||
name,
|
name,
|
||||||
command: params.command,
|
command: params.command,
|
||||||
cwd,
|
cwd,
|
||||||
env: yield* shellEnv(ctx, cwd),
|
env: yield* shellEnv(ctx, cwd),
|
||||||
timeout,
|
timeout,
|
||||||
description: params.description,
|
description: params.description,
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const MultiEditTool = Tool.define(
|
|||||||
"multiedit",
|
"multiedit",
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const editInfo = yield* EditTool
|
const editInfo = yield* EditTool
|
||||||
const edit = yield* Effect.promise(() => editInfo.init())
|
const edit = yield* editInfo.init()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
|
|||||||
@@ -17,84 +17,84 @@ export const SkillTool = Tool.define(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const rg = yield* Ripgrep.Service
|
const rg = yield* Ripgrep.Service
|
||||||
|
return () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const list = yield* skill.available().pipe(Effect.provide(EffectLogger.layer))
|
||||||
|
|
||||||
return async () => {
|
const description =
|
||||||
const list = await Effect.runPromise(skill.available().pipe(Effect.provide(EffectLogger.layer)))
|
list.length === 0
|
||||||
|
? "Load a specialized skill that provides domain-specific instructions and workflows. No skills are currently available."
|
||||||
const description =
|
: [
|
||||||
list.length === 0
|
"Load a specialized skill that provides domain-specific instructions and workflows.",
|
||||||
? "Load a specialized skill that provides domain-specific instructions and workflows. No skills are currently available."
|
|
||||||
: [
|
|
||||||
"Load a specialized skill that provides domain-specific instructions and workflows.",
|
|
||||||
"",
|
|
||||||
"When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
|
|
||||||
"",
|
|
||||||
"The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
|
|
||||||
"",
|
|
||||||
'Tool output includes a `<skill_content name="...">` block with the loaded content.',
|
|
||||||
"",
|
|
||||||
"The following skills provide specialized sets of instructions for particular tasks",
|
|
||||||
"Invoke this tool to load a skill when a task matches one of the available skills listed below:",
|
|
||||||
"",
|
|
||||||
Skill.fmt(list, { verbose: false }),
|
|
||||||
].join("\n")
|
|
||||||
|
|
||||||
return {
|
|
||||||
description,
|
|
||||||
parameters: Parameters,
|
|
||||||
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const info = yield* skill.get(params.name)
|
|
||||||
|
|
||||||
if (!info) {
|
|
||||||
const all = yield* skill.all()
|
|
||||||
const available = all.map((s) => s.name).join(", ")
|
|
||||||
throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* ctx.ask({
|
|
||||||
permission: "skill",
|
|
||||||
patterns: [params.name],
|
|
||||||
always: [params.name],
|
|
||||||
metadata: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
const dir = path.dirname(info.location)
|
|
||||||
const base = pathToFileURL(dir).href
|
|
||||||
|
|
||||||
const limit = 10
|
|
||||||
const files = yield* rg.files({ cwd: dir, follow: false, hidden: true }).pipe(
|
|
||||||
Stream.filter((file) => !file.includes("SKILL.md")),
|
|
||||||
Stream.map((file) => path.resolve(dir, file)),
|
|
||||||
Stream.take(limit),
|
|
||||||
Stream.runCollect,
|
|
||||||
Effect.map((chunk) => [...chunk].map((file) => `<file>${file}</file>`).join("\n")),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `Loaded skill: ${info.name}`,
|
|
||||||
output: [
|
|
||||||
`<skill_content name="${info.name}">`,
|
|
||||||
`# Skill: ${info.name}`,
|
|
||||||
"",
|
"",
|
||||||
info.content.trim(),
|
"When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
|
||||||
"",
|
"",
|
||||||
`Base directory for this skill: ${base}`,
|
"The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
|
||||||
"Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
|
|
||||||
"Note: file list is sampled.",
|
|
||||||
"",
|
"",
|
||||||
"<skill_files>",
|
'Tool output includes a `<skill_content name="...">` block with the loaded content.',
|
||||||
files,
|
"",
|
||||||
"</skill_files>",
|
"The following skills provide specialized sets of instructions for particular tasks",
|
||||||
"</skill_content>",
|
"Invoke this tool to load a skill when a task matches one of the available skills listed below:",
|
||||||
].join("\n"),
|
"",
|
||||||
metadata: {
|
Skill.fmt(list, { verbose: false }),
|
||||||
name: info.name,
|
].join("\n")
|
||||||
dir,
|
|
||||||
},
|
return {
|
||||||
}
|
description,
|
||||||
}).pipe(Effect.orDie),
|
parameters: Parameters,
|
||||||
}
|
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
||||||
}
|
Effect.gen(function* () {
|
||||||
|
const info = yield* skill.get(params.name)
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
const all = yield* skill.all()
|
||||||
|
const available = all.map((s) => s.name).join(", ")
|
||||||
|
throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* ctx.ask({
|
||||||
|
permission: "skill",
|
||||||
|
patterns: [params.name],
|
||||||
|
always: [params.name],
|
||||||
|
metadata: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const dir = path.dirname(info.location)
|
||||||
|
const base = pathToFileURL(dir).href
|
||||||
|
|
||||||
|
const limit = 10
|
||||||
|
const files = yield* rg.files({ cwd: dir, follow: false, hidden: true }).pipe(
|
||||||
|
Stream.filter((file) => !file.includes("SKILL.md")),
|
||||||
|
Stream.map((file) => path.resolve(dir, file)),
|
||||||
|
Stream.take(limit),
|
||||||
|
Stream.runCollect,
|
||||||
|
Effect.map((chunk) => [...chunk].map((file) => `<file>${file}</file>`).join("\n")),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `Loaded skill: ${info.name}`,
|
||||||
|
output: [
|
||||||
|
`<skill_content name="${info.name}">`,
|
||||||
|
`# Skill: ${info.name}`,
|
||||||
|
"",
|
||||||
|
info.content.trim(),
|
||||||
|
"",
|
||||||
|
`Base directory for this skill: ${base}`,
|
||||||
|
"Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
|
||||||
|
"Note: file list is sampled.",
|
||||||
|
"",
|
||||||
|
"<skill_files>",
|
||||||
|
files,
|
||||||
|
"</skill_files>",
|
||||||
|
"</skill_content>",
|
||||||
|
].join("\n"),
|
||||||
|
metadata: {
|
||||||
|
name: info.name,
|
||||||
|
dir,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}).pipe(Effect.orDie),
|
||||||
|
}
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,9 +47,13 @@ export namespace Tool {
|
|||||||
|
|
||||||
export interface Info<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
|
export interface Info<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
|
||||||
id: string
|
id: string
|
||||||
init: () => Promise<DefWithoutID<Parameters, M>>
|
init: () => Effect.Effect<DefWithoutID<Parameters, M>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Init<Parameters extends z.ZodType, M extends Metadata> =
|
||||||
|
| DefWithoutID<Parameters, M>
|
||||||
|
| (() => Effect.Effect<DefWithoutID<Parameters, M>>)
|
||||||
|
|
||||||
export type InferParameters<T> =
|
export type InferParameters<T> =
|
||||||
T extends Info<infer P, any>
|
T extends Info<infer P, any>
|
||||||
? z.infer<P>
|
? z.infer<P>
|
||||||
@@ -66,60 +70,58 @@ export namespace Tool {
|
|||||||
? Def<P, M>
|
? Def<P, M>
|
||||||
: never
|
: never
|
||||||
|
|
||||||
function wrap<Parameters extends z.ZodType, Result extends Metadata>(
|
function wrap<Parameters extends z.ZodType, Result extends Metadata>(id: string, init: Init<Parameters, Result>) {
|
||||||
id: string,
|
return () =>
|
||||||
init: (() => Promise<DefWithoutID<Parameters, Result>>) | DefWithoutID<Parameters, Result>,
|
Effect.gen(function* () {
|
||||||
) {
|
const toolInfo = init instanceof Function ? { ...(yield* init()) } : { ...init }
|
||||||
return async () => {
|
const execute = toolInfo.execute
|
||||||
const toolInfo = init instanceof Function ? await init() : { ...init }
|
toolInfo.execute = (args, ctx) =>
|
||||||
const execute = toolInfo.execute
|
Effect.gen(function* () {
|
||||||
toolInfo.execute = (args, ctx) =>
|
yield* Effect.try({
|
||||||
Effect.gen(function* () {
|
try: () => toolInfo.parameters.parse(args),
|
||||||
yield* Effect.try({
|
catch: (error) => {
|
||||||
try: () => toolInfo.parameters.parse(args),
|
if (error instanceof z.ZodError && toolInfo.formatValidationError) {
|
||||||
catch: (error) => {
|
return new Error(toolInfo.formatValidationError(error), { cause: error })
|
||||||
if (error instanceof z.ZodError && toolInfo.formatValidationError) {
|
}
|
||||||
return new Error(toolInfo.formatValidationError(error), { cause: error })
|
return new Error(
|
||||||
}
|
`The ${id} tool was called with invalid arguments: ${error}.\nPlease rewrite the input so it satisfies the expected schema.`,
|
||||||
return new Error(
|
{ cause: error },
|
||||||
`The ${id} tool was called with invalid arguments: ${error}.\nPlease rewrite the input so it satisfies the expected schema.`,
|
)
|
||||||
{ cause: error },
|
},
|
||||||
)
|
})
|
||||||
},
|
const result = yield* execute(args, ctx)
|
||||||
})
|
if (result.metadata.truncated !== undefined) {
|
||||||
const result = yield* execute(args, ctx)
|
return result
|
||||||
if (result.metadata.truncated !== undefined) {
|
}
|
||||||
return result
|
const agent = yield* Effect.promise(() => Agent.get(ctx.agent))
|
||||||
}
|
const truncated = yield* Effect.promise(() => Truncate.output(result.output, {}, agent))
|
||||||
const agent = yield* Effect.promise(() => Agent.get(ctx.agent))
|
return {
|
||||||
const truncated = yield* Effect.promise(() => Truncate.output(result.output, {}, agent))
|
...result,
|
||||||
return {
|
output: truncated.content,
|
||||||
...result,
|
metadata: {
|
||||||
output: truncated.content,
|
...result.metadata,
|
||||||
metadata: {
|
truncated: truncated.truncated,
|
||||||
...result.metadata,
|
...(truncated.truncated && { outputPath: truncated.outputPath }),
|
||||||
truncated: truncated.truncated,
|
},
|
||||||
...(truncated.truncated && { outputPath: truncated.outputPath }),
|
}
|
||||||
},
|
}).pipe(Effect.orDie)
|
||||||
}
|
return toolInfo
|
||||||
}).pipe(Effect.orDie)
|
})
|
||||||
return toolInfo
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function define<Parameters extends z.ZodType, Result extends Metadata, R, ID extends string = string>(
|
export function define<Parameters extends z.ZodType, Result extends Metadata, R, ID extends string = string>(
|
||||||
id: ID,
|
id: ID,
|
||||||
init: Effect.Effect<(() => Promise<DefWithoutID<Parameters, Result>>) | DefWithoutID<Parameters, Result>, never, R>,
|
init: Effect.Effect<Init<Parameters, Result>, never, R>,
|
||||||
): Effect.Effect<Info<Parameters, Result>, never, R> & { id: ID } {
|
): Effect.Effect<Info<Parameters, Result>, never, R> & { id: ID } {
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
Effect.map(init, (next) => ({ id, init: wrap(id, next) })),
|
Effect.map(init, (init) => ({ id, init: wrap(id, init) })),
|
||||||
{ id },
|
{ id },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function init<P extends z.ZodType, M extends Metadata>(info: Info<P, M>): Effect.Effect<Def<P, M>> {
|
export function init<P extends z.ZodType, M extends Metadata>(info: Info<P, M>): Effect.Effect<Def<P, M>> {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const init = yield* Effect.promise(() => info.init())
|
const init = yield* info.init()
|
||||||
return {
|
return {
|
||||||
...init,
|
...init,
|
||||||
id: info.id,
|
id: info.id,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ type ToolCtx = typeof baseCtx & {
|
|||||||
|
|
||||||
const execute = async (params: { patchText: string }, ctx: ToolCtx) => {
|
const execute = async (params: { patchText: string }, ctx: ToolCtx) => {
|
||||||
const info = await runtime.runPromise(ApplyPatchTool)
|
const info = await runtime.runPromise(ApplyPatchTool)
|
||||||
const tool = await info.init()
|
const tool = await runtime.runPromise(info.init())
|
||||||
return Effect.runPromise(tool.execute(params, ctx))
|
return Effect.runPromise(tool.execute(params, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const runtime = ManagedRuntime.make(
|
|||||||
)
|
)
|
||||||
|
|
||||||
function initBash() {
|
function initBash() {
|
||||||
return runtime.runPromise(BashTool.pipe(Effect.flatMap((info) => Effect.promise(() => info.init()))))
|
return runtime.runPromise(BashTool.pipe(Effect.flatMap((info) => info.init())))
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const resolve = () =>
|
|||||||
runtime.runPromise(
|
runtime.runPromise(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const info = yield* EditTool
|
const info = yield* EditTool
|
||||||
return yield* Effect.promise(() => info.init())
|
return yield* info.init()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
|||||||
const runtime = ManagedRuntime.make(Layer.mergeAll(CrossSpawnSpawner.defaultLayer))
|
const runtime = ManagedRuntime.make(Layer.mergeAll(CrossSpawnSpawner.defaultLayer))
|
||||||
|
|
||||||
function initGrep() {
|
function initGrep() {
|
||||||
return runtime.runPromise(GrepTool.pipe(Effect.flatMap((info) => Effect.promise(() => info.init()))))
|
return runtime.runPromise(GrepTool.pipe(Effect.flatMap((info) => info.init())))
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ describe("tool.question", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const question = yield* Question.Service
|
const question = yield* Question.Service
|
||||||
const toolInfo = yield* QuestionTool
|
const toolInfo = yield* QuestionTool
|
||||||
const tool = yield* Effect.promise(() => toolInfo.init())
|
const tool = yield* toolInfo.init()
|
||||||
const questions = [
|
const questions = [
|
||||||
{
|
{
|
||||||
question: "What is your favorite color?",
|
question: "What is your favorite color?",
|
||||||
@@ -64,7 +64,7 @@ describe("tool.question", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const question = yield* Question.Service
|
const question = yield* Question.Service
|
||||||
const toolInfo = yield* QuestionTool
|
const toolInfo = yield* QuestionTool
|
||||||
const tool = yield* Effect.promise(() => toolInfo.init())
|
const tool = yield* toolInfo.init()
|
||||||
const questions = [
|
const questions = [
|
||||||
{
|
{
|
||||||
question: "What is your favorite animal?",
|
question: "What is your favorite animal?",
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const it = testEffect(
|
|||||||
|
|
||||||
const init = Effect.fn("ReadToolTest.init")(function* () {
|
const init = Effect.fn("ReadToolTest.init")(function* () {
|
||||||
const info = yield* ReadTool
|
const info = yield* ReadTool
|
||||||
return yield* Effect.promise(() => info.init())
|
return yield* info.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
const run = Effect.fn("ReadToolTest.run")(function* (
|
const run = Effect.fn("ReadToolTest.run")(function* (
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ Use this skill.
|
|||||||
fn: async () => {
|
fn: async () => {
|
||||||
const runtime = ManagedRuntime.make(Layer.mergeAll(Skill.defaultLayer, Ripgrep.defaultLayer))
|
const runtime = ManagedRuntime.make(Layer.mergeAll(Skill.defaultLayer, Ripgrep.defaultLayer))
|
||||||
const info = await runtime.runPromise(SkillTool)
|
const info = await runtime.runPromise(SkillTool)
|
||||||
const tool = await info.init()
|
const tool = await runtime.runPromise(info.init())
|
||||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||||
const ctx: Tool.Context = {
|
const ctx: Tool.Context = {
|
||||||
...baseCtx,
|
...baseCtx,
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ describe("tool.task", () => {
|
|||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" })
|
const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" })
|
||||||
const tool = yield* TaskTool
|
const tool = yield* TaskTool
|
||||||
const def = yield* Effect.promise(() => tool.init())
|
const def = yield* tool.init()
|
||||||
let seen: SessionPrompt.PromptInput | undefined
|
let seen: SessionPrompt.PromptInput | undefined
|
||||||
const promptOps = stubOps({ text: "resumed", onPrompt: (input) => (seen = input) })
|
const promptOps = stubOps({ text: "resumed", onPrompt: (input) => (seen = input) })
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ describe("tool.task", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
const tool = yield* TaskTool
|
const tool = yield* TaskTool
|
||||||
const def = yield* Effect.promise(() => tool.init())
|
const def = yield* tool.init()
|
||||||
const calls: unknown[] = []
|
const calls: unknown[] = []
|
||||||
const promptOps = stubOps()
|
const promptOps = stubOps()
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ describe("tool.task", () => {
|
|||||||
const sessions = yield* Session.Service
|
const sessions = yield* Session.Service
|
||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
const tool = yield* TaskTool
|
const tool = yield* TaskTool
|
||||||
const def = yield* Effect.promise(() => tool.init())
|
const def = yield* tool.init()
|
||||||
let seen: SessionPrompt.PromptInput | undefined
|
let seen: SessionPrompt.PromptInput | undefined
|
||||||
const promptOps = stubOps({ text: "created", onPrompt: (input) => (seen = input) })
|
const promptOps = stubOps({ text: "created", onPrompt: (input) => (seen = input) })
|
||||||
|
|
||||||
@@ -318,7 +318,7 @@ describe("tool.task", () => {
|
|||||||
const sessions = yield* Session.Service
|
const sessions = yield* Session.Service
|
||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
const tool = yield* TaskTool
|
const tool = yield* TaskTool
|
||||||
const def = yield* Effect.promise(() => tool.init())
|
const def = yield* tool.init()
|
||||||
let seen: SessionPrompt.PromptInput | undefined
|
let seen: SessionPrompt.PromptInput | undefined
|
||||||
const promptOps = stubOps({ onPrompt: (input) => (seen = input) })
|
const promptOps = stubOps({ onPrompt: (input) => (seen = input) })
|
||||||
|
|
||||||
|
|||||||
@@ -23,23 +23,23 @@ describe("Tool.define", () => {
|
|||||||
|
|
||||||
const info = await Effect.runPromise(Tool.define("test-tool", Effect.succeed(original)))
|
const info = await Effect.runPromise(Tool.define("test-tool", Effect.succeed(original)))
|
||||||
|
|
||||||
await info.init()
|
await Effect.runPromise(info.init())
|
||||||
await info.init()
|
await Effect.runPromise(info.init())
|
||||||
await info.init()
|
await Effect.runPromise(info.init())
|
||||||
|
|
||||||
expect(original.execute).toBe(originalExecute)
|
expect(original.execute).toBe(originalExecute)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("function-defined tool returns fresh objects and is unaffected", async () => {
|
test("effect-defined tool returns fresh objects and is unaffected", async () => {
|
||||||
const info = await Effect.runPromise(
|
const info = await Effect.runPromise(
|
||||||
Tool.define(
|
Tool.define(
|
||||||
"test-fn-tool",
|
"test-fn-tool",
|
||||||
Effect.succeed(() => Promise.resolve(makeTool("test"))),
|
Effect.succeed(() => Effect.succeed(makeTool("test"))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const first = await info.init()
|
const first = await Effect.runPromise(info.init())
|
||||||
const second = await info.init()
|
const second = await Effect.runPromise(info.init())
|
||||||
|
|
||||||
expect(first).not.toBe(second)
|
expect(first).not.toBe(second)
|
||||||
})
|
})
|
||||||
@@ -47,8 +47,8 @@ describe("Tool.define", () => {
|
|||||||
test("object-defined tool returns distinct objects per init() call", async () => {
|
test("object-defined tool returns distinct objects per init() call", async () => {
|
||||||
const info = await Effect.runPromise(Tool.define("test-copy", Effect.succeed(makeTool("test"))))
|
const info = await Effect.runPromise(Tool.define("test-copy", Effect.succeed(makeTool("test"))))
|
||||||
|
|
||||||
const first = await info.init()
|
const first = await Effect.runPromise(info.init())
|
||||||
const second = await info.init()
|
const second = await Effect.runPromise(info.init())
|
||||||
|
|
||||||
expect(first).not.toBe(second)
|
expect(first).not.toBe(second)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ async function withFetch(fetch: (req: Request) => Response | Promise<Response>,
|
|||||||
|
|
||||||
function initTool() {
|
function initTool() {
|
||||||
return WebFetchTool.pipe(
|
return WebFetchTool.pipe(
|
||||||
Effect.flatMap((info) => Effect.promise(() => info.init())),
|
Effect.flatMap((info) => info.init()),
|
||||||
Effect.provide(FetchHttpClient.layer),
|
Effect.provide(FetchHttpClient.layer),
|
||||||
Effect.runPromise,
|
Effect.runPromise,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const it = testEffect(
|
|||||||
|
|
||||||
const init = Effect.fn("WriteToolTest.init")(function* () {
|
const init = Effect.fn("WriteToolTest.init")(function* () {
|
||||||
const info = yield* WriteTool
|
const info = yield* WriteTool
|
||||||
return yield* Effect.promise(() => info.init())
|
return yield* info.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
const run = Effect.fn("WriteToolTest.run")(function* (
|
const run = Effect.fn("WriteToolTest.run")(function* (
|
||||||
|
|||||||
Reference in New Issue
Block a user