feat(core): compact durable tool metadata (#38343)
This commit is contained in:
parent
b91dd78ab3
commit
7a1f9764a2
14 changed files with 169 additions and 14 deletions
|
|
@ -157,6 +157,53 @@ function failedTool(inputID: string): V2Event[] {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function successfulGrep(inputID: string): V2Event[] {
|
||||||
|
const text = "Found 2 matches\n/src/a.ts:\n Line 1: needle\n/src/b.ts:\n Line 2: needle"
|
||||||
|
return [
|
||||||
|
prompted(inputID),
|
||||||
|
{
|
||||||
|
id: "evt_grep_input",
|
||||||
|
created: 1,
|
||||||
|
type: "session.tool.input.started",
|
||||||
|
durable: { aggregateID: "ses_1", seq: 1, version: 1 },
|
||||||
|
data: {
|
||||||
|
sessionID: "ses_1",
|
||||||
|
assistantMessageID: "msg_grep",
|
||||||
|
callID: "call_grep",
|
||||||
|
name: "grep",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "evt_grep_called",
|
||||||
|
created: 2,
|
||||||
|
type: "session.tool.called",
|
||||||
|
durable: { aggregateID: "ses_1", seq: 2, version: 1 },
|
||||||
|
data: {
|
||||||
|
sessionID: "ses_1",
|
||||||
|
assistantMessageID: "msg_grep",
|
||||||
|
callID: "call_grep",
|
||||||
|
input: { pattern: "needle" },
|
||||||
|
executed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "evt_grep_success",
|
||||||
|
created: 3,
|
||||||
|
type: "session.tool.success",
|
||||||
|
durable: { aggregateID: "ses_1", seq: 3, version: 1 },
|
||||||
|
data: {
|
||||||
|
sessionID: "ses_1",
|
||||||
|
assistantMessageID: "msg_grep",
|
||||||
|
callID: "call_grep",
|
||||||
|
structured: { matches: 2 },
|
||||||
|
content: [{ type: "text", text }],
|
||||||
|
executed: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settled(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
// Runs one non-interactive prompt against a mocked SDK. `turn` produces the
|
// Runs one non-interactive prompt against a mocked SDK. `turn` produces the
|
||||||
// live events the prompt admission triggers, keyed by the generated message ID.
|
// live events the prompt admission triggers, keyed by the generated message ID.
|
||||||
async function run(input: {
|
async function run(input: {
|
||||||
|
|
@ -269,6 +316,32 @@ afterEach(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("runNonInteractivePrompt", () => {
|
describe("runNonInteractivePrompt", () => {
|
||||||
|
test("keeps formatted tool output and compact structured metadata in JSON", async () => {
|
||||||
|
const output = await capture({ format: "json", turn: successfulGrep })
|
||||||
|
const events = output.stdout
|
||||||
|
.split("\n")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => JSON.parse(line))
|
||||||
|
|
||||||
|
expect(events).toHaveLength(1)
|
||||||
|
expect(events[0]).toMatchObject({
|
||||||
|
type: "tool_use",
|
||||||
|
part: {
|
||||||
|
tool: "grep",
|
||||||
|
state: {
|
||||||
|
status: "completed",
|
||||||
|
output: expect.stringContaining("Found 2 matches"),
|
||||||
|
metadata: {
|
||||||
|
structured: { matches: 2 },
|
||||||
|
content: [{ type: "text", text: expect.stringContaining("/src/a.ts") }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(events[0].part.state.metadata.structured).toEqual({ matches: 2 })
|
||||||
|
expect(events[0].part.state.metadata.result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
test("uses session.wait then reconciles projected output without a terminal event", async () => {
|
test("uses session.wait then reconciles projected output without a terminal event", async () => {
|
||||||
const idle = Promise.withResolvers<void>()
|
const idle = Promise.withResolvers<void>()
|
||||||
let done = false
|
let done = false
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { FileSystem } from "../filesystem"
|
||||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { Ripgrep } from "../ripgrep"
|
import { Ripgrep } from "../ripgrep"
|
||||||
import { RelativePath } from "../schema"
|
import { NonNegativeInt, RelativePath } from "../schema"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
|
|
||||||
|
|
@ -25,6 +25,9 @@ export const Input = Schema.Struct({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Output = Schema.Array(FileSystem.Entry)
|
export const Output = Schema.Array(FileSystem.Entry)
|
||||||
|
const StructuredOutput = Schema.Struct({
|
||||||
|
count: NonNegativeInt,
|
||||||
|
})
|
||||||
type ModelOutput = typeof Output.Encoded
|
type ModelOutput = typeof Output.Encoded
|
||||||
|
|
||||||
/** Format raw search results into the concise line-oriented output models expect. */
|
/** Format raw search results into the concise line-oriented output models expect. */
|
||||||
|
|
@ -51,6 +54,8 @@ export const Plugin = {
|
||||||
"Find files by glob pattern within the active Location. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.",
|
"Find files by glob pattern within the active Location. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.",
|
||||||
input: Input,
|
input: Input,
|
||||||
output: Output,
|
output: Output,
|
||||||
|
structured: StructuredOutput,
|
||||||
|
toStructuredOutput: ({ output }) => ({ count: output.length }),
|
||||||
toModelOutput: ({ output }) => [
|
toModelOutput: ({ output }) => [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { Ripgrep } from "../ripgrep"
|
import { Ripgrep } from "../ripgrep"
|
||||||
import { RelativePath } from "../schema"
|
import { NonNegativeInt, RelativePath } from "../schema"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
|
|
||||||
export const name = "grep"
|
export const name = "grep"
|
||||||
|
|
@ -30,6 +30,9 @@ export const Input = Schema.Struct({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Output = Schema.Array(FileSystem.Match)
|
export const Output = Schema.Array(FileSystem.Match)
|
||||||
|
const StructuredOutput = Schema.Struct({
|
||||||
|
matches: NonNegativeInt,
|
||||||
|
})
|
||||||
type ModelOutput = typeof Output.Encoded
|
type ModelOutput = typeof Output.Encoded
|
||||||
|
|
||||||
/** Format raw search matches into the familiar concise model output. */
|
/** Format raw search matches into the familiar concise model output. */
|
||||||
|
|
@ -65,6 +68,8 @@ export const Plugin = {
|
||||||
"Search file contents by regular expression within the active Location or an absolute managed tool-output file. Use a path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise file resources, line numbers, and bounded line previews.",
|
"Search file contents by regular expression within the active Location or an absolute managed tool-output file. Use a path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise file resources, line numbers, and bounded line previews.",
|
||||||
input: Input,
|
input: Input,
|
||||||
output: Output,
|
output: Output,
|
||||||
|
structured: StructuredOutput,
|
||||||
|
toStructuredOutput: ({ output }) => ({ matches: output.length }),
|
||||||
toModelOutput: ({ output }) => [
|
toModelOutput: ({ output }) => [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ export const Output = Schema.Struct({
|
||||||
directory: Schema.String,
|
directory: Schema.String,
|
||||||
output: Schema.String,
|
output: Schema.String,
|
||||||
})
|
})
|
||||||
|
const StructuredOutput = Schema.Struct({
|
||||||
|
name: Output.fields.name,
|
||||||
|
directory: Output.fields.directory,
|
||||||
|
})
|
||||||
|
|
||||||
export const description = [
|
export const description = [
|
||||||
"Load a specialized skill when the task at hand matches one of the available skills in the instructions.",
|
"Load a specialized skill when the task at hand matches one of the available skills in the instructions.",
|
||||||
|
|
@ -66,6 +70,8 @@ export const Plugin = {
|
||||||
description,
|
description,
|
||||||
input: Input,
|
input: Input,
|
||||||
output: Output,
|
output: Output,
|
||||||
|
structured: StructuredOutput,
|
||||||
|
toStructuredOutput: ({ output }) => ({ name: output.name, directory: output.directory }),
|
||||||
toModelOutput: ({ output }) => [{ type: "text", text: output.output }],
|
toModelOutput: ({ output }) => [{ type: "text", text: output.output }],
|
||||||
execute: (input, context) =>
|
execute: (input, context) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ export const Output = Schema.Struct({
|
||||||
status: Schema.Literals(["completed", "running"]),
|
status: Schema.Literals(["completed", "running"]),
|
||||||
output: Schema.String,
|
output: Schema.String,
|
||||||
})
|
})
|
||||||
|
const StructuredOutput = Schema.Struct({
|
||||||
|
sessionID: Output.fields.sessionID,
|
||||||
|
status: Output.fields.status,
|
||||||
|
})
|
||||||
|
|
||||||
export const description = [
|
export const description = [
|
||||||
"Spawn a subagent: a child session running a configured agent with fresh context.",
|
"Spawn a subagent: a child session running a configured agent with fresh context.",
|
||||||
|
|
@ -115,6 +119,8 @@ export const Plugin = {
|
||||||
description,
|
description,
|
||||||
input: Input,
|
input: Input,
|
||||||
output: Output,
|
output: Output,
|
||||||
|
structured: StructuredOutput,
|
||||||
|
toStructuredOutput: ({ output }) => ({ sessionID: output.sessionID, status: output.status }),
|
||||||
toModelOutput: ({ output }) => [{ type: "text", text: output.output }],
|
toModelOutput: ({ output }) => [{ type: "text", text: output.output }],
|
||||||
execute: (input, context) =>
|
execute: (input, context) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ const Output = Schema.Struct({
|
||||||
format: Input.fields.format,
|
format: Input.fields.format,
|
||||||
output: Schema.String,
|
output: Schema.String,
|
||||||
})
|
})
|
||||||
|
const StructuredOutput = Schema.Struct({
|
||||||
|
contentType: Output.fields.contentType,
|
||||||
|
})
|
||||||
|
|
||||||
type Format = (typeof Input.Type)["format"]
|
type Format = (typeof Input.Type)["format"]
|
||||||
|
|
||||||
|
|
@ -126,6 +129,8 @@ export const Plugin = {
|
||||||
description,
|
description,
|
||||||
input: Input,
|
input: Input,
|
||||||
output: Output,
|
output: Output,
|
||||||
|
structured: StructuredOutput,
|
||||||
|
toStructuredOutput: ({ output }) => ({ contentType: output.contentType }),
|
||||||
toModelOutput: ({ output }) => [{ type: "text", text: output.output }],
|
toModelOutput: ({ output }) => [{ type: "text", text: output.output }],
|
||||||
execute: (input, context) =>
|
execute: (input, context) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,9 @@ const Output = Schema.Struct({
|
||||||
provider: Provider,
|
provider: Provider,
|
||||||
text: Schema.String,
|
text: Schema.String,
|
||||||
})
|
})
|
||||||
|
const StructuredOutput = Schema.Struct({
|
||||||
|
provider: Output.fields.provider,
|
||||||
|
})
|
||||||
|
|
||||||
export const Plugin = {
|
export const Plugin = {
|
||||||
id: "opencode.tool.websearch",
|
id: "opencode.tool.websearch",
|
||||||
|
|
@ -206,6 +209,8 @@ export const Plugin = {
|
||||||
description,
|
description,
|
||||||
input: Input,
|
input: Input,
|
||||||
output: Output,
|
output: Output,
|
||||||
|
structured: StructuredOutput,
|
||||||
|
toStructuredOutput: ({ output }) => ({ provider: output.provider }),
|
||||||
toModelOutput: ({ output }) => [{ type: "text", text: output.text }],
|
toModelOutput: ({ output }) => [{ type: "text", text: output.text }],
|
||||||
execute: (input, context) => {
|
execute: (input, context) => {
|
||||||
const provider = selectProvider(context.sessionID, config, config.provider)
|
const provider = selectProvider(context.sessionID, config, config.provider)
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,12 @@ describe("search tools", () => {
|
||||||
const glob = yield* settleTool(registry, call("glob", { pattern: "*" }))
|
const glob = yield* settleTool(registry, call("glob", { pattern: "*" }))
|
||||||
const grep = yield* settleTool(registry, call("grep", { pattern: "needle" }))
|
const grep = yield* settleTool(registry, call("grep", { pattern: "needle" }))
|
||||||
|
|
||||||
expect(glob.output?.structured).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT)
|
expect(glob.output?.structured).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT })
|
||||||
expect(grep.output?.structured).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT)
|
expect(grep.output?.structured).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT })
|
||||||
|
expect(glob.output?.content).toEqual([{ type: "text", text: glob.result.value }])
|
||||||
|
expect(grep.output?.content).toEqual([{ type: "text", text: grep.result.value }])
|
||||||
|
expect(String(glob.result.value).split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT)
|
||||||
|
expect(grep.result.value).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -119,9 +119,12 @@ describe("SkillTool", () => {
|
||||||
...toolIdentity,
|
...toolIdentity,
|
||||||
call: { type: "tool-call", id: "call-skill-overflow", name: "skill", input: { id: "effect" } },
|
call: { type: "tool-call", id: "call-skill-overflow", name: "skill", input: { id: "effect" } },
|
||||||
}),
|
}),
|
||||||
).toMatchObject({
|
).toEqual({
|
||||||
result: { type: "text", value: SkillTool.toModelOutput(info, [reference]) },
|
result: { type: "text", value: SkillTool.toModelOutput(info, [reference]) },
|
||||||
output: { structured: { name: "Effect" } },
|
output: {
|
||||||
|
structured: { name: "Effect", directory },
|
||||||
|
content: [{ type: "text", text: SkillTool.toModelOutput(info, [reference]) }],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(assertions).toMatchObject([
|
expect(assertions).toMatchObject([
|
||||||
{ sessionID, action: "skill", resources: ["effect"], save: ["effect"] },
|
{ sessionID, action: "skill", resources: ["effect"], save: ["effect"] },
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ const childModel = ModelV2.Ref.make({ id: ModelV2.ID.make("child"), providerID:
|
||||||
const parentModel = ModelV2.Ref.make({ id: ModelV2.ID.make("parent"), providerID: ProviderV2.ID.make("test") })
|
const parentModel = ModelV2.Ref.make({ id: ModelV2.ID.make("parent"), providerID: ProviderV2.ID.make("test") })
|
||||||
const tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
const tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
||||||
|
|
||||||
const outputSessionID = (value: unknown) => Schema.decodeUnknownSync(SubagentTool.Output)(value).sessionID
|
const outputSessionID = (value: unknown) =>
|
||||||
|
Schema.decodeUnknownSync(Schema.Struct({ sessionID: SessionV2.ID }))(value).sessionID
|
||||||
|
|
||||||
const executionNode = makeGlobalNode({
|
const executionNode = makeGlobalNode({
|
||||||
service: SessionExecution.Service,
|
service: SessionExecution.Service,
|
||||||
|
|
@ -229,7 +230,17 @@ describe("SubagentTool", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(settled.output?.structured).toMatchObject({ status: "completed", output: childText })
|
expect(settled).toMatchObject({
|
||||||
|
result: { type: "text", value: childText },
|
||||||
|
output: {
|
||||||
|
structured: { status: "completed" },
|
||||||
|
content: [{ type: "text", text: childText }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(settled.output?.structured).toEqual({
|
||||||
|
sessionID: outputSessionID(settled.output?.structured),
|
||||||
|
status: "completed",
|
||||||
|
})
|
||||||
expect((yield* sessions.get(outputSessionID(settled.output?.structured))).parentID).toBe(parent.id)
|
expect((yield* sessions.get(outputSessionID(settled.output?.structured))).parentID).toBe(parent.id)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -264,8 +275,15 @@ describe("SubagentTool", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(settled.output?.structured).toMatchObject({ status: "completed", output: childText })
|
expect(settled).toMatchObject({
|
||||||
|
result: { type: "text", value: childText },
|
||||||
|
output: {
|
||||||
|
structured: { status: "completed" },
|
||||||
|
content: [{ type: "text", text: childText }],
|
||||||
|
},
|
||||||
|
})
|
||||||
const child = yield* sessions.get(outputSessionID(settled.output?.structured))
|
const child = yield* sessions.get(outputSessionID(settled.output?.structured))
|
||||||
|
expect(settled.output?.structured).toEqual({ sessionID: child.id, status: "completed" })
|
||||||
expect(progress[0]?.structured).toEqual({ sessionID: child.id, status: "running" })
|
expect(progress[0]?.structured).toEqual({ sessionID: child.id, status: "running" })
|
||||||
expect(child).toMatchObject({
|
expect(child).toMatchObject({
|
||||||
parentID: parent.id,
|
parentID: parent.id,
|
||||||
|
|
@ -361,8 +379,10 @@ describe("SubagentTool", () => {
|
||||||
const childID = outputSessionID(settled.output?.structured)
|
const childID = outputSessionID(settled.output?.structured)
|
||||||
expect(settled.output?.structured).toMatchObject({
|
expect(settled.output?.structured).toMatchObject({
|
||||||
status: "running",
|
status: "running",
|
||||||
output: expect.stringContaining(`id: ${childID}`),
|
|
||||||
})
|
})
|
||||||
|
expect(settled.output?.structured).toEqual({ sessionID: childID, status: "running" })
|
||||||
|
expect(settled.result).toEqual({ type: "text", value: expect.stringContaining(`id: ${childID}`) })
|
||||||
|
expect(settled.output?.content).toEqual([{ type: "text", text: expect.stringContaining(`id: ${childID}`) }])
|
||||||
|
|
||||||
const admission = Array.from(yield* Fiber.join(admitted))[0]
|
const admission = Array.from(yield* Fiber.join(admitted))[0]
|
||||||
expect(admission?.data.input.data.text).toContain(`<subagent id="${childID}" state="completed"`)
|
expect(admission?.data.input.data.text).toContain(`<subagent id="${childID}" state="completed"`)
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ describe("WebFetchTool registration", () => {
|
||||||
expect(yield* settleTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({
|
expect(yield* settleTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({
|
||||||
result: { type: "text", value: "hello" },
|
result: { type: "text", value: "hello" },
|
||||||
output: {
|
output: {
|
||||||
structured: { url, contentType: "text/plain", format: "text", output: "hello" },
|
structured: { contentType: "text/plain" },
|
||||||
content: [{ type: "text", text: "hello" }],
|
content: [{ type: "text", text: "hello" }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ describe("WebSearchTool registration", () => {
|
||||||
expect(settled).toEqual({
|
expect(settled).toEqual({
|
||||||
result: { type: "text", value: "parallel results" },
|
result: { type: "text", value: "parallel results" },
|
||||||
output: {
|
output: {
|
||||||
structured: { provider: "parallel", text: "parallel results" },
|
structured: { provider: "parallel" },
|
||||||
content: [{ type: "text", text: "parallel results" }],
|
content: [{ type: "text", text: "parallel results" }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2669,8 +2669,7 @@ function WebFetch(props: ToolProps) {
|
||||||
function WebSearch(props: ToolProps) {
|
function WebSearch(props: ToolProps) {
|
||||||
return (
|
return (
|
||||||
<InlineTool icon="◈" pending="Searching web..." complete={stringValue(props.input.query)} part={props.part}>
|
<InlineTool icon="◈" pending="Searching web..." complete={stringValue(props.input.query)} part={props.part}>
|
||||||
{webSearchProviderLabel(props.metadata.provider)} "{stringValue(props.input.query)}"{" "}
|
{webSearchProviderLabel(props.metadata.provider)} "{stringValue(props.input.query)}"
|
||||||
<Show when={finiteNumber(props.metadata.numResults)}>({finiteNumber(props.metadata.numResults)} results)</Show>
|
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { normalizeTool, toolInlineInfo, toolOutputText, toolPath, toolScroll } from "../../src/mini/tool"
|
import { normalizeTool, toolInlineInfo, toolOutputText, toolPath, toolScroll } from "../../src/mini/tool"
|
||||||
|
import { canonicalToolPart } from "./fixture/tool-part"
|
||||||
|
|
||||||
describe("Mini tool presentation", () => {
|
describe("Mini tool presentation", () => {
|
||||||
test("uses V2 shell output without the model-facing status", () => {
|
test("uses V2 shell output without the model-facing status", () => {
|
||||||
|
|
@ -105,6 +106,29 @@ describe("Mini tool presentation", () => {
|
||||||
).toBe('→ Skill "effect"')
|
).toBe('→ Skill "effect"')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("renders compact search metadata", () => {
|
||||||
|
expect(
|
||||||
|
toolInlineInfo(
|
||||||
|
canonicalToolPart("glob", {
|
||||||
|
status: "completed",
|
||||||
|
input: { pattern: "*.ts" },
|
||||||
|
structured: { count: 3 },
|
||||||
|
content: [],
|
||||||
|
}),
|
||||||
|
).description,
|
||||||
|
).toBe("3 matches")
|
||||||
|
expect(
|
||||||
|
toolInlineInfo(
|
||||||
|
canonicalToolPart("grep", {
|
||||||
|
status: "completed",
|
||||||
|
input: { pattern: "needle" },
|
||||||
|
structured: { matches: 1 },
|
||||||
|
content: [],
|
||||||
|
}),
|
||||||
|
).description,
|
||||||
|
).toBe("1 match")
|
||||||
|
})
|
||||||
|
|
||||||
test("keeps segment-safe contained tool paths relative", () => {
|
test("keeps segment-safe contained tool paths relative", () => {
|
||||||
expect(toolPath("..cache/result.txt", { directory: "/work/project" })).toBe("..cache/result.txt")
|
expect(toolPath("..cache/result.txt", { directory: "/work/project" })).toBe("..cache/result.txt")
|
||||||
expect(toolPath("../shared/result.txt", { directory: "/work/project" })).toBe("/work/shared/result.txt")
|
expect(toolPath("../shared/result.txt", { directory: "/work/project" })).toBe("/work/shared/result.txt")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue