Harness spec 1.1.0: one plan, prototypes and the library shared; the CLI implements it
The spec: plan_write and plan_update (new, shared, offered in every mode) and plan_submit (now shared) keep one structured plan — findings, objectives, phases of tasks — in place of the todo list, which lives on as a former name of plan_write. The shared system text is one text for both projects, with variables for what differs; the mode texts, a prototypes block and a library block are shared. acp.md: the client hands the device what only it knows (context, library, prototypes), the plan travels as a structure (plan_state), library calls carry the chat's scope, and a client may rewind a session. loop.json says its numbers as numbers. Four conformance areas: plan, prototype, library_scope, toolcall_index. The CLI: the plan is kept per session in the store and drawn above the prompt and on the approval card; the prompt is assembled from the spec's texts with their variables filled; the link reads the instance's instructions, personalization, knowledge scope and prototype offer, sends the plan as a structure and answers rewind; library calls to the instance carry the session as scope; a prototype written in the terminal is saved as a file and named in the transcript; a tool-call fragment without an index continues the last call; the default theme is Moria; dead exports and Hermes leftovers removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
96 files changed
+3259
-527
No files matched your search
File renamed without changes.
+7
-3
@@ -2,7 +2,7 @@
|
||||
// Wording of knowledge_search after LLeMbas's tool.knowledge prompt (© Jaroslav Beneš,
|
||||
// MIT): look there before the web, and read the document before answering from it.
|
||||
import { z } from "zod"
|
||||
import { SNIPPET_CHARS } from "../library/store.ts"
|
||||
import { resolveBase, SNIPPET_CHARS } from "../library/store.ts"
|
||||
import { defineTool, ToolError, type ToolContext } from "./tool.ts"
|
||||
import { described } from "../harness.ts"
|
||||
|
||||
@@ -104,8 +104,12 @@ export const knowledgeSearchTool = defineTool({
|
||||
permission: (a) => ({ permission: "knowledge", class: "read", patterns: [a.base ?? "*"] }),
|
||||
async run(a, ctx) {
|
||||
const L = lib(ctx)
|
||||
const bases = a.base ? [a.base] : L.bases
|
||||
if (a.base && L.bases && !L.bases.includes(a.base)) throw new ToolError(`"${a.base}" is not one of this project's knowledge bases: ${L.bases.join(", ")}`)
|
||||
// The bases in scope (all of this machine's, or the project's `knowledge:`), and `base` read
|
||||
// against them as LLeMbas reads it: by name whatever its case, an unknown one named back.
|
||||
const inScope = L.lib.bases().filter((b) => !L.bases || L.bases.includes(b.name)).map((b) => ({ id: b.name, name: b.name }))
|
||||
const resolved = resolveBase(inScope, a.base)
|
||||
if ("error" in resolved) throw new ToolError(resolved.error)
|
||||
const bases = resolved.ids
|
||||
let note = ""
|
||||
let hits
|
||||
try {
|
||||
|
||||
+9
-1
@@ -3,11 +3,13 @@
|
||||
// agent file, a model trained on the old name — runs the tool it now is. Only the CLI's own
|
||||
// former names: LLeMbas's differ in their arguments too, and are LLeMbas's to read.
|
||||
import { TOOL_SPECS } from "../harness.ts"
|
||||
import { fromTodos, isPlan } from "../plan/plan.ts"
|
||||
|
||||
const RENAMED: Record<string, string> = {}
|
||||
const ARGUMENTS_RENAMED: Record<string, Record<string, string>> = {}
|
||||
for (const t of Object.values(TOOL_SPECS)) {
|
||||
// notes became three tools; which one depends on the call's action (below).
|
||||
// notes became three tools; which one depends on the call's action (below). todo took a
|
||||
// different argument (todos[]), converted below.
|
||||
for (const old of t.aliases.cli) if (old !== "notes") RENAMED[old] = t.name
|
||||
if (t.argument_aliases) ARGUMENTS_RENAMED[t.name] = t.argument_aliases
|
||||
}
|
||||
@@ -24,6 +26,12 @@ export function resolveCall(name: string, raw: unknown): { name: string; raw: un
|
||||
else to = "note_manage"
|
||||
args = to === "note_manage" ? obj : rest
|
||||
}
|
||||
// todo's list is one phase of the plan now (the spec's one argument conversion, conformance
|
||||
// plan.json `todos`): the whole plan is sent, as plan_write takes it.
|
||||
if (name === "todo" && obj) {
|
||||
const plan = fromTodos(obj.todos)
|
||||
args = isPlan(plan) ? { title: plan.title, phases: plan.phases.map((p) => ({ title: p.title, status: p.status, tasks: p.tasks.map((t) => ({ text: t.text, status: t.status })) })) } : { title: "Work", phases: [] }
|
||||
}
|
||||
const renamed = ARGUMENTS_RENAMED[to]
|
||||
if (renamed && args && typeof args === "object" && !Array.isArray(args)) {
|
||||
const a = { ...(args as Record<string, unknown>) }
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { existsSync, readFileSync, realpathSync } from "node:fs"
|
||||
import { z } from "zod"
|
||||
import type { Mode } from "../config/schema.ts"
|
||||
import { absPath, defineTool, showPath, ToolError } from "./tool.ts"
|
||||
import { described } from "../harness.ts"
|
||||
|
||||
export type PlanReply = { kind: "approve"; mode: Extract<Mode, "edit" | "manual"> } | { kind: "revise"; feedback: string } | { kind: "back"; text: string } | { kind: "dismissed" }
|
||||
|
||||
/** What the model is told when nobody can approve (also where the client turns out to have no plan
|
||||
* card). */
|
||||
export const nobodyToApprove = (path: string) => `Nobody is present to approve the plan. It is saved at ${path}. Stop here: say the plan is ready and where it is. Make no changes.`
|
||||
|
||||
export const planSubmitTool = defineTool({
|
||||
...described("plan_submit"),
|
||||
purpose: false,
|
||||
access: "plan_submit",
|
||||
requires: "plan",
|
||||
exclusive: true,
|
||||
schema: z.object({ path: z.string() }),
|
||||
permission: () => ({ permission: "plan_submit", class: "interact", patterns: ["*"] }),
|
||||
async run(a, ctx) {
|
||||
const abs = absPath(a.path, ctx)
|
||||
const plans = ctx.planDir
|
||||
if (plans && !abs.startsWith(plans + "/")) throw new ToolError(`The plan must be a file under ${showPath(plans, ctx)}; ${showPath(abs, ctx)} is not.`)
|
||||
if (!existsSync(abs)) throw new ToolError(`${showPath(abs, ctx)} does not exist — write the plan first.`)
|
||||
// By where it really is: a link under the plans directory could point anywhere.
|
||||
if (plans && !realpathSync(abs).startsWith(realpathSync(plans) + "/")) throw new ToolError(`${showPath(abs, ctx)} leads out of the plans directory.`)
|
||||
const text = readFileSync(abs, "utf8")
|
||||
if (!ctx.plan)
|
||||
return {
|
||||
output: nobodyToApprove(showPath(abs, ctx)),
|
||||
title: `${showPath(abs, ctx)} · saved, not approved`,
|
||||
}
|
||||
const r = await ctx.plan({ path: showPath(abs, ctx), text }, ctx.callId)
|
||||
const shown = showPath(abs, ctx)
|
||||
switch (r.kind) {
|
||||
case "approve":
|
||||
ctx.setMode?.(r.mode)
|
||||
return {
|
||||
output: `The user approved the plan in ${shown}. The permission mode is now ${r.mode}. Implement it now, step by step as written, keeping a todo list that follows its steps. If you find the plan wrong somewhere, say so and adjust rather than follow it into a mistake.`,
|
||||
title: `${shown} · approved → ${r.mode}`,
|
||||
}
|
||||
case "revise":
|
||||
return { output: `The user wants the plan revised: ${r.feedback}\nUpdate ${shown}, then call plan_submit again.`, title: `${shown} · revise` }
|
||||
case "back":
|
||||
return { output: `Before deciding, the user asks: "${r.text}"\nAnswer it in your reply, revise ${shown} if the answer changes it, then call plan_submit again.`, title: `${shown} · asked back` }
|
||||
case "dismissed":
|
||||
return { output: `The user closed the plan without deciding. Stay in plan mode; say the plan is at ${shown} and wait.`, title: `${shown} · not decided` }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
// plan_submit: the plan written with plan_write, presented for approval — plan mode only, where
|
||||
// it is the approval. The plan itself is the session's (src/plan/plan.ts); a copy goes under the
|
||||
// project's plans directory for the record, since that is the one place plan mode may write.
|
||||
import { mkdirSync, writeFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { z } from "zod"
|
||||
import type { Mode } from "../config/schema.ts"
|
||||
import { described } from "../harness.ts"
|
||||
import { render, type Plan } from "../plan/plan.ts"
|
||||
import { defineTool, showPath, ToolError } from "./tool.ts"
|
||||
|
||||
export type PlanReply = { kind: "approve"; mode: Extract<Mode, "edit" | "manual"> } | { kind: "revise"; feedback: string } | { kind: "back"; text: string } | { kind: "dismissed" }
|
||||
|
||||
/** What is put to the user: the plan, its text as the card draws it, and the file it was saved to
|
||||
* for the record (relative to the project), or "" where there is no plans directory. */
|
||||
export interface PlanRequest {
|
||||
path: string
|
||||
text: string
|
||||
plan: Plan
|
||||
}
|
||||
|
||||
/** What the model is told when nobody can approve (also where the client turns out to have no plan
|
||||
* card). */
|
||||
export const nobodyToApprove = (path: string) => `Nobody is present to approve the plan.${path ? ` It is saved at ${path}.` : ""} Stop here: say the plan is ready${path ? " and where it is" : ""}. Make no changes.`
|
||||
|
||||
const slug = (title: string) =>
|
||||
title
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 48) || "plan"
|
||||
|
||||
export const planSubmitTool = defineTool({
|
||||
...described("plan_submit"),
|
||||
purpose: false,
|
||||
access: "plan_submit",
|
||||
requires: "plan",
|
||||
exclusive: true,
|
||||
schema: z.object({ path: z.string().optional() }),
|
||||
permission: () => ({ permission: "plan_submit", class: "interact", patterns: ["*"] }),
|
||||
async run(_a, ctx) {
|
||||
const plan = ctx.plan?.get()
|
||||
if (!plan) throw new ToolError("There is no plan yet. Write it with plan_write, then call plan_submit.")
|
||||
const text = render(plan)
|
||||
// For the record: the plans directory is the one place plan mode writes (made on the way, as
|
||||
// the write tool made it before), and a plan that was approved is worth finding again. Nothing
|
||||
// waits on it: a directory that cannot be written is not a reason to lose the approval.
|
||||
let path = ""
|
||||
if (ctx.planDir) {
|
||||
const abs = join(ctx.planDir, `${new Date().toISOString().slice(0, 10)}-${slug(plan.title)}.md`)
|
||||
try {
|
||||
mkdirSync(ctx.planDir, { recursive: true })
|
||||
writeFileSync(abs, text + "\n")
|
||||
path = showPath(abs, ctx)
|
||||
} catch {}
|
||||
}
|
||||
const saved = path ? ` It is saved at ${path}.` : ""
|
||||
if (!ctx.plan?.ask) return { output: nobodyToApprove(path), title: `${plan.title} · saved, not approved` }
|
||||
const r = await ctx.plan.ask({ path, text, plan }, ctx.callId)
|
||||
switch (r.kind) {
|
||||
case "approve":
|
||||
ctx.setMode?.(r.mode)
|
||||
return {
|
||||
output: `The user approved the plan.${saved} The permission mode is now ${r.mode}. Implement it now, phase by phase as written, keeping the plan current with plan_update: a task doing when you start it, done once you have checked it. If you find the plan wrong somewhere, say so and adjust rather than follow it into a mistake.`,
|
||||
title: `${plan.title} · approved → ${r.mode}`,
|
||||
}
|
||||
case "revise":
|
||||
return { output: `The user wants the plan revised: ${r.feedback}\nChange it with plan_write or plan_update, then call plan_submit again.`, title: `${plan.title} · revise` }
|
||||
case "back":
|
||||
return { output: `Before deciding, the user asks: "${r.text}"\nAnswer it in your reply, revise the plan if the answer changes it, then call plan_submit again.`, title: `${plan.title} · asked back` }
|
||||
case "dismissed":
|
||||
return { output: `The user closed the plan without deciding. Stay in plan mode; say the plan is ready${saved} and wait.`, title: `${plan.title} · not decided` }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
// plan_update: the plan changed as the work goes — a task marked doing or done, one dropped with
|
||||
// why, work added, a finding recorded. Several changes in one call; an unknown id is an error
|
||||
// naming the known ones, and then nothing changes (src/plan/plan.ts apply).
|
||||
import { z } from "zod"
|
||||
import { described } from "../harness.ts"
|
||||
import { apply, OBJECTIVE_STATUSES, PHASE_STATUSES, render, TASK_STATUSES } from "../plan/plan.ts"
|
||||
import { planTitle } from "./plan_write.ts"
|
||||
import { defineTool } from "./tool.ts"
|
||||
|
||||
const asList = (v: unknown) => (v === undefined || v === null || Array.isArray(v) ? v : [v])
|
||||
|
||||
export const planUpdateSchema = z.object({
|
||||
tasks: z.preprocess(asList, z.array(z.object({ id: z.string().min(1), status: z.enum(TASK_STATUSES).optional(), note: z.string().max(300).optional() }))).optional(),
|
||||
objectives: z.preprocess(asList, z.array(z.object({ id: z.string().min(1), status: z.enum(OBJECTIVE_STATUSES) }))).optional(),
|
||||
phases: z.preprocess(asList, z.array(z.object({ id: z.string().min(1), status: z.enum(PHASE_STATUSES) }))).optional(),
|
||||
add_tasks: z.preprocess(asList, z.array(z.object({ phase: z.string().min(1), text: z.string().min(1).max(300), after: z.string().optional() }))).optional(),
|
||||
add_findings: z.preprocess(asList, z.array(z.string().min(1).max(300))).optional(),
|
||||
})
|
||||
|
||||
export const planUpdateTool = defineTool({
|
||||
...described("plan_update"),
|
||||
purpose: false,
|
||||
access: "plan",
|
||||
schema: planUpdateSchema,
|
||||
permission: () => ({ permission: "plan", class: "interact", patterns: ["*"] }),
|
||||
async run(a, ctx) {
|
||||
const current = ctx.plan?.get()
|
||||
if (!current) return { output: "There is no plan to update. Write one with plan_write first.", isError: true }
|
||||
const r = apply(current, a)
|
||||
if ("error" in r) return { output: r.error, isError: true }
|
||||
ctx.plan?.set(r.plan)
|
||||
return { output: render(r.plan), title: planTitle(r.plan) }
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
// plan_write: the plan for this work, written whole (it replaces the previous one). The structure,
|
||||
// its ids and bounds are src/plan/plan.ts; the engine keeps the plan per session and puts it into
|
||||
// the prompt on every step while it exists.
|
||||
import { z } from "zod"
|
||||
import { described } from "../harness.ts"
|
||||
import { isPlan, normalise, OBJECTIVE_STATUSES, PHASE_STATUSES, progress, render, TASK_STATUSES, type Plan } from "../plan/plan.ts"
|
||||
import { defineTool, ToolError } from "./tool.ts"
|
||||
|
||||
// Small models send a task as a bare string, a phase as its title, a finding as `{text}`: all of
|
||||
// it is accepted here and read by normalise, as LLeMbas does.
|
||||
const asText = (v: unknown) => (v && typeof v === "object" && "text" in v ? String((v as { text: unknown }).text ?? "") : v)
|
||||
const asItem = (v: unknown) => (typeof v === "string" ? { text: v } : v)
|
||||
const asPhase = (v: unknown) => (typeof v === "string" ? { title: v, tasks: [] } : v)
|
||||
const asList = (v: unknown) => (v === undefined || v === null || Array.isArray(v) ? v : [v])
|
||||
|
||||
export const planWriteSchema = z.object({
|
||||
title: z.string().min(1).max(120),
|
||||
summary: z.string().max(300).optional(),
|
||||
findings: z.preprocess(asList, z.array(z.preprocess(asText, z.string().max(300))).max(20)).optional(),
|
||||
objectives: z
|
||||
.preprocess(asList, z.array(z.preprocess(asItem, z.object({ text: z.string().min(1).max(300), status: z.enum(OBJECTIVE_STATUSES).default("open") }))).max(8))
|
||||
.optional(),
|
||||
phases: z.preprocess(
|
||||
asList,
|
||||
z
|
||||
.array(
|
||||
z.preprocess(
|
||||
asPhase,
|
||||
z.object({
|
||||
title: z.string().max(120).optional(),
|
||||
status: z.enum(PHASE_STATUSES).default("pending"),
|
||||
tasks: z.preprocess(asList, z.array(z.preprocess(asItem, z.object({ text: z.string().min(1).max(300), status: z.enum(TASK_STATUSES).default("todo"), note: z.string().max(300).optional() }))).max(12)),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.max(8),
|
||||
),
|
||||
})
|
||||
|
||||
/** The tool's title line: how much of the plan is done. */
|
||||
export const planTitle = (plan: Plan) => {
|
||||
const { done, total } = progress(plan)
|
||||
return `${done}/${total} done`
|
||||
}
|
||||
|
||||
export const planWriteTool = defineTool({
|
||||
...described("plan_write"),
|
||||
purpose: false,
|
||||
access: "plan",
|
||||
schema: planWriteSchema,
|
||||
permission: () => ({ permission: "plan", class: "interact", patterns: ["*"] }),
|
||||
async run(a, ctx) {
|
||||
const plan = normalise(a)
|
||||
if (!isPlan(plan)) throw new ToolError("The plan is empty: give it a title and at least one phase with tasks.")
|
||||
ctx.plan?.set(plan)
|
||||
return { output: render(plan), title: planTitle(plan) }
|
||||
},
|
||||
})
|
||||
@@ -2,12 +2,13 @@ import { applyPatchTool } from "./apply_patch.ts"
|
||||
import { bashKillTool, bashListTool, bashOutputTool, bashTool } from "./bash.ts"
|
||||
import { editTool } from "./edit.ts"
|
||||
import { multieditTool } from "./multiedit.ts"
|
||||
import { askUserTool } from "./question.ts"
|
||||
import { askUserTool } from "./ask_user.ts"
|
||||
import { readTool } from "./read.ts"
|
||||
import { globTool, grepTool, listTool } from "./search.ts"
|
||||
import { todoTool } from "./todo.ts"
|
||||
import { viewImageTool } from "./view_image.ts"
|
||||
import { planSubmitTool } from "./plan_exit.ts"
|
||||
import { planSubmitTool } from "./plan_submit.ts"
|
||||
import { planUpdateTool } from "./plan_update.ts"
|
||||
import { planWriteTool } from "./plan_write.ts"
|
||||
import { taskTool } from "./task.ts"
|
||||
import { decisionsTool, tasksTool } from "./project_tools.ts"
|
||||
import { webFetchTool, webSearchTool } from "./web.ts"
|
||||
@@ -35,7 +36,8 @@ export const BUILTIN_TOOLS: Tool[] = [
|
||||
webSearchTool,
|
||||
webFetchTool,
|
||||
taskTool,
|
||||
todoTool,
|
||||
planWriteTool,
|
||||
planUpdateTool,
|
||||
tasksTool,
|
||||
decisionsTool,
|
||||
askUserTool,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// A working todo list for multi-step tasks. The whole list is sent each time (OpenCode's
|
||||
// todowrite): simpler for small models than Hermes' merge-by-id, and nothing can drift.
|
||||
import { z } from "zod"
|
||||
import { defineTool } from "./tool.ts"
|
||||
import { described } from "../harness.ts"
|
||||
|
||||
export const TODO_STATUSES = ["pending", "in_progress", "completed", "cancelled"] as const
|
||||
export type Todo = { content: string; status: (typeof TODO_STATUSES)[number] }
|
||||
|
||||
const MARK: Record<Todo["status"], string> = { pending: "[ ]", in_progress: "[>]", completed: "[x]", cancelled: "[~]" }
|
||||
|
||||
export function renderTodos(items: Todo[]): string {
|
||||
return items.map((t) => `${MARK[t.status]} ${t.content}`).join("\n") || "(empty)"
|
||||
}
|
||||
|
||||
export const todoTool = defineTool({
|
||||
...described("todo"),
|
||||
purpose: false,
|
||||
access: "todo",
|
||||
schema: z.object({
|
||||
todos: z
|
||||
.array(
|
||||
z.preprocess(
|
||||
(t) => (typeof t === "string" ? { content: t, status: "pending" } : t),
|
||||
z.object({ content: z.string().min(1), status: z.enum(TODO_STATUSES).default("pending") }),
|
||||
),
|
||||
)
|
||||
.max(30),
|
||||
}),
|
||||
permission: () => ({ permission: "todo", class: "interact", patterns: ["*"] }),
|
||||
async run(a, ctx) {
|
||||
const items = a.todos as Todo[]
|
||||
if (ctx.todos) {
|
||||
ctx.todos.items = items
|
||||
ctx.todos.changed?.(items)
|
||||
}
|
||||
const done = items.filter((t) => t.status === "completed").length
|
||||
return { output: renderTodos(items), title: `${done}/${items.length} done` }
|
||||
},
|
||||
})
|
||||
+11
-5
@@ -20,7 +20,7 @@ export interface ToolContext {
|
||||
/** Instruction files (AGENTS.md / CLAUDE.md) the model has already been given. */
|
||||
instructionsSeen?: Set<string>
|
||||
/** Ask the user questions (the ask_user tool). Absent where nobody can answer. */
|
||||
question?: (req: import("./question.ts").QuestionRequest, callId?: string) => Promise<import("./question.ts").QuestionReply>
|
||||
question?: (req: import("./ask_user.ts").QuestionRequest, callId?: string) => Promise<import("./ask_user.ts").QuestionReply>
|
||||
/** Run a subagent (the task tool). Absent inside a subagent: no recursion. */
|
||||
spawn?: (agent: string, prompt: string, signal: AbortSignal, callId?: string, opts?: { worktree?: boolean; description?: string; model?: string }) => Promise<import("./task.ts").SubagentResult>
|
||||
/** The id of the call being run (set by the engine per call). */
|
||||
@@ -29,8 +29,13 @@ export interface ToolContext {
|
||||
projectDir?: string
|
||||
/** The plans directory (plan mode writes only there). */
|
||||
planDir?: string
|
||||
/** Present a plan for approval (plan_submit). Absent where nobody can answer. */
|
||||
plan?: (req: { path: string; text: string }, callId?: string) => Promise<import("./plan_exit.ts").PlanReply>
|
||||
/** The session's plan (plan_write, plan_update, plan_submit): read it, replace it (null clears),
|
||||
* and present it for approval — `ask` is absent where nobody can answer. */
|
||||
plan?: {
|
||||
get(): import("../plan/plan.ts").Plan | undefined
|
||||
set(plan: import("../plan/plan.ts").Plan | null): void
|
||||
ask?: (req: import("./plan_submit.ts").PlanRequest, callId?: string) => Promise<import("./plan_submit.ts").PlanReply>
|
||||
}
|
||||
/** Change the permission mode (plan_submit, on approval). */
|
||||
setMode?: (mode: import("../config/schema.ts").Mode) => void
|
||||
/** Web search and fetch settings (config `search:`). */
|
||||
@@ -50,6 +55,9 @@ export interface ToolContext {
|
||||
set(key: string, value: unknown, scope?: import("../config/settings.ts").Scope): import("../settings.ts").SetResult
|
||||
models(): string[]
|
||||
}
|
||||
/** The scope sent on every call to the instance's library (`_meta.lembas.scope`, harness/acp.md):
|
||||
* the session's id, where the client has said what the session may search. */
|
||||
libraryScope?: string
|
||||
/** The library: notes, and the knowledge bases this project searches (all, when
|
||||
* `bases` is undefined); `project` is the scope of this project's notes. */
|
||||
library?: {
|
||||
@@ -58,8 +66,6 @@ export interface ToolContext {
|
||||
bases?: string[]
|
||||
embedder?: import("../library/store.ts").Embedder
|
||||
}
|
||||
/** The session's todo list (the todo tool). */
|
||||
todos?: { items: import("./todo.ts").Todo[]; changed?: (items: import("./todo.ts").Todo[]) => void }
|
||||
}
|
||||
|
||||
export interface ToolResult {
|
||||
|
||||
Reference in new issue
Block a user