From 7a0e9b3e19e1ee7a0c9932fac6041aa694f89fb9 Mon Sep 17 00:00:00 2001 From: MagMueller Date: Thu, 27 Aug 2026 03:24:50 -0700 Subject: [PATCH] docs(sdk): add ask-human tool example --- packages/sdk-next/README.md | 15 ++++++++ packages/sdk-next/examples/ask-human.ts | 15 ++++++++ .../sdk-next/test/ask-human-example.test.ts | 34 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 packages/sdk-next/examples/ask-human.ts create mode 100644 packages/sdk-next/test/ask-human-example.test.ts diff --git a/packages/sdk-next/README.md b/packages/sdk-next/README.md index 6a9e3bef3d..51a17fe46e 100644 --- a/packages/sdk-next/README.md +++ b/packages/sdk-next/README.md @@ -13,6 +13,21 @@ const session = yield * opencode.sessions.get({ sessionID }) It also exports `Tool` and exposes local-only `tools.register(...)`, replacing the former `@opencode-ai/core/public` facade. Registration uses Core's host-level `ApplicationTools` service shared by the host's Locations; each Location retains its own `ToolRegistry` for overlay, lookup, and settlement. Closing the owning Effect Scope releases router resources, location services, fibers, and scoped tool registrations. +Host applications can register a callback as an agent tool. For example, an app can pause for a person instead of teaching each agent how its UI works: + +```ts +import { OpenCode } from "@opencode-ai/sdk-next" +import { makeAskHuman } from "./ask-human" + +const opencode = yield * OpenCode.create() +yield * + opencode.tools.register({ + ask_human: makeAskHuman((question) => showQuestionModal(question)), + }) +``` + +A complete `makeAskHuman` implementation is in [`examples/ask-human.ts`](examples/ask-human.ts). + `sessions.events({ sessionID, after })` replays durable events after the optional aggregate sequence, then emits newly committed durable events. `sessions.interrupt(...)` targets execution owned by this host, and `sessions.message(...)` retrieves one projected Session message. The same constructor is available as a service Layer: diff --git a/packages/sdk-next/examples/ask-human.ts b/packages/sdk-next/examples/ask-human.ts new file mode 100644 index 0000000000..aa63b35f52 --- /dev/null +++ b/packages/sdk-next/examples/ask-human.ts @@ -0,0 +1,15 @@ +import { Tool } from "@opencode-ai/sdk-next" +import { Effect, Schema } from "effect" + +export function makeAskHuman(ask: (question: string) => Promise) { + return Tool.make({ + description: "Ask the person using this agent for a decision or missing information", + input: Schema.Struct({ question: Schema.String }), + output: Schema.Struct({ answer: Schema.String }), + execute: ({ question }) => + Effect.tryPromise({ + try: () => ask(question), + catch: () => new Tool.Failure({ message: "The person did not answer" }), + }).pipe(Effect.map((answer) => ({ answer }))), + }) +} diff --git a/packages/sdk-next/test/ask-human-example.test.ts b/packages/sdk-next/test/ask-human-example.test.ts new file mode 100644 index 0000000000..b71b131e12 --- /dev/null +++ b/packages/sdk-next/test/ask-human-example.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from "bun:test" +import { Agent, Session, SessionMessage } from "@opencode-ai/sdk-next" +import { Tool } from "@opencode-ai/core/tool/tool" +import { Effect } from "effect" +import { makeAskHuman } from "../examples/ask-human" + +test("ask_human returns the host application's answer", async () => { + const asked: string[] = [] + const tool = makeAskHuman(async (question) => { + asked.push(question) + return "Use the existing account" + }) + + const result = await Effect.runPromise( + Tool.settle( + tool, + { + type: "tool-call", + id: "call-ask-human", + name: "ask_human", + input: { question: "Which account should I use?" }, + }, + { + sessionID: Session.ID.make("ses_ask_human"), + agent: Agent.ID.make("build"), + assistantMessageID: SessionMessage.ID.make("msg_ask_human"), + toolCallID: "call-ask-human", + }, + ), + ) + + expect(asked).toEqual(["Which account should I use?"]) + expect(result.structured).toEqual({ answer: "Use the existing account" }) +})