mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-05 01:43:27 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| caec8d6638 | |||
| 0c89e24aac | |||
| b4ec03e5f1 | |||
| af64ffa5d6 | |||
| 1b708aa098 | |||
| 53b8111121 | |||
| 1fd817ffcf |
@@ -24,7 +24,7 @@ export type ToolExecute<Parameters extends ToolSchema<any>, Success extends Tool
|
||||
) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
|
||||
|
||||
export interface ToolModelOutputInput<Parameters, Output> {
|
||||
readonly callID: ToolCallPart["id"]
|
||||
readonly id: ToolCallPart["id"]
|
||||
readonly parameters: Parameters
|
||||
readonly output: Output
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export interface Definition<Parameters extends ToolSchema<any>, Success extends
|
||||
/** @internal */
|
||||
readonly _project: (
|
||||
parameters: Schema.Schema.Type<Parameters>,
|
||||
callID: ToolCallPart["id"],
|
||||
id: ToolCallPart["id"],
|
||||
output: unknown,
|
||||
) => ToolOutputType
|
||||
/** @internal */
|
||||
@@ -173,8 +173,8 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
|
||||
toStructuredOutput: config.toStructuredOutput,
|
||||
_decode: Effect.succeed,
|
||||
_encode: Effect.succeed,
|
||||
_project: (parameters, callID, output) =>
|
||||
project(config.toModelOutput, config.toStructuredOutput, parameters, callID, output),
|
||||
_project: (parameters, id, output) =>
|
||||
project(config.toModelOutput, config.toStructuredOutput, parameters, id, output),
|
||||
_legacyResult: config.toModelOutput === undefined && config.toStructuredOutput === undefined,
|
||||
_definition: new ToolDefinition({
|
||||
name: "",
|
||||
@@ -193,8 +193,8 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
|
||||
toStructuredOutput: config.toStructuredOutput,
|
||||
_decode: Schema.decodeUnknownEffect(config.parameters),
|
||||
_encode: Schema.encodeEffect(config.success),
|
||||
_project: (parameters, callID, output) =>
|
||||
project(config.toModelOutput, config.toStructuredOutput, parameters, callID, output),
|
||||
_project: (parameters, id, output) =>
|
||||
project(config.toModelOutput, config.toStructuredOutput, parameters, id, output),
|
||||
_legacyResult: false,
|
||||
_definition: new ToolDefinition({
|
||||
name: "",
|
||||
@@ -239,12 +239,12 @@ const project = (
|
||||
toModelOutput: ((input: ToolModelOutputInput<any, any>) => ReadonlyArray<Tool.Content>) | undefined,
|
||||
toStructuredOutput: ((output: unknown) => unknown) | undefined,
|
||||
parameters: unknown,
|
||||
callID: ToolCallPart["id"],
|
||||
id: ToolCallPart["id"],
|
||||
output: unknown,
|
||||
): ToolOutputType =>
|
||||
ToolOutput.make(
|
||||
toStructuredOutput?.(output) ?? output,
|
||||
toModelOutput?.({ callID, parameters, output }) ??
|
||||
toModelOutput?.({ id, parameters, output }) ??
|
||||
(typeof output === "string" ? [{ type: "text", text: output }] : []),
|
||||
)
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("LLMClient tools", () => {
|
||||
LLMEvent.toolCall({ id: "call_projected", name: "projected", input: { prefix: "count" } }),
|
||||
)
|
||||
|
||||
expect(calls).toEqual([{ callID: "call_projected", parameters: { prefix: "count" }, output: { count: "2" } }])
|
||||
expect(calls).toEqual([{ id: "call_projected", parameters: { prefix: "count" }, output: { count: "2" } }])
|
||||
expect(dispatched.result).toEqual({ type: "text", value: "count:2" })
|
||||
expect(dispatched.output).toEqual({ structured: { count: "2" }, content: [{ type: "text", text: "count:2" }] })
|
||||
expect(dispatched.events).toEqual([
|
||||
|
||||
@@ -27,8 +27,8 @@ Tool.make({
|
||||
parameters: Schema.Struct({ city: Schema.String }),
|
||||
success: Schema.Struct({ forecast: Schema.NumberFromString }),
|
||||
execute: () => Effect.succeed({ forecast: 1 }),
|
||||
toModelOutput: ({ callID, parameters, output }) => [
|
||||
{ type: "text", text: `${callID}:${parameters.city}:${output.forecast}` },
|
||||
toModelOutput: ({ id, parameters, output }) => [
|
||||
{ type: "text", text: `${id}:${parameters.city}:${output.forecast}` },
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export async function streamTurn(input: {
|
||||
if (next.done) throw new Error("event stream disconnected during prompt execution")
|
||||
const event = next.value
|
||||
if (event.type === "permission.asked" && event.data.sessionID === input.sessionID) {
|
||||
const tool = event.data.source?.callID ? tools.get(event.data.source.callID) : undefined
|
||||
const tool = event.data.source?.id ? tools.get(event.data.source.id) : undefined
|
||||
await replyPermission({
|
||||
client: input.client,
|
||||
connection: input.connection,
|
||||
@@ -120,11 +120,11 @@ export async function streamTurn(input: {
|
||||
}
|
||||
if (event.type === "session.tool.input.started") {
|
||||
assistantMessageID = event.data.assistantMessageID
|
||||
tools.set(event.data.callID, { name: event.data.name, input: {}, metadata: {}, content: [] })
|
||||
tools.set(event.data.id, { name: event.data.name, input: {}, metadata: {}, content: [] })
|
||||
await update({
|
||||
sessionUpdate: "tool_call",
|
||||
...pendingToolCall({
|
||||
toolCallId: event.data.callID,
|
||||
toolCallId: event.data.id,
|
||||
toolName: event.data.name,
|
||||
state: { input: {} },
|
||||
cwd: input.cwd,
|
||||
@@ -134,13 +134,13 @@ export async function streamTurn(input: {
|
||||
}
|
||||
if (event.type === "session.tool.called") {
|
||||
assistantMessageID = event.data.assistantMessageID
|
||||
const current = tools.get(event.data.callID) ?? emptyToolState()
|
||||
const current = tools.get(event.data.id) ?? emptyToolState()
|
||||
current.input = event.data.input
|
||||
tools.set(event.data.callID, current)
|
||||
tools.set(event.data.id, current)
|
||||
await update({
|
||||
sessionUpdate: "tool_call_update",
|
||||
...runningToolUpdate({
|
||||
toolCallId: event.data.callID,
|
||||
toolCallId: event.data.id,
|
||||
toolName: current.name,
|
||||
state: { input: current.input },
|
||||
cwd: input.cwd,
|
||||
@@ -149,13 +149,13 @@ export async function streamTurn(input: {
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.tool.progress") {
|
||||
const current = tools.get(event.data.callID)
|
||||
const current = tools.get(event.data.id)
|
||||
if (!current) continue
|
||||
current.metadata = event.data.metadata
|
||||
await update({
|
||||
sessionUpdate: "tool_call_update",
|
||||
...runningToolUpdate({
|
||||
toolCallId: event.data.callID,
|
||||
toolCallId: event.data.id,
|
||||
toolName: current.name,
|
||||
state: { input: current.input },
|
||||
cwd: input.cwd,
|
||||
@@ -164,8 +164,8 @@ export async function streamTurn(input: {
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.tool.success") {
|
||||
const current = tools.get(event.data.callID) ?? emptyToolState()
|
||||
tools.delete(event.data.callID)
|
||||
const current = tools.get(event.data.id) ?? emptyToolState()
|
||||
tools.delete(event.data.id)
|
||||
await syncEditedFiles({
|
||||
connection: input.connection,
|
||||
writeTextFile: input.writeTextFile,
|
||||
@@ -178,7 +178,7 @@ export async function streamTurn(input: {
|
||||
await update({
|
||||
sessionUpdate: "tool_call_update",
|
||||
...completedToolUpdate({
|
||||
toolCallId: event.data.callID,
|
||||
toolCallId: event.data.id,
|
||||
toolName: current.name,
|
||||
input: current.input,
|
||||
metadata: event.data.metadata,
|
||||
@@ -188,12 +188,12 @@ export async function streamTurn(input: {
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.tool.failed") {
|
||||
const current = tools.get(event.data.callID) ?? emptyToolState()
|
||||
tools.delete(event.data.callID)
|
||||
const current = tools.get(event.data.id) ?? emptyToolState()
|
||||
tools.delete(event.data.id)
|
||||
await update({
|
||||
sessionUpdate: "tool_call_update",
|
||||
...errorToolUpdate({
|
||||
toolCallId: event.data.callID,
|
||||
toolCallId: event.data.id,
|
||||
toolName: current.name,
|
||||
input: current.input,
|
||||
metadata: event.data.metadata ?? current.metadata,
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function replyPermission(input: {
|
||||
sessionId: input.sessionID,
|
||||
toolCall: {
|
||||
...pendingToolCall({
|
||||
toolCallId: input.event.data.source?.callID ?? input.event.data.id,
|
||||
toolCallId: input.event.data.source?.id ?? input.event.data.id,
|
||||
toolName,
|
||||
state: { input: toolInput, title: permissionTitle(toolName, toolInput, previews) },
|
||||
cwd: input.cwd,
|
||||
|
||||
@@ -300,7 +300,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
|
||||
if (event.type === "session.tool.input.started") {
|
||||
flushStep()
|
||||
tools.set(toolKey(event.data.assistantMessageID, event.data.callID), {
|
||||
tools.set(toolKey(event.data.assistantMessageID, event.data.id), {
|
||||
id: partID(event.id),
|
||||
timestamp: time,
|
||||
assistantMessageID: event.data.assistantMessageID,
|
||||
@@ -312,18 +312,18 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.tool.input.ended") {
|
||||
const current = tools.get(toolKey(event.data.assistantMessageID, event.data.callID))
|
||||
const current = tools.get(toolKey(event.data.assistantMessageID, event.data.id))
|
||||
if (current) current.raw = event.data.text
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.tool.input.delta") {
|
||||
const current = tools.get(toolKey(event.data.assistantMessageID, event.data.callID))
|
||||
const current = tools.get(toolKey(event.data.assistantMessageID, event.data.id))
|
||||
if (current) current.raw = (current.raw ?? "") + event.data.delta
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.tool.called") {
|
||||
flushStep()
|
||||
const key = toolKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = toolKey(event.data.assistantMessageID, event.data.id)
|
||||
const current = tools.get(key)
|
||||
tools.set(key, {
|
||||
id: current?.id ?? partID(event.id),
|
||||
@@ -340,18 +340,18 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.tool.progress") {
|
||||
const current = tools.get(toolKey(event.data.assistantMessageID, event.data.callID))
|
||||
const current = tools.get(toolKey(event.data.assistantMessageID, event.data.id))
|
||||
if (current) {
|
||||
current.metadata = event.data.metadata
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.tool.success") {
|
||||
const key = toolKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = toolKey(event.data.assistantMessageID, event.data.id)
|
||||
const current = tools.get(key) ?? fallbackTool(event)
|
||||
const tool: SessionMessageAssistantTool = {
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: current.tool,
|
||||
executed: event.data.executed,
|
||||
providerState: current.providerState,
|
||||
@@ -365,11 +365,11 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
time: { created: current.timestamp, ran: current.timestamp, completed: time },
|
||||
}
|
||||
const part: MiniToolPart = {
|
||||
id: current.id,
|
||||
partID: current.id,
|
||||
sessionID: input.sessionID,
|
||||
messageID: event.data.assistantMessageID,
|
||||
type: "tool",
|
||||
callID: event.data.callID,
|
||||
id: event.data.id,
|
||||
tool: current.tool,
|
||||
state: {
|
||||
status: "completed",
|
||||
@@ -392,14 +392,14 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
continue
|
||||
}
|
||||
if (event.type === "session.tool.failed") {
|
||||
const key = toolKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = toolKey(event.data.assistantMessageID, event.data.id)
|
||||
const current = tools.get(key) ?? fallbackTool(event)
|
||||
const error = event.data.error.message
|
||||
const metadata = event.data.metadata ?? current.metadata
|
||||
const content = event.data.content ?? nonEmptyToolContent(current.content)
|
||||
const tool: SessionMessageAssistantTool = {
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: current.tool,
|
||||
executed: event.data.executed,
|
||||
providerState: current.providerState,
|
||||
@@ -414,11 +414,11 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
time: { created: current.timestamp, ran: current.timestamp, completed: time },
|
||||
}
|
||||
const part: MiniToolPart = {
|
||||
id: current.id,
|
||||
partID: current.id,
|
||||
sessionID: input.sessionID,
|
||||
messageID: event.data.assistantMessageID,
|
||||
type: "tool",
|
||||
callID: event.data.callID,
|
||||
id: event.data.id,
|
||||
tool: current.tool,
|
||||
state: {
|
||||
status: "error",
|
||||
@@ -578,11 +578,11 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
const key = toolKey(message.id, item.id)
|
||||
if (renderedTools.has(key) || item.state.status === "streaming" || item.state.status === "running") continue
|
||||
const part: MiniToolPart = {
|
||||
id: projectedPartID(message.id, `tool-${item.id}`),
|
||||
partID: projectedPartID(message.id, `tool-${item.id}`),
|
||||
sessionID: input.sessionID,
|
||||
messageID: message.id,
|
||||
type: "tool",
|
||||
callID: item.id,
|
||||
id: item.id,
|
||||
tool: item.name,
|
||||
state:
|
||||
item.state.status === "completed"
|
||||
@@ -771,8 +771,8 @@ function partID(eventID: string) {
|
||||
return `prt_${eventID.replace(/^evt_/, "")}`
|
||||
}
|
||||
|
||||
function toolKey(messageID: string, callID: string) {
|
||||
return `${messageID}\u0000${callID}`
|
||||
function toolKey(messageID: string, id: string) {
|
||||
return `${messageID}\u0000${id}`
|
||||
}
|
||||
|
||||
function contentKey(messageID: string, ordinal: number) {
|
||||
@@ -786,7 +786,7 @@ function projectedPartID(messageID: string, part: string) {
|
||||
function fallbackTool(event: {
|
||||
id: string
|
||||
created: number
|
||||
data: { assistantMessageID: string; callID: string }
|
||||
data: { assistantMessageID: string; id: string }
|
||||
}): ToolState {
|
||||
return {
|
||||
id: partID(event.id),
|
||||
|
||||
@@ -200,7 +200,7 @@ describe("acp event behavior", () => {
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_tools",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_ok",
|
||||
id: "call_ok",
|
||||
name: "shell",
|
||||
}),
|
||||
)
|
||||
@@ -208,7 +208,7 @@ describe("acp event behavior", () => {
|
||||
durableEvent("session.tool.called", {
|
||||
sessionID: "ses_tools",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_ok",
|
||||
id: "call_ok",
|
||||
input: { command: "printf done", workdir: "sub" },
|
||||
executed: false,
|
||||
}),
|
||||
@@ -217,7 +217,7 @@ describe("acp event behavior", () => {
|
||||
ephemeralEvent("session.tool.progress", {
|
||||
sessionID: "ses_tools",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_ok",
|
||||
id: "call_ok",
|
||||
metadata: { phase: 1 },
|
||||
}),
|
||||
)
|
||||
@@ -225,7 +225,7 @@ describe("acp event behavior", () => {
|
||||
durableEvent("session.tool.success", {
|
||||
sessionID: "ses_tools",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_ok",
|
||||
id: "call_ok",
|
||||
metadata: { exit: 0 },
|
||||
content: [{ type: "text", text: "done" }],
|
||||
executed: true,
|
||||
@@ -235,7 +235,7 @@ describe("acp event behavior", () => {
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_tools",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_fail",
|
||||
id: "call_fail",
|
||||
name: "read",
|
||||
}),
|
||||
)
|
||||
@@ -243,7 +243,7 @@ describe("acp event behavior", () => {
|
||||
durableEvent("session.tool.called", {
|
||||
sessionID: "ses_tools",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_fail",
|
||||
id: "call_fail",
|
||||
input: { path: "/workspace/missing.ts" },
|
||||
executed: false,
|
||||
}),
|
||||
@@ -252,7 +252,7 @@ describe("acp event behavior", () => {
|
||||
ephemeralEvent("session.tool.progress", {
|
||||
sessionID: "ses_tools",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_fail",
|
||||
id: "call_fail",
|
||||
metadata: { bytes: 0 },
|
||||
}),
|
||||
)
|
||||
@@ -260,7 +260,7 @@ describe("acp event behavior", () => {
|
||||
durableEvent("session.tool.failed", {
|
||||
sessionID: "ses_tools",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_fail",
|
||||
id: "call_fail",
|
||||
error: { type: "tool.error", message: "not found" },
|
||||
metadata: { bytes: 0 },
|
||||
content: [{ type: "text", text: "opening" }],
|
||||
|
||||
@@ -43,14 +43,14 @@ describe("acp permission behavior", () => {
|
||||
permissionAsked("ses_allow", "perm_once", {
|
||||
action: "shell",
|
||||
metadata: { command: "printf hello" },
|
||||
source: { type: "tool", messageID: "msg_allow", callID: "call_once" },
|
||||
source: { type: "tool", messageID: "msg_allow", id: "call_once" },
|
||||
}),
|
||||
)
|
||||
send(
|
||||
permissionAsked("ses_allow", "perm_always", {
|
||||
action: "read",
|
||||
metadata: { path: "/workspace/file.ts" },
|
||||
source: { type: "tool", messageID: "msg_allow", callID: "call_always" },
|
||||
source: { type: "tool", messageID: "msg_allow", id: "call_always" },
|
||||
}),
|
||||
)
|
||||
send(durableEvent("session.execution.succeeded", { sessionID: "ses_allow" }))
|
||||
@@ -166,7 +166,7 @@ describe("acp permission behavior", () => {
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_edit",
|
||||
assistantMessageID: "msg_edit",
|
||||
callID: "call_edit",
|
||||
id: "call_edit",
|
||||
name: "edit",
|
||||
}),
|
||||
)
|
||||
@@ -174,7 +174,7 @@ describe("acp permission behavior", () => {
|
||||
durableEvent("session.tool.called", {
|
||||
sessionID: "ses_edit",
|
||||
assistantMessageID: "msg_edit",
|
||||
callID: "call_edit",
|
||||
id: "call_edit",
|
||||
input: { path: "file.ts", oldString: "before", newString: "after" },
|
||||
executed: false,
|
||||
}),
|
||||
@@ -182,7 +182,7 @@ describe("acp permission behavior", () => {
|
||||
send(
|
||||
permissionAsked("ses_edit", "perm_edit", {
|
||||
action: "edit",
|
||||
source: { type: "tool", messageID: "msg_edit", callID: "call_edit" },
|
||||
source: { type: "tool", messageID: "msg_edit", id: "call_edit" },
|
||||
}),
|
||||
)
|
||||
},
|
||||
@@ -192,7 +192,7 @@ describe("acp permission behavior", () => {
|
||||
durableEvent("session.tool.success", {
|
||||
sessionID: "ses_edit",
|
||||
assistantMessageID: "msg_edit",
|
||||
callID: "call_edit",
|
||||
id: "call_edit",
|
||||
metadata: { files: [{ file: "file.ts" }], replacements: 1 },
|
||||
content: [{ type: "text", text: "edited" }],
|
||||
executed: true,
|
||||
@@ -256,7 +256,7 @@ describe("acp permission behavior", () => {
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_patch",
|
||||
assistantMessageID: "msg_patch",
|
||||
callID: "call_patch",
|
||||
id: "call_patch",
|
||||
name: "patch",
|
||||
}),
|
||||
)
|
||||
@@ -264,7 +264,7 @@ describe("acp permission behavior", () => {
|
||||
durableEvent("session.tool.called", {
|
||||
sessionID: "ses_patch",
|
||||
assistantMessageID: "msg_patch",
|
||||
callID: "call_patch",
|
||||
id: "call_patch",
|
||||
input: { patchText },
|
||||
executed: false,
|
||||
}),
|
||||
@@ -272,7 +272,7 @@ describe("acp permission behavior", () => {
|
||||
send(
|
||||
permissionAsked("ses_patch", "perm_patch", {
|
||||
action: "edit",
|
||||
source: { type: "tool", messageID: "msg_patch", callID: "call_patch" },
|
||||
source: { type: "tool", messageID: "msg_patch", id: "call_patch" },
|
||||
}),
|
||||
)
|
||||
},
|
||||
@@ -285,7 +285,7 @@ describe("acp permission behavior", () => {
|
||||
durableEvent("session.tool.success", {
|
||||
sessionID: "ses_patch",
|
||||
assistantMessageID: "msg_patch",
|
||||
callID: "call_patch",
|
||||
id: "call_patch",
|
||||
metadata: { files: [{ file: "first.ts" }, { file: "second.ts" }] },
|
||||
content: [{ type: "text", text: "patched" }],
|
||||
executed: true,
|
||||
@@ -499,7 +499,7 @@ function permissionAsked(
|
||||
input: {
|
||||
readonly action?: string
|
||||
readonly metadata?: Record<string, unknown>
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
||||
} = {},
|
||||
) {
|
||||
return ephemeralEvent("permission.asked", {
|
||||
|
||||
@@ -109,7 +109,7 @@ function failedTool(inputID: string): V2Event[] {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_failed_tool",
|
||||
callID: "call_failed_tool",
|
||||
id: "call_failed_tool",
|
||||
name: "shell",
|
||||
},
|
||||
},
|
||||
@@ -121,7 +121,7 @@ function failedTool(inputID: string): V2Event[] {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_failed_tool",
|
||||
callID: "call_failed_tool",
|
||||
id: "call_failed_tool",
|
||||
input: { command: "printf partial && false" },
|
||||
executed: true,
|
||||
},
|
||||
@@ -133,7 +133,7 @@ function failedTool(inputID: string): V2Event[] {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_failed_tool",
|
||||
callID: "call_failed_tool",
|
||||
id: "call_failed_tool",
|
||||
metadata: { checkpoint: 1 },
|
||||
},
|
||||
},
|
||||
@@ -145,7 +145,7 @@ function failedTool(inputID: string): V2Event[] {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_failed_tool",
|
||||
callID: "call_failed_tool",
|
||||
id: "call_failed_tool",
|
||||
error: { type: "unknown", message: "tool failed" },
|
||||
metadata: { checkpoint: 1 },
|
||||
content: [{ type: "text", text: "partial output" }],
|
||||
@@ -168,7 +168,7 @@ function successfulGrep(inputID: string): V2Event[] {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_grep",
|
||||
callID: "call_grep",
|
||||
id: "call_grep",
|
||||
name: "grep",
|
||||
},
|
||||
},
|
||||
@@ -180,7 +180,7 @@ function successfulGrep(inputID: string): V2Event[] {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_grep",
|
||||
callID: "call_grep",
|
||||
id: "call_grep",
|
||||
input: { pattern: "needle" },
|
||||
executed: true,
|
||||
},
|
||||
@@ -193,7 +193,7 @@ function successfulGrep(inputID: string): V2Event[] {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_grep",
|
||||
callID: "call_grep",
|
||||
id: "call_grep",
|
||||
metadata: { matches: 2 },
|
||||
content: [{ type: "text", text }],
|
||||
executed: false,
|
||||
@@ -561,7 +561,7 @@ describe("runNonInteractivePrompt", () => {
|
||||
type: "tool_use",
|
||||
part: {
|
||||
type: "tool",
|
||||
callID: "call_failed_tool",
|
||||
id: "call_failed_tool",
|
||||
tool: "shell",
|
||||
state: {
|
||||
status: "error",
|
||||
|
||||
@@ -605,7 +605,7 @@ export type Endpoint5_26Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly callID: string
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
}
|
||||
}
|
||||
@@ -619,7 +619,7 @@ export type Endpoint5_26Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly callID: string
|
||||
readonly id: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
@@ -633,7 +633,7 @@ export type Endpoint5_26Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly callID: string
|
||||
readonly id: string
|
||||
readonly input: { readonly [x: string]: unknown }
|
||||
readonly executed: boolean
|
||||
readonly state?: SessionMessage.ProviderState | undefined
|
||||
@@ -649,7 +649,7 @@ export type Endpoint5_26Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly callID: string
|
||||
readonly id: string
|
||||
readonly content: readonly [
|
||||
(
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
@@ -685,7 +685,7 @@ export type Endpoint5_26Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly callID: string
|
||||
readonly id: string
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||
readonly content?:
|
||||
| readonly [
|
||||
@@ -764,6 +764,14 @@ export type Endpoint5_26Output =
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly text: string
|
||||
readonly recent: string
|
||||
readonly media?:
|
||||
| ReadonlyArray<{
|
||||
readonly type: "file"
|
||||
readonly uri: string
|
||||
readonly mime: string
|
||||
readonly name?: string | undefined
|
||||
}>
|
||||
| undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -56,7 +56,6 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
||||
let announced = false
|
||||
let lastSpawn = 0
|
||||
let spawnDelay = 5_000
|
||||
let ownerHeld = false
|
||||
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) =>
|
||||
Effect.sync(() => {
|
||||
if (announced) return
|
||||
@@ -84,7 +83,6 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
||||
const info = registration.info
|
||||
const service = registration.service
|
||||
if (service !== undefined) {
|
||||
ownerHeld = false
|
||||
spawnDelay = 5_000
|
||||
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
|
||||
if (compatible && service.state === "ready") return Option.some(service)
|
||||
@@ -97,14 +95,13 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
||||
return Option.none<LocalService>()
|
||||
} else if (lastSpawn === 0 && info !== undefined) lastSpawn = Date.now()
|
||||
|
||||
const failure = [...contenders].map(contenderFailure).find((error): error is Error => error !== undefined)
|
||||
if (failure !== undefined) return yield* Effect.fail(failure)
|
||||
const finished = [...contenders].filter(contenderFinished)
|
||||
const failure = finished.map(contenderFailure).find((error): error is Error => error !== undefined)
|
||||
if (finished.some((item) => item.child.exitCode === 0)) {
|
||||
ownerHeld = true
|
||||
spawnDelay = Math.min(spawnDelay * 2, 30_000)
|
||||
}
|
||||
finished.forEach((item) => contenders.delete(item))
|
||||
if (failure !== undefined && contenders.size === 0) return yield* Effect.fail(failure)
|
||||
// Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
|
||||
if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
|
||||
yield* announce("missing")
|
||||
|
||||
@@ -121,17 +121,6 @@ export type SessionMessageCompactionRunning = {
|
||||
recent: string
|
||||
}
|
||||
|
||||
export type SessionMessageCompactionCompleted = {
|
||||
type: "compaction"
|
||||
id: string
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
time: { created: number }
|
||||
status: "completed"
|
||||
reason: "auto" | "manual"
|
||||
summary: string
|
||||
recent: string
|
||||
}
|
||||
|
||||
export type InstructionEntryKey = string
|
||||
|
||||
export type SessionGenerateResponse = { data: { text: string } }
|
||||
@@ -297,7 +286,7 @@ export type FormExternalField = { key: string; type: "external"; url: string; ti
|
||||
|
||||
export type FormValue = string | number | boolean | Array<string>
|
||||
|
||||
export type PermissionSource = { type: "tool"; messageID: string; callID: string }
|
||||
export type PermissionSource = { type: "tool"; messageID: string; id: string }
|
||||
|
||||
export type PermissionSavedInfo = { id: string; projectID: string; action: string; resource: string }
|
||||
|
||||
@@ -486,7 +475,7 @@ export type Pty = {
|
||||
|
||||
export type QuestionOption = { label: string; description: string }
|
||||
|
||||
export type QuestionTool = { messageID: string; callID: string }
|
||||
export type QuestionTool = { messageID: string; id: string }
|
||||
|
||||
export type QuestionAnswer = Array<string>
|
||||
|
||||
@@ -744,7 +733,7 @@ export type SessionToolInputStarted = {
|
||||
type: "session.tool.input.started"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; assistantMessageID: string; callID: string; name: string }
|
||||
data: { sessionID: string; assistantMessageID: string; id: string; name: string }
|
||||
}
|
||||
|
||||
export type SessionToolInputEnded = {
|
||||
@@ -754,7 +743,7 @@ export type SessionToolInputEnded = {
|
||||
type: "session.tool.input.ended"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; assistantMessageID: string; callID: string; text: string }
|
||||
data: { sessionID: string; assistantMessageID: string; id: string; text: string }
|
||||
}
|
||||
|
||||
export type SessionCompactionAdmitted = {
|
||||
@@ -777,16 +766,6 @@ export type SessionCompactionStarted = {
|
||||
data: { sessionID: string; reason: "auto" | "manual"; recent: string; inputID?: string }
|
||||
}
|
||||
|
||||
export type SessionCompactionEnded = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.compaction.ended"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string }
|
||||
}
|
||||
|
||||
export type SessionRevertCleared = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -915,7 +894,7 @@ export type SessionToolInputDelta = {
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.tool.input.delta"
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; assistantMessageID: string; callID: string; delta: string }
|
||||
data: { sessionID: string; assistantMessageID: string; id: string; delta: string }
|
||||
}
|
||||
|
||||
export type SessionToolProgress = {
|
||||
@@ -924,7 +903,7 @@ export type SessionToolProgress = {
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.tool.progress"
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; assistantMessageID: string; callID: string; metadata: { [x: string]: JsonValue } }
|
||||
data: { sessionID: string; assistantMessageID: string; id: string; metadata: { [x: string]: JsonValue } }
|
||||
}
|
||||
|
||||
export type SessionCompactionDelta = {
|
||||
@@ -1227,6 +1206,18 @@ export type SessionMessageAssistantReasoning = {
|
||||
|
||||
export type ToolContent = ToolTextContent | ToolFileContent
|
||||
|
||||
export type SessionMessageCompactionCompleted = {
|
||||
type: "compaction"
|
||||
id: string
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
time: { created: number }
|
||||
status: "completed"
|
||||
reason: "auto" | "manual"
|
||||
summary: string
|
||||
recent: string
|
||||
media?: Array<ToolFileContent>
|
||||
}
|
||||
|
||||
export type SessionMessageAssistantRetry = { attempt: number; at: number; error: SessionStructuredError }
|
||||
|
||||
export type SessionMessageCompactionFailed = {
|
||||
@@ -1380,7 +1371,7 @@ export type SessionToolCalled = {
|
||||
data: {
|
||||
sessionID: string
|
||||
assistantMessageID: string
|
||||
callID: string
|
||||
id: string
|
||||
input: { [x: string]: any }
|
||||
executed: boolean
|
||||
state?: SessionMessageProviderState7
|
||||
@@ -1389,6 +1380,16 @@ export type SessionToolCalled = {
|
||||
|
||||
export type ToolContent1 = ToolTextContent | ToolFileContent1
|
||||
|
||||
export type SessionCompactionEnded = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.compaction.ended"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string; media?: Array<ToolFileContent1> }
|
||||
}
|
||||
|
||||
export type ModelCompatibility = { reasoningField?: ModelReasoningField }
|
||||
|
||||
export type ModelCost = {
|
||||
@@ -1831,7 +1832,7 @@ export type SessionToolSuccess = {
|
||||
data: {
|
||||
sessionID: string
|
||||
assistantMessageID: string
|
||||
callID: string
|
||||
id: string
|
||||
content: [ToolContent1, ...Array<ToolContent1>]
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
executed: boolean
|
||||
@@ -1849,7 +1850,7 @@ export type SessionToolFailed = {
|
||||
data: {
|
||||
sessionID: string
|
||||
assistantMessageID: string
|
||||
callID: string
|
||||
id: string
|
||||
error: SessionStructuredError
|
||||
content?: [ToolContent1, ...Array<ToolContent1>]
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
@@ -4472,7 +4473,7 @@ export type PermissionCreateInput = {
|
||||
readonly resources: ReadonlyArray<string>
|
||||
readonly save?: ReadonlyArray<string>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
||||
readonly agent?: string | null
|
||||
}["id"]
|
||||
readonly action: {
|
||||
@@ -4481,7 +4482,7 @@ export type PermissionCreateInput = {
|
||||
readonly resources: ReadonlyArray<string>
|
||||
readonly save?: ReadonlyArray<string>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
||||
readonly agent?: string | null
|
||||
}["action"]
|
||||
readonly resources: {
|
||||
@@ -4490,7 +4491,7 @@ export type PermissionCreateInput = {
|
||||
readonly resources: ReadonlyArray<string>
|
||||
readonly save?: ReadonlyArray<string>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
||||
readonly agent?: string | null
|
||||
}["resources"]
|
||||
readonly save?: {
|
||||
@@ -4499,7 +4500,7 @@ export type PermissionCreateInput = {
|
||||
readonly resources: ReadonlyArray<string>
|
||||
readonly save?: ReadonlyArray<string>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
||||
readonly agent?: string | null
|
||||
}["save"]
|
||||
readonly metadata?: {
|
||||
@@ -4508,7 +4509,7 @@ export type PermissionCreateInput = {
|
||||
readonly resources: ReadonlyArray<string>
|
||||
readonly save?: ReadonlyArray<string>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
||||
readonly agent?: string | null
|
||||
}["metadata"]
|
||||
readonly source?: {
|
||||
@@ -4517,7 +4518,7 @@ export type PermissionCreateInput = {
|
||||
readonly resources: ReadonlyArray<string>
|
||||
readonly save?: ReadonlyArray<string>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
||||
readonly agent?: string | null
|
||||
}["source"]
|
||||
readonly agent?: {
|
||||
@@ -4526,7 +4527,7 @@ export type PermissionCreateInput = {
|
||||
readonly resources: ReadonlyArray<string>
|
||||
readonly save?: ReadonlyArray<string>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
|
||||
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
||||
readonly agent?: string | null
|
||||
}["agent"]
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
let announced = false
|
||||
let lastSpawn = 0
|
||||
let spawnDelay = 5_000
|
||||
let ownerHeld = false
|
||||
|
||||
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) => {
|
||||
if (announced) return
|
||||
@@ -65,7 +64,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
const registration = await registered(options.file, true)
|
||||
|
||||
if (registration.service !== undefined) {
|
||||
ownerHeld = false
|
||||
spawnDelay = 5_000
|
||||
const service = registration.service
|
||||
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
|
||||
@@ -78,14 +76,13 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
}
|
||||
} else {
|
||||
if (lastSpawn === 0 && registration.info !== undefined) lastSpawn = Date.now()
|
||||
const failure = [...contenders].map(contenderFailure).find((error) => error !== undefined)
|
||||
if (failure !== undefined) throw failure
|
||||
const finished = [...contenders].filter(contenderFinished)
|
||||
const failure = finished.map(contenderFailure).find((error) => error !== undefined)
|
||||
if (finished.some((item) => item.child.exitCode === 0)) {
|
||||
ownerHeld = true
|
||||
spawnDelay = Math.min(spawnDelay * 2, 30_000)
|
||||
}
|
||||
finished.forEach((item) => contenders.delete(item))
|
||||
if (failure !== undefined && contenders.size === 0) throw failure
|
||||
// Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
|
||||
if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
|
||||
announce("missing")
|
||||
|
||||
@@ -9,14 +9,20 @@ if (mode === "record-start") {
|
||||
}
|
||||
if (mode === "signal") process.kill(process.pid, process.platform === "win32" ? "SIGTERM" : "SIGKILL")
|
||||
|
||||
if (mode === "delayed" || mode === "delayed-failed" || mode === "coordinated") {
|
||||
if (
|
||||
mode === "delayed" ||
|
||||
mode === "delayed-failed" ||
|
||||
mode === "coordinated" ||
|
||||
mode === "coordinated-failed-loser"
|
||||
) {
|
||||
await appendFile(registration + ".starts", process.pid + "\n")
|
||||
const owner = await writeFile(registration + ".owner", String(process.pid), { flag: "wx" })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (!owner) process.exit()
|
||||
if (mode === "coordinated") {
|
||||
if (!owner) process.exit(mode === "coordinated-failed-loser" ? 1 : 0)
|
||||
if (mode === "coordinated" || mode === "coordinated-failed-loser") {
|
||||
while ((await Bun.file(registration + ".starts").text()).trim().split("\n").length < 2) await Bun.sleep(10)
|
||||
if (mode === "coordinated-failed-loser") await Bun.sleep(1_500)
|
||||
} else await Bun.sleep(Number(delay))
|
||||
if (mode === "delayed-failed") process.exit(1)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@ test("ensures a missing service with native promises", async () => {
|
||||
}
|
||||
}, 15_000)
|
||||
|
||||
test("waits for a live contender when another native contender fails", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
|
||||
const endpoint = await Service.ensure({
|
||||
file: registration,
|
||||
version: "test",
|
||||
command: [process.execPath, fixture, registration, "coordinated-failed-loser"],
|
||||
})
|
||||
const info = await Bun.file(registration).json()
|
||||
try {
|
||||
expect(endpoint.url).toBe(info.url)
|
||||
} finally {
|
||||
process.kill(info.pid, "SIGTERM")
|
||||
await waitForExit(info.pid)
|
||||
}
|
||||
}, 15_000)
|
||||
|
||||
test("reports a failed registered service", async () => {
|
||||
const registration = await setup("failed-owner")
|
||||
|
||||
|
||||
@@ -141,6 +141,24 @@ test("waits for a slow winner while bounding lock probes", async () => {
|
||||
}
|
||||
}, 15_000)
|
||||
|
||||
test("waits for a live contender when another contender fails", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
const endpoint = await run(
|
||||
Service.ensure({
|
||||
file: registration,
|
||||
version: "test",
|
||||
command: [process.execPath, fixture, registration, "coordinated-failed-loser"],
|
||||
}),
|
||||
)
|
||||
const info = await Bun.file(registration).json()
|
||||
try {
|
||||
expect(endpoint.url).toBe(info.url)
|
||||
} finally {
|
||||
process.kill(info.pid, "SIGTERM")
|
||||
}
|
||||
}, 15_000)
|
||||
|
||||
test("reports a contender that fails to start", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
|
||||
@@ -2,6 +2,7 @@ export * as SessionCompaction from "./compaction"
|
||||
|
||||
import { LLM, LLMClient, AIError, LLMEvent, Message, type LLMRequest, type LanguageModel } from "@opencode-ai/ai"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Context, Effect, Layer, Stream } from "effect"
|
||||
import { Config } from "../config"
|
||||
import { Bus } from "../bus"
|
||||
@@ -19,9 +20,10 @@ import type { Info } from "../model"
|
||||
import { SessionUsage } from "./usage"
|
||||
|
||||
const DEFAULT_BUFFER = 20_000
|
||||
const DEFAULT_KEEP_TOKENS = 8_000
|
||||
const DEFAULT_KEEP_TOKENS = 15_000
|
||||
const OUTPUT_TOKEN_MAX = 32_000
|
||||
const TOOL_OUTPUT_MAX_CHARS = 2_000
|
||||
const MEDIA_TOKEN_ESTIMATE = 1_500
|
||||
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <template> and keep the section order unchanged. Do not include the <template> tags in your response.
|
||||
<template>
|
||||
## Objective
|
||||
@@ -90,6 +92,7 @@ type Plan = {
|
||||
readonly reason: SessionMessage.Compaction["reason"]
|
||||
readonly prompt: string
|
||||
readonly recent: string
|
||||
readonly media: readonly Tool.FileContent[]
|
||||
readonly inputID?: SessionMessage.ID
|
||||
}
|
||||
|
||||
@@ -108,6 +111,15 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Se
|
||||
const truncate = (value: string) =>
|
||||
value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]`
|
||||
|
||||
const isMedia = (mime: string) => {
|
||||
const value = mime.toLowerCase()
|
||||
return (
|
||||
value.startsWith("image/") ||
|
||||
value.startsWith("audio/") ||
|
||||
value.startsWith("video/") ||
|
||||
value === "application/pdf"
|
||||
)
|
||||
}
|
||||
export const serializeToolContent = (content: SessionMessage.ToolStateCompleted["content"]) =>
|
||||
content
|
||||
.map((item) =>
|
||||
@@ -115,6 +127,24 @@ export const serializeToolContent = (content: SessionMessage.ToolStateCompleted[
|
||||
)
|
||||
.join("\n")
|
||||
|
||||
const isEstimatedMedia = (mime: string) =>
|
||||
mime.toLowerCase().startsWith("image/") || mime.toLowerCase() === "application/pdf"
|
||||
|
||||
export const estimateMediaTokens = (message: SessionMessage.Info) => {
|
||||
if (message.type === "user")
|
||||
return (message.files?.filter((file) => isEstimatedMedia(file.mime)).length ?? 0) * MEDIA_TOKEN_ESTIMATE
|
||||
if (message.type !== "assistant") return 0
|
||||
return (
|
||||
message.content
|
||||
.flatMap((part) =>
|
||||
part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")
|
||||
? (part.state.content ?? [])
|
||||
: [],
|
||||
)
|
||||
.filter((content) => content.type === "file" && isEstimatedMedia(content.mime)).length * MEDIA_TOKEN_ESTIMATE
|
||||
)
|
||||
}
|
||||
|
||||
const serialize = (message: SessionMessage.Info) => {
|
||||
if (message.type === "user") {
|
||||
const files =
|
||||
@@ -162,7 +192,11 @@ const settings = (documents: readonly Config.Entry[]) => {
|
||||
const select = (
|
||||
messages: readonly SessionMessage.Info[],
|
||||
tokens: number,
|
||||
): { readonly head: string; readonly recent: string } | undefined => {
|
||||
): {
|
||||
readonly head: string
|
||||
readonly recent: string
|
||||
readonly media: readonly Tool.FileContent[]
|
||||
} | undefined => {
|
||||
const conversation = messages
|
||||
.filter((message) => message.type !== "compaction" && message.type !== "system")
|
||||
.flatMap((message) => {
|
||||
@@ -173,7 +207,7 @@ const select = (
|
||||
let total = 0
|
||||
let split = conversation.length
|
||||
for (let index = conversation.length - 1; index >= 0; index--) {
|
||||
const next = total + Token.estimate(conversation[index].text)
|
||||
const next = total + Token.estimate(conversation[index].text) + estimateMediaTokens(conversation[index].message)
|
||||
if (split < conversation.length && next > tokens) break
|
||||
total = next
|
||||
split = index
|
||||
@@ -183,15 +217,33 @@ const select = (
|
||||
const latestUser = conversation.findLastIndex((item) => item.message.type === "user")
|
||||
if (latestUser > 0) split = latestUser
|
||||
}
|
||||
const tail = conversation.slice(split)
|
||||
return {
|
||||
head: conversation
|
||||
.slice(0, split)
|
||||
.map((item) => item.text)
|
||||
.join("\n\n"),
|
||||
recent: conversation
|
||||
.slice(split)
|
||||
.map((item) => item.text)
|
||||
.join("\n\n"),
|
||||
recent: tail.map((item) => item.text).join("\n\n"),
|
||||
media: tail.flatMap((item) => {
|
||||
if (item.message.type === "user")
|
||||
return (
|
||||
item.message.files
|
||||
?.filter((file) => isMedia(file.mime))
|
||||
.map((file) => ({
|
||||
type: "file" as const,
|
||||
uri: `data:${file.mime};base64,${file.data}`,
|
||||
mime: file.mime,
|
||||
name: file.name,
|
||||
})) ?? []
|
||||
)
|
||||
if (item.message.type !== "assistant") return []
|
||||
return item.message.content.flatMap((part) => {
|
||||
if (part.type !== "tool" || (part.state.status !== "completed" && part.state.status !== "error")) return []
|
||||
return (part.state.content ?? []).flatMap((content) =>
|
||||
content.type === "file" && isMedia(content.mime) ? [content] : [],
|
||||
)
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +271,7 @@ const planContent = (messages: readonly SessionMessage.Info[], tokens: number) =
|
||||
context: summarizeRecent ? [selected.recent] : [previousRecent, selected.head].filter(Boolean),
|
||||
}),
|
||||
recent: summarizeRecent ? "" : selected.recent,
|
||||
media: summarizeRecent ? [] : selected.media,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +371,7 @@ const make = (dependencies: Dependencies) => {
|
||||
reason: plan.reason,
|
||||
text: summary,
|
||||
recent: plan.recent,
|
||||
media: plan.media,
|
||||
})
|
||||
return { status: "completed" as const }
|
||||
})
|
||||
|
||||
@@ -111,9 +111,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
type DraftText = WritableDraft<SessionMessage.AssistantText>
|
||||
type DraftReasoning = WritableDraft<SessionMessage.AssistantReasoning>
|
||||
|
||||
const latestTool = (assistant: DraftAssistant | undefined, callID?: string) =>
|
||||
const latestTool = (assistant: DraftAssistant | undefined, id?: string) =>
|
||||
assistant?.content.findLast(
|
||||
(item): item is DraftTool => item.type === "tool" && (callID === undefined || item.id === callID),
|
||||
(item): item is DraftTool => item.type === "tool" && (id === undefined || item.id === id),
|
||||
)
|
||||
|
||||
const latestText = (assistant: DraftAssistant | undefined) =>
|
||||
@@ -331,7 +331,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
castDraft(
|
||||
SessionMessage.AssistantTool.make({
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: event.data.name,
|
||||
time: { created: event.created },
|
||||
state: SessionMessage.ToolStateStreaming.make({ status: "streaming", input: "" }),
|
||||
@@ -343,13 +343,13 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
"session.tool.input.delta": () => Effect.void,
|
||||
"session.tool.input.ended": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
const match = latestTool(draft, event.data.id)
|
||||
if (match && match.state.status === "streaming") match.state.input = event.data.text
|
||||
})
|
||||
},
|
||||
"session.tool.called": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
const match = latestTool(draft, event.data.id)
|
||||
if (match) {
|
||||
match.executed = event.data.executed
|
||||
match.providerState = event.data.state
|
||||
@@ -366,7 +366,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
},
|
||||
"session.tool.progress": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
const match = latestTool(draft, event.data.id)
|
||||
if (match && match.state.status === "running") {
|
||||
match.state.metadata = event.data.metadata
|
||||
}
|
||||
@@ -376,7 +376,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
// never reaches into ephemeral progress history.
|
||||
"session.tool.success": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
const match = latestTool(draft, event.data.id)
|
||||
if (match && match.state.status === "running") {
|
||||
match.executed = event.data.executed || match.executed === true
|
||||
match.providerResultState = event.data.resultState
|
||||
@@ -394,7 +394,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
},
|
||||
"session.tool.failed": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
const match = latestTool(draft, event.data.id)
|
||||
if (match && (match.state.status === "streaming" || match.state.status === "running")) {
|
||||
match.executed = event.data.executed || match.executed === true
|
||||
match.providerResultState = event.data.resultState
|
||||
@@ -480,6 +480,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
reason: event.data.reason,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
media: event.data.media,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -492,6 +493,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
reason: event.data.reason,
|
||||
summary: event.data.text,
|
||||
recent: event.data.recent,
|
||||
media: event.data.media,
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -488,7 +488,7 @@ const layer = Layer.effect(
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID,
|
||||
assistantMessageID: message.id,
|
||||
callID: tool.id,
|
||||
id: tool.id,
|
||||
error: { type: "aborted", message: `Tool execution interrupted: ${tool.name}` },
|
||||
executed: tool.executed === true,
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ const hostedContent = (result: ToolResultValue): NonEmptyContent => {
|
||||
* between a check (`tool.settled`, `stepStarted`, ...) and its mark, so check-and-mark
|
||||
* stays atomic under cooperative scheduling. (2) Never require a cross-source event
|
||||
* order: each publishing fiber is sequential, so per-source order holds by construction,
|
||||
* and consumers fold by callID/ordinal rather than global position.
|
||||
* and consumers fold by id/ordinal rather than global position.
|
||||
*/
|
||||
export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, input: Input) => {
|
||||
const tools = new Map<
|
||||
@@ -188,14 +188,14 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
}),
|
||||
true,
|
||||
)
|
||||
const toolInput = fragments("tool input", (callID, value) =>
|
||||
const toolInput = fragments("tool input", (id, value) =>
|
||||
Effect.gen(function* () {
|
||||
const tool = tools.get(callID)
|
||||
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${callID}`))
|
||||
const tool = tools.get(id)
|
||||
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${id}`))
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
id,
|
||||
text: value,
|
||||
})
|
||||
}),
|
||||
@@ -225,7 +225,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID,
|
||||
callID: event.id,
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
})
|
||||
})
|
||||
@@ -258,7 +258,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
id: event.id,
|
||||
error: {
|
||||
type: "tool.input-json",
|
||||
message: "Tool call arguments were malformed JSON and were not executed. Retry with valid JSON.",
|
||||
@@ -272,14 +272,14 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
yield* flushFragments()
|
||||
})
|
||||
|
||||
const failTool = Effect.fnUntraced(function* (callID: string, error: SessionError.Error) {
|
||||
const tool = tools.get(callID)
|
||||
const failTool = Effect.fnUntraced(function* (id: string, error: SessionError.Error) {
|
||||
const tool = tools.get(id)
|
||||
if (!tool || tool.settled) return false
|
||||
tool.settled = true
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
id,
|
||||
error,
|
||||
...failureSnapshot(tool),
|
||||
executed: tool.providerExecuted,
|
||||
@@ -289,10 +289,10 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
|
||||
const failTools = Effect.fnUntraced(function* (error: SessionError.Error, mode: "all" | "hosted" | "uncalled") {
|
||||
let failed = false
|
||||
for (const [callID, tool] of tools) {
|
||||
for (const [id, tool] of tools) {
|
||||
if (tool.settled || (mode === "hosted" && !tool.providerExecuted) || (mode === "uncalled" && tool.called))
|
||||
continue
|
||||
failed = (yield* failTool(callID, error)) || failed
|
||||
failed = (yield* failTool(id, error)) || failed
|
||||
}
|
||||
return failed
|
||||
})
|
||||
@@ -328,9 +328,9 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
return yield* failTools(error, scope)
|
||||
})
|
||||
|
||||
const assistantMessageIDForTool = (callID: string) => {
|
||||
const tool = tools.get(callID)
|
||||
return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(new Error(`Unknown tool call: ${callID}`))
|
||||
const assistantMessageIDForTool = (id: string) => {
|
||||
const tool = tools.get(id)
|
||||
return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(new Error(`Unknown tool call: ${id}`))
|
||||
}
|
||||
|
||||
const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (event: LLMEvent) {
|
||||
@@ -399,7 +399,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
id: event.id,
|
||||
delta: event.text,
|
||||
})
|
||||
return
|
||||
@@ -424,7 +424,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
yield* bus.publish(SessionEvent.Tool.Called, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
id: event.id,
|
||||
input: asRecord(event.input),
|
||||
executed: tool.providerExecuted,
|
||||
state: providerState(event.providerMetadata),
|
||||
@@ -450,7 +450,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
id: event.id,
|
||||
error: { type: "tool.execution", message: stringify(event.result.value) },
|
||||
...failureSnapshot(tool),
|
||||
executed,
|
||||
@@ -461,7 +461,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
yield* bus.publish(SessionEvent.Tool.Success, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
id: event.id,
|
||||
content: hostedContent(event.result),
|
||||
executed,
|
||||
resultState,
|
||||
@@ -478,7 +478,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
id: event.id,
|
||||
error:
|
||||
event.message === `Unknown tool: ${event.name}`
|
||||
? { type: "tool.unknown", message: event.message }
|
||||
@@ -508,30 +508,30 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
}
|
||||
})
|
||||
|
||||
const progress = Effect.fnUntraced(function* (callID: string, update: Tool.Metadata) {
|
||||
const tool = tools.get(callID)
|
||||
const progress = Effect.fnUntraced(function* (id: string, update: Tool.Metadata) {
|
||||
const tool = tools.get(id)
|
||||
if (!tool?.called || tool.settled)
|
||||
return yield* Effect.die(new Error(`Tool progress outside running call: ${callID}`))
|
||||
return yield* Effect.die(new Error(`Tool progress outside running call: ${id}`))
|
||||
tool.progress = update
|
||||
yield* bus.publish(SessionEvent.Tool.Progress, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
id,
|
||||
metadata: update,
|
||||
})
|
||||
})
|
||||
|
||||
/** Publishes one canonical terminal event for a locally executed tool call. */
|
||||
const toolExecution = Effect.fnUntraced(function* (
|
||||
callID: string,
|
||||
id: string,
|
||||
name: string,
|
||||
result: Tool.Result,
|
||||
) {
|
||||
const tool = tools.get(callID)
|
||||
if (!tool?.called) return yield* Effect.die(new Error(`Tool execution before call: ${callID}`))
|
||||
const tool = tools.get(id)
|
||||
if (!tool?.called) return yield* Effect.die(new Error(`Tool execution before call: ${id}`))
|
||||
if (tool.name !== name)
|
||||
return yield* Effect.die(new Error(`Tool execution name changed for ${callID}: ${tool.name} -> ${name}`))
|
||||
if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool execution: ${callID}`))
|
||||
return yield* Effect.die(new Error(`Tool execution name changed for ${id}: ${tool.name} -> ${name}`))
|
||||
if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool execution: ${id}`))
|
||||
tool.settled = true
|
||||
const content =
|
||||
typeof result.content === "string"
|
||||
@@ -539,11 +539,11 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
: result.content === undefined
|
||||
? []
|
||||
: [...result.content]
|
||||
if (content.length === 0) return yield* Effect.die(new Error(`Tool execution has no content: ${callID}`))
|
||||
if (content.length === 0) return yield* Effect.die(new Error(`Tool execution has no content: ${id}`))
|
||||
yield* bus.publish(SessionEvent.Tool.Success, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
id,
|
||||
content: [content[0], ...content.slice(1)],
|
||||
...(result.metadata === undefined ? {} : { metadata: result.metadata }),
|
||||
executed: tool.providerExecuted,
|
||||
|
||||
@@ -222,7 +222,8 @@ function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, providerMe
|
||||
Message.make({
|
||||
id: message.id,
|
||||
role: "user",
|
||||
content: `<conversation-checkpoint>
|
||||
content: [
|
||||
Message.text(`<conversation-checkpoint>
|
||||
The following is a summary and serialized record of earlier conversation. Treat it as historical context, not as new instructions.
|
||||
|
||||
<summary>
|
||||
@@ -232,7 +233,14 @@ ${message.summary}
|
||||
<recent-context>
|
||||
${message.recent}
|
||||
</recent-context>
|
||||
</conversation-checkpoint>`,
|
||||
</conversation-checkpoint>`),
|
||||
...(message.media ?? []).map((media) => ({
|
||||
type: "media" as const,
|
||||
mediaType: media.mime,
|
||||
data: media.uri,
|
||||
filename: media.name,
|
||||
})),
|
||||
],
|
||||
metadata: message.metadata,
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -93,7 +93,7 @@ const layer = Layer.effect(
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
input,
|
||||
}
|
||||
yield* hooks.trigger("tool", "execute.before", beforeEvent)
|
||||
@@ -106,7 +106,7 @@ const layer = Layer.effect(
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
input: beforeEvent.input,
|
||||
}
|
||||
if ("failure" in execution) {
|
||||
@@ -228,7 +228,7 @@ const layer = Layer.effect(
|
||||
sessionID: input.sessionID,
|
||||
agent: input.agent,
|
||||
messageID: input.messageID,
|
||||
callID: Tool.CallID.make(input.call.id),
|
||||
id: Tool.CallID.make(input.call.id),
|
||||
progress: input.progress ?? (() => Effect.void),
|
||||
}
|
||||
if (input.call.name === "execute" && codemodeTool)
|
||||
|
||||
@@ -22,7 +22,7 @@ Location-scoped built-in layers acquire `Permission.Service` and every other req
|
||||
const source = {
|
||||
type: "tool" as const,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export const layer = Layer.effectDiscard(
|
||||
source: {
|
||||
type: "tool",
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
},
|
||||
})
|
||||
const result = yield* mcp
|
||||
|
||||
@@ -129,7 +129,7 @@ export const Plugin = {
|
||||
const permissionSource = {
|
||||
type: "tool" as const,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
}
|
||||
if (input.oldString === input.newString) {
|
||||
return yield* new ToolFailure({
|
||||
|
||||
@@ -61,7 +61,7 @@ export const Plugin = {
|
||||
execute: (input, context) =>
|
||||
Effect.gen(function* () {
|
||||
const searchPath = input.path === "undefined" || input.path === "null" ? undefined : input.path
|
||||
const source = { type: "tool" as const, messageID: context.messageID, callID: context.callID }
|
||||
const source = { type: "tool" as const, messageID: context.messageID, id: context.id }
|
||||
const target = yield* mutation.resolve({ path: searchPath ?? ".", kind: "directory" })
|
||||
const external = target.externalDirectory
|
||||
if (external)
|
||||
|
||||
@@ -76,7 +76,7 @@ export const Plugin = {
|
||||
output: Output,
|
||||
execute: (input, context) =>
|
||||
Effect.gen(function* () {
|
||||
const source = { type: "tool" as const, messageID: context.messageID, callID: context.callID }
|
||||
const source = { type: "tool" as const, messageID: context.messageID, id: context.id }
|
||||
const target = yield* mutation.resolve({ path: input.path ?? "." })
|
||||
if (target.externalDirectory)
|
||||
yield* permission.assert({
|
||||
|
||||
@@ -95,7 +95,7 @@ export const Plugin = {
|
||||
const source = {
|
||||
type: "tool" as const,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
}
|
||||
if (!input.patchText) return yield* new ToolFailure({ message: "patchText is required" })
|
||||
const hunks = yield* Effect.fromResult(Patch.parse(input.patchText)).pipe(
|
||||
|
||||
@@ -70,7 +70,7 @@ export const Plugin = {
|
||||
resources: ["*"],
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
source: { type: "tool", messageID: context.messageID, id: context.id },
|
||||
})
|
||||
.pipe(
|
||||
Effect.mapError((error) => new ToolFailure({ message: "Permission denied: question", error })),
|
||||
@@ -81,7 +81,7 @@ export const Plugin = {
|
||||
title: "Questions",
|
||||
metadata: {
|
||||
kind: "question",
|
||||
tool: { messageID: context.messageID, callID: context.callID },
|
||||
tool: { messageID: context.messageID, id: context.id },
|
||||
},
|
||||
fields: [
|
||||
toField(input.questions[0], 0),
|
||||
|
||||
@@ -56,7 +56,7 @@ export const Plugin = {
|
||||
const source = {
|
||||
type: "tool" as const,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
}
|
||||
const target = yield* mutation.resolve({ path: input.path, kind: "directory" })
|
||||
const external = target.externalDirectory
|
||||
|
||||
@@ -89,10 +89,10 @@ export const Plugin = {
|
||||
|
||||
const notifyWhenDone = Effect.fn("ShellTool.notifyWhenDone")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
callID: string,
|
||||
id: string,
|
||||
command: string,
|
||||
) {
|
||||
yield* runtime.job.wait({ id: callID }).pipe(
|
||||
yield* runtime.job.wait({ id: id }).pipe(
|
||||
Effect.flatMap((result) => {
|
||||
const state =
|
||||
result.info?.status === "completed"
|
||||
@@ -111,7 +111,7 @@ export const Plugin = {
|
||||
: "Command cancelled"
|
||||
return runtime.session.synthetic({
|
||||
sessionID,
|
||||
text: `<shell id="${callID}" state="${state}" command="${command}">\n${text}\n</shell>`,
|
||||
text: `<shell id="${id}" state="${state}" command="${command}">\n${text}\n</shell>`,
|
||||
description: command,
|
||||
metadata: { source: "shell", state },
|
||||
})
|
||||
@@ -134,7 +134,7 @@ export const Plugin = {
|
||||
const source = {
|
||||
type: "tool" as const,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
}
|
||||
const timeout = input.background === true ? (input.timeout ?? 0) : (input.timeout ?? DEFAULT_TIMEOUT_MS)
|
||||
let finalTimeout = timeout
|
||||
@@ -227,7 +227,7 @@ export const Plugin = {
|
||||
Effect.onInterrupt(() => shell.remove(info.id).pipe(Effect.ignore)),
|
||||
)
|
||||
const job = yield* runtime.job.start({
|
||||
id: context.callID,
|
||||
id: context.id,
|
||||
type: name,
|
||||
title: info.command,
|
||||
metadata: { sessionID: context.sessionID, shellID: info.id },
|
||||
@@ -236,7 +236,7 @@ export const Plugin = {
|
||||
|
||||
if (input.background === true) {
|
||||
yield* runtime.job.background(job.id)
|
||||
yield* notifyWhenDone(context.sessionID, context.callID, info.command)
|
||||
yield* notifyWhenDone(context.sessionID, context.id, info.command)
|
||||
return {
|
||||
output: BACKGROUND_STARTED,
|
||||
shellID: info.id,
|
||||
@@ -250,7 +250,7 @@ export const Plugin = {
|
||||
)
|
||||
if (result?.type === "backgrounded") {
|
||||
yield* shell.timeout(info.id, 0)
|
||||
yield* notifyWhenDone(context.sessionID, context.callID, info.command)
|
||||
yield* notifyWhenDone(context.sessionID, context.id, info.command)
|
||||
return {
|
||||
output: BACKGROUND_STARTED,
|
||||
shellID: info.id,
|
||||
|
||||
@@ -75,7 +75,7 @@ export const Plugin = {
|
||||
save: [skill.id],
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
source: { type: "tool", messageID: context.messageID, id: context.id },
|
||||
})
|
||||
const directory = path.dirname(skill.location)
|
||||
const files =
|
||||
|
||||
@@ -159,7 +159,7 @@ export const Plugin = {
|
||||
source: {
|
||||
type: "tool",
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
},
|
||||
})
|
||||
.pipe(Effect.mapError((error) => new ToolFailure({ message: `Subagent denied: ${agent.id}`, error })))
|
||||
|
||||
@@ -139,7 +139,7 @@ export const Plugin = {
|
||||
metadata: input,
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
source: { type: "tool", messageID: context.messageID, id: context.id },
|
||||
})
|
||||
|
||||
const { body, contentType } = yield* Effect.gen(function* () {
|
||||
|
||||
@@ -47,7 +47,7 @@ export const Plugin = {
|
||||
metadata: input,
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.messageID, callID: context.callID },
|
||||
source: { type: "tool", messageID: context.messageID, id: context.id },
|
||||
})
|
||||
const result = yield* ctx.websearch.query(input).pipe(
|
||||
Effect.catch((error) => {
|
||||
|
||||
@@ -67,7 +67,7 @@ export const Plugin = {
|
||||
const source = {
|
||||
type: "tool" as const,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
}
|
||||
const target = yield* mutation.resolve({ path: input.path, kind: "file" })
|
||||
const external = target.externalDirectory
|
||||
|
||||
@@ -714,7 +714,7 @@ describe("DatabaseMigration", () => {
|
||||
sql`INSERT INTO event VALUES ('evt_success', 'session.tool.success.1', ${JSON.stringify({
|
||||
sessionID: "ses_test",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_hosted",
|
||||
id: "call_hosted",
|
||||
structured: {},
|
||||
content: [],
|
||||
result: { type: "json", value: [{ url: "https://example.com" }] },
|
||||
@@ -725,7 +725,7 @@ describe("DatabaseMigration", () => {
|
||||
sql`INSERT INTO event VALUES ('evt_failed', 'session.tool.failed.1', ${JSON.stringify({
|
||||
sessionID: "ses_test",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_failed",
|
||||
id: "call_failed",
|
||||
error: { type: "tool.execution", message: "timed out" },
|
||||
metadata: { truncated: false },
|
||||
executed: false,
|
||||
@@ -795,7 +795,7 @@ describe("DatabaseMigration", () => {
|
||||
expect(JSON.parse(event!.data)).toEqual({
|
||||
sessionID: "ses_test",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_hosted",
|
||||
id: "call_hosted",
|
||||
structured: {},
|
||||
content: [],
|
||||
result: { type: "json", value: [{ url: "https://example.com" }] },
|
||||
@@ -806,7 +806,7 @@ describe("DatabaseMigration", () => {
|
||||
expect(JSON.parse(failedEvent!.data)).toEqual({
|
||||
sessionID: "ses_test",
|
||||
assistantMessageID: "msg_tools",
|
||||
callID: "call_failed",
|
||||
id: "call_failed",
|
||||
error: { type: "tool.execution", message: "timed out" },
|
||||
metadata: { truncated: false },
|
||||
executed: false,
|
||||
|
||||
@@ -928,7 +928,7 @@ it.effect("waits for permission before calling an MCP tool", () =>
|
||||
source: {
|
||||
type: "tool",
|
||||
messageID: toolIdentity.messageID,
|
||||
callID: "call_mcp_permission",
|
||||
id: "call_mcp_permission",
|
||||
},
|
||||
})
|
||||
expect(calls).toBe(0)
|
||||
|
||||
@@ -22,6 +22,7 @@ import { App } from "@opencode-ai/core/app"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Base64, FileAttachment } from "@opencode-ai/schema/prompt"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { DateTime, Effect, Fiber, Layer, Schema, Stream } from "effect"
|
||||
import { asc, eq } from "drizzle-orm"
|
||||
@@ -113,6 +114,24 @@ test("compaction describes tool media without embedding base64", () => {
|
||||
expect(serialized).not.toContain(base64)
|
||||
})
|
||||
|
||||
test("compaction estimates media context without counting base64", () => {
|
||||
const image = FileAttachment.make({
|
||||
data: Base64.make("a".repeat(10_000)),
|
||||
mime: "image/png",
|
||||
source: { type: "inline" },
|
||||
name: "image.png",
|
||||
})
|
||||
const message = SessionMessage.User.make({
|
||||
id: SessionMessage.ID.create(),
|
||||
type: "user",
|
||||
text: "Compare these images.",
|
||||
files: [image, image, FileAttachment.make({ ...image, mime: "application/pdf" })],
|
||||
time: { created: DateTime.makeUnsafe(0) },
|
||||
})
|
||||
|
||||
expect(SessionCompaction.estimateMediaTokens(message)).toBe(4_500)
|
||||
})
|
||||
|
||||
test("compaction prompt requires the checkpoint headings in order", () => {
|
||||
const prompt = SessionCompaction.buildPrompt({ context: ["Conversation history"] })
|
||||
expect(prompt.match(/^#{2,3} .+$/gm)).toEqual([
|
||||
@@ -178,7 +197,7 @@ it.effect("auto compaction reserves a buffer below the prompt ceiling", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
it.effect("manual compaction preserves ordered media in the retained tail", () =>
|
||||
Effect.gen(function* () {
|
||||
requests = []
|
||||
const db = (yield* Database.Service).db
|
||||
@@ -190,9 +209,35 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
const userMessage = {
|
||||
id: SessionMessage.ID.create(),
|
||||
type: "user" as const,
|
||||
text: "Manual compaction should include this short conversation.",
|
||||
text: `Manual compaction should include this older conversation. ${"older context ".repeat(4_500)}`,
|
||||
time: { created: DateTime.makeUnsafe(0) },
|
||||
}
|
||||
const recentMessage = SessionMessage.User.make({
|
||||
id: SessionMessage.ID.create(),
|
||||
type: "user",
|
||||
text: "Compare the retained media.",
|
||||
files: [
|
||||
FileAttachment.make({
|
||||
data: Base64.make("aW1hZ2U="),
|
||||
mime: "application/pdf",
|
||||
source: { type: "inline" },
|
||||
name: "prompt.pdf",
|
||||
}),
|
||||
FileAttachment.make({
|
||||
data: Base64.make("aW1hZ2U="),
|
||||
mime: "image/png",
|
||||
source: { type: "inline" },
|
||||
name: "prompt.png",
|
||||
}),
|
||||
],
|
||||
time: { created: DateTime.makeUnsafe(1) },
|
||||
})
|
||||
const latestMessage = SessionMessage.User.make({
|
||||
id: SessionMessage.ID.create(),
|
||||
type: "user",
|
||||
text: "Newest text after the retained media.",
|
||||
time: { created: DateTime.makeUnsafe(2) },
|
||||
})
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||
@@ -228,7 +273,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
expect(
|
||||
yield* compaction.compactManual({
|
||||
session,
|
||||
messages: [userMessage],
|
||||
messages: [userMessage, recentMessage, latestMessage],
|
||||
inputID: SessionMessage.ID.make("msg_manual_compaction"),
|
||||
}),
|
||||
).toEqual({ status: "completed" })
|
||||
@@ -245,9 +290,30 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
|
||||
"x-opencode-client": "opencode",
|
||||
})
|
||||
expect(requests[0]?.generation).toBeUndefined()
|
||||
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
|
||||
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this older conversation.")
|
||||
expect(yield* store.context(sessionID)).toMatchObject([
|
||||
{ type: "compaction", reason: "manual", summary: "manual summary", recent: "" },
|
||||
{
|
||||
type: "compaction",
|
||||
reason: "manual",
|
||||
summary: "manual summary",
|
||||
recent: expect.stringMatching(
|
||||
/\[User\]: Compare the retained media\.\n\[Attached application\/pdf: prompt\.pdf\]\n\[Attached image\/png: prompt\.png\]\n\n\[User\]: Newest text after the retained media\./,
|
||||
),
|
||||
media: [
|
||||
{
|
||||
type: "file",
|
||||
uri: "data:application/pdf;base64,aW1hZ2U=",
|
||||
mime: "application/pdf",
|
||||
name: "prompt.pdf",
|
||||
},
|
||||
{
|
||||
type: "file",
|
||||
uri: "data:image/png;base64,aW1hZ2U=",
|
||||
mime: "image/png",
|
||||
name: "prompt.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
expect(yield* store.get(sessionID)).toMatchObject({
|
||||
cost: 0.0000233,
|
||||
|
||||
@@ -255,19 +255,19 @@ it.effect("generates from fresh settled Session context without durable mutation
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
assistantMessageID: activeAssistant,
|
||||
callID: "active-call",
|
||||
id: "active-call",
|
||||
name: "echo",
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID,
|
||||
assistantMessageID: activeAssistant,
|
||||
callID: "active-call",
|
||||
id: "active-call",
|
||||
text: "{}",
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Tool.Called, {
|
||||
sessionID,
|
||||
assistantMessageID: activeAssistant,
|
||||
callID: "active-call",
|
||||
id: "active-call",
|
||||
input: {},
|
||||
executed: false,
|
||||
})
|
||||
|
||||
@@ -102,7 +102,15 @@ describe("toLLMMessages", () => {
|
||||
status: "completed",
|
||||
reason: "auto",
|
||||
summary: "Earlier work",
|
||||
recent: "Recent work",
|
||||
recent: "Recent work\n[Attached image/png: retained.png]",
|
||||
media: [
|
||||
{
|
||||
type: "file",
|
||||
uri: "data:image/png;base64,aGVsbG8=",
|
||||
mime: "image/png",
|
||||
name: "retained.png",
|
||||
},
|
||||
],
|
||||
time: { created },
|
||||
}),
|
||||
],
|
||||
@@ -142,9 +150,16 @@ Earlier work
|
||||
|
||||
<recent-context>
|
||||
Recent work
|
||||
[Attached image/png: retained.png]
|
||||
</recent-context>
|
||||
</conversation-checkpoint>`,
|
||||
},
|
||||
{
|
||||
type: "media",
|
||||
mediaType: "image/png",
|
||||
data: "data:image/png;base64,aGVsbG8=",
|
||||
filename: "retained.png",
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
@@ -237,7 +237,7 @@ test("success event data can carry provider-executed result state", () => {
|
||||
const decoded = Schema.decodeUnknownSync(SessionEvent.Tool.Success.data)({
|
||||
sessionID,
|
||||
assistantMessageID: SessionMessage.ID.create(),
|
||||
callID: "call-old",
|
||||
id: "call-old",
|
||||
content: [{ type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png" }],
|
||||
executed: true,
|
||||
resultState: {
|
||||
|
||||
@@ -339,7 +339,7 @@ describe("Tool", () => {
|
||||
call: { type: "tool-call", id: "call-context", name: "context", input: {} },
|
||||
})
|
||||
expect(contexts).toEqual([
|
||||
{ sessionID, ...identity, callID: Tool.CallID.make("call-context"), progress: expect.any(Function) },
|
||||
{ sessionID, ...identity, id: Tool.CallID.make("call-context"), progress: expect.any(Function) },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -935,7 +935,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* TestLLM.push(TestLLM.tool("call-location", "location_context", { query: "hello" }), [])
|
||||
const bus = yield* Bus.Service
|
||||
const progressFiber = yield* bus.subscribe(SessionEvent.Tool.Progress).pipe(
|
||||
Stream.filter((event) => event.data.sessionID === sessionID && event.data.callID === "call-location"),
|
||||
Stream.filter((event) => event.data.sessionID === sessionID && event.data.id === "call-location"),
|
||||
Stream.take(1),
|
||||
Stream.runCollect,
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
@@ -949,7 +949,7 @@ describe("SessionRunnerLLM", () => {
|
||||
sessionID,
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: expect.stringMatching(/^msg_/),
|
||||
callID: Tool.CallID.make("call-location"),
|
||||
id: Tool.CallID.make("call-location"),
|
||||
progress: expect.any(Function),
|
||||
},
|
||||
])
|
||||
@@ -2382,7 +2382,7 @@ describe("SessionRunnerLLM", () => {
|
||||
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(messageRoles(requests[1])).toEqual(["user", "assistant", "tool"])
|
||||
expect(authorizations).toMatchObject([{ sessionID, callID: "call-echo" }])
|
||||
expect(authorizations).toMatchObject([{ sessionID, id: "call-echo" }])
|
||||
expect(executions).toEqual(["hello"])
|
||||
const context = yield* session.context(sessionID)
|
||||
expect(context).toMatchObject([
|
||||
@@ -2994,19 +2994,19 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-interrupted",
|
||||
id: "call-interrupted",
|
||||
name: "echo",
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-interrupted",
|
||||
id: "call-interrupted",
|
||||
text: '{"text":"stale"}',
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Tool.Called, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-interrupted",
|
||||
id: "call-interrupted",
|
||||
input: { text: "stale" },
|
||||
executed: false,
|
||||
})
|
||||
@@ -3051,19 +3051,19 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-hosted-interrupted",
|
||||
id: "call-hosted-interrupted",
|
||||
name: "web_search",
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-hosted-interrupted",
|
||||
id: "call-hosted-interrupted",
|
||||
text: '{"query":"stale"}',
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Tool.Called, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-hosted-interrupted",
|
||||
id: "call-hosted-interrupted",
|
||||
input: { query: "stale" },
|
||||
executed: true,
|
||||
state: { itemId: "call-hosted-interrupted" },
|
||||
@@ -3102,7 +3102,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-pending-interrupted",
|
||||
id: "call-pending-interrupted",
|
||||
name: "echo",
|
||||
})
|
||||
requests.length = 0
|
||||
@@ -4120,7 +4120,7 @@ describe("SessionRunnerLLM", () => {
|
||||
{
|
||||
type: "session.tool.failed.2",
|
||||
data: {
|
||||
callID: "call-malformed",
|
||||
id: "call-malformed",
|
||||
error: { type: "provider.invalid-output", message: "Invalid JSON input for tool call echo" },
|
||||
},
|
||||
},
|
||||
@@ -4220,7 +4220,7 @@ describe("SessionRunnerLLM", () => {
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
expect(durable.find((event) => event.type === "session.tool.input.ended.1")?.data).toMatchObject({
|
||||
callID: "call-malformed",
|
||||
id: "call-malformed",
|
||||
text: raw,
|
||||
})
|
||||
}),
|
||||
@@ -4616,13 +4616,13 @@ describe("SessionRunnerLLM", () => {
|
||||
|
||||
const assistant = requireAssistant(yield* session.context(sessionID))
|
||||
const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id)
|
||||
expect(bus.map((event) => ({ type: event.type, callID: event.data.callID }))).toEqual([
|
||||
{ type: "session.step.started.1", callID: undefined },
|
||||
{ type: "session.tool.called.1", callID: "call-local-raw-failure" },
|
||||
{ type: "session.tool.called.1", callID: "call-hosted-raw-failure-pair" },
|
||||
{ type: "session.tool.failed.2", callID: "call-local-raw-failure" },
|
||||
{ type: "session.tool.failed.2", callID: "call-hosted-raw-failure-pair" },
|
||||
{ type: "session.step.failed.1", callID: undefined },
|
||||
expect(bus.map((event) => ({ type: event.type, id: event.data.id }))).toEqual([
|
||||
{ type: "session.step.started.1", id: undefined },
|
||||
{ type: "session.tool.called.1", id: "call-local-raw-failure" },
|
||||
{ type: "session.tool.called.1", id: "call-hosted-raw-failure-pair" },
|
||||
{ type: "session.tool.failed.2", id: "call-local-raw-failure" },
|
||||
{ type: "session.tool.failed.2", id: "call-hosted-raw-failure-pair" },
|
||||
{ type: "session.step.failed.1", id: undefined },
|
||||
])
|
||||
expect(
|
||||
bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"),
|
||||
|
||||
@@ -65,18 +65,18 @@ describe("Tool.Metadata", () => {
|
||||
if (!row) return yield* Effect.die("Missing projected assistant")
|
||||
return Schema.decodeUnknownSync(SessionMessage.Assistant)({ ...row.data, id: row.id, type: row.type })
|
||||
})
|
||||
const start = (callID: string) =>
|
||||
const start = (id: string) =>
|
||||
Effect.gen(function* () {
|
||||
yield* service.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID,
|
||||
id,
|
||||
name: "bash",
|
||||
})
|
||||
yield* service.publish(SessionEvent.Tool.Called, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID,
|
||||
id,
|
||||
input: { command: "pwd" },
|
||||
executed: false,
|
||||
})
|
||||
@@ -90,7 +90,7 @@ describe("Tool.Metadata", () => {
|
||||
const progress = yield* service.publish(SessionEvent.Tool.Progress, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-success",
|
||||
id: "call-success",
|
||||
metadata: { phase: "checkpoint" },
|
||||
})
|
||||
expect((yield* readAssistant).content[0]).toMatchObject({
|
||||
@@ -100,7 +100,7 @@ describe("Tool.Metadata", () => {
|
||||
const success = yield* service.publish(SessionEvent.Tool.Success, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-success",
|
||||
id: "call-success",
|
||||
metadata: { phase: "done" },
|
||||
content: content("complete"),
|
||||
executed: false,
|
||||
@@ -113,13 +113,13 @@ describe("Tool.Metadata", () => {
|
||||
yield* service.publish(SessionEvent.Tool.Progress, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-failed",
|
||||
id: "call-failed",
|
||||
metadata: { phase: "checkpoint" },
|
||||
})
|
||||
const failed = yield* service.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call-failed",
|
||||
id: "call-failed",
|
||||
error: { type: "unknown", message: "boom" },
|
||||
metadata: { phase: "checkpoint" },
|
||||
content: content("before failure"),
|
||||
|
||||
@@ -12,7 +12,7 @@ const context = {
|
||||
sessionID: Session.ID.make("ses_execute"),
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.make("msg_execute"),
|
||||
callID: Tool.CallID.make("call_execute"),
|
||||
id: Tool.CallID.make("call_execute"),
|
||||
progress: () => Effect.void,
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ describe("QuestionTool", () => {
|
||||
expect(capturedInput()).toEqual({
|
||||
sessionID,
|
||||
title: "Questions",
|
||||
metadata: { kind: "question", tool: { messageID: toolIdentity.messageID, callID: "call-question" } },
|
||||
metadata: { kind: "question", tool: { messageID: toolIdentity.messageID, id: "call-question" } },
|
||||
fields: [
|
||||
{
|
||||
key: "q0",
|
||||
@@ -212,7 +212,7 @@ describe("QuestionTool", () => {
|
||||
expect(capturedInput()).toEqual({
|
||||
sessionID,
|
||||
title: "Questions",
|
||||
metadata: { kind: "question", tool: { messageID: toolIdentity.messageID, callID: "call-question" } },
|
||||
metadata: { kind: "question", tool: { messageID: toolIdentity.messageID, id: "call-question" } },
|
||||
fields: [
|
||||
{
|
||||
key: "q0",
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface ToolHooks {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly callID: Tool.CallID
|
||||
readonly id: Tool.CallID
|
||||
input: unknown
|
||||
}
|
||||
readonly "execute.after": {
|
||||
@@ -25,7 +25,7 @@ export interface ToolHooks {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly callID: Tool.CallID
|
||||
readonly id: Tool.CallID
|
||||
readonly input: unknown
|
||||
} & (
|
||||
| {
|
||||
|
||||
@@ -34,7 +34,7 @@ interface ToolHooks {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly callID: Tool.CallID
|
||||
readonly id: Tool.CallID
|
||||
input: unknown
|
||||
}
|
||||
readonly "execute.after": {
|
||||
@@ -42,7 +42,7 @@ interface ToolHooks {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly callID: Tool.CallID
|
||||
readonly id: Tool.CallID
|
||||
readonly input: unknown
|
||||
} & (
|
||||
| {
|
||||
|
||||
@@ -17,7 +17,7 @@ export const Source = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("tool"),
|
||||
messageID: Schema.String,
|
||||
callID: Schema.String,
|
||||
id: Schema.String,
|
||||
}),
|
||||
]).annotate({ identifier: "Permission.Source" })
|
||||
export type Source = typeof Source.Type
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
|
||||
|
||||
export const Tool = Schema.Struct({
|
||||
messageID: Schema.String,
|
||||
callID: Schema.String,
|
||||
id: Schema.String,
|
||||
}).annotate({ identifier: "Question.Tool" })
|
||||
export interface Tool extends Schema.Schema.Type<typeof Tool> {}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
||||
import { optional } from "./schema.js"
|
||||
import { Event } from "./event.js"
|
||||
import { FinishReason } from "./llm.js"
|
||||
import { Content } from "./tool.js"
|
||||
import { Content, FileContent } from "./tool.js"
|
||||
import { Model } from "./model.js"
|
||||
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema.js"
|
||||
import { FileAttachment } from "./prompt.js"
|
||||
@@ -365,7 +365,7 @@ export namespace Tool {
|
||||
const ToolBase = {
|
||||
...Base,
|
||||
assistantMessageID: SessionMessage.ID,
|
||||
callID: Schema.String,
|
||||
id: Schema.String,
|
||||
}
|
||||
|
||||
export namespace Input {
|
||||
@@ -515,6 +515,7 @@ export namespace Compaction {
|
||||
reason: Started.data.fields.reason,
|
||||
text: Schema.String,
|
||||
recent: Schema.String,
|
||||
media: Schema.Array(FileContent).pipe(optional),
|
||||
},
|
||||
})
|
||||
export type Ended = typeof Ended.Type
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as SessionMessage from "./session-message.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { optional } from "./schema.js"
|
||||
import { Content } from "./tool.js"
|
||||
import { Content, FileContent } from "./tool.js"
|
||||
import { Model } from "./model.js"
|
||||
import { Prompt } from "./prompt.js"
|
||||
import { DateTimeUtcFromMillis, PositiveInt, RelativePath, statics } from "./schema.js"
|
||||
@@ -222,6 +222,7 @@ export const CompactionCompleted = Schema.Struct({
|
||||
reason: Schema.Literals(["auto", "manual"]),
|
||||
summary: Schema.String,
|
||||
recent: Schema.String,
|
||||
media: Schema.Array(FileContent).pipe(optional),
|
||||
}).annotate({ identifier: "Session.Message.Compaction.Completed" })
|
||||
|
||||
export interface CompactionFailed extends Schema.Schema.Type<typeof CompactionFailed> {}
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface Context {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly callID: CallID
|
||||
readonly id: CallID
|
||||
readonly progress: (update: Metadata) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ describe("public event manifest", () => {
|
||||
const tool = SessionEvent.Tool.Called.data.make({
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
callID: "call_test",
|
||||
id: "call_test",
|
||||
input: {},
|
||||
executed: true,
|
||||
state: { itemId: "item_test" },
|
||||
|
||||
@@ -477,7 +477,7 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* (
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
messageID: context.messageID,
|
||||
callID: context.callID,
|
||||
id: context.id,
|
||||
},
|
||||
}
|
||||
const pending: PendingToolInvocation = {
|
||||
|
||||
@@ -503,7 +503,7 @@ export namespace Backend {
|
||||
sessionID: Schema.String,
|
||||
agent: Schema.String,
|
||||
messageID: Schema.String,
|
||||
callID: Schema.String,
|
||||
id: Schema.String,
|
||||
}),
|
||||
})
|
||||
export interface ToolInvocation extends Schema.Schema.Type<typeof ToolInvocation> {}
|
||||
|
||||
@@ -279,7 +279,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => {
|
||||
expect.objectContaining({ name: "lookup", description: "Look up a value" }),
|
||||
)
|
||||
const progress: Tool.Metadata[] = []
|
||||
const executeCall = (callID: string, query: string) =>
|
||||
const executeCall = (id: string, query: string) =>
|
||||
toolSet.execute({
|
||||
sessionID: Session.ID.make("ses_simulated_tools"),
|
||||
agent: Agent.ID.make("build"),
|
||||
@@ -287,7 +287,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => {
|
||||
progress: (update) => Effect.sync(() => progress.push(update)),
|
||||
call: {
|
||||
type: "tool-call",
|
||||
id: callID,
|
||||
id: id,
|
||||
name: "lookup",
|
||||
input: { query },
|
||||
},
|
||||
@@ -302,7 +302,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => {
|
||||
sessionID: "ses_simulated_tools",
|
||||
agent: "build",
|
||||
messageID: "msg_simulated_tools",
|
||||
callID: "call_success",
|
||||
id: "call_success",
|
||||
},
|
||||
})
|
||||
const successID = requireString(requireRecord(successInvocation.params).id)
|
||||
@@ -420,25 +420,25 @@ test("controls arbitrary tools through scoped SDK overlays", async () => {
|
||||
invocations.map((invocation) => {
|
||||
const params = requireRecord(invocation.params)
|
||||
const context = requireRecord(params.context)
|
||||
return [requireString(context.callID), requireString(params.id)]
|
||||
return [requireString(context.id), requireString(params.id)]
|
||||
}),
|
||||
)
|
||||
for (const [id, callID, value] of [
|
||||
for (const [requestID, toolID, value] of [
|
||||
[5, "call_second", "second result"],
|
||||
[6, "call_first", "first result"],
|
||||
] as const) {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
id: requestID,
|
||||
method: "tool.finish",
|
||||
params: {
|
||||
id: byCall.get(callID),
|
||||
id: byCall.get(toolID),
|
||||
output: { structured: value, content: [{ type: "text", text: value }] },
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(yield* Queue.take(messages)).toMatchObject({ id, result: { ok: true } })
|
||||
expect(yield* Queue.take(messages)).toMatchObject({ id: requestID, result: { ok: true } })
|
||||
}
|
||||
expect(yield* Fiber.join(concurrent[0])).toMatchObject({
|
||||
output: "first result",
|
||||
|
||||
@@ -208,10 +208,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
const item = messages.findLast((item) => item.type === "compaction" && item.status === "running")
|
||||
return item?.type === "compaction" ? item : undefined
|
||||
},
|
||||
latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
|
||||
latestTool(assistant: SessionMessageAssistant | undefined, id?: string) {
|
||||
return assistant?.content.findLast(
|
||||
(item): item is SessionMessageAssistantTool =>
|
||||
item.type === "tool" && (callID === undefined || item.id === callID),
|
||||
item.type === "tool" && (id === undefined || item.id === id),
|
||||
)
|
||||
},
|
||||
latestText(assistant: SessionMessageAssistant | undefined) {
|
||||
@@ -592,7 +592,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
message.assistant(draft, index, event.data.assistantMessageID)?.content.push({
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: event.data.name,
|
||||
time: { created: event.created },
|
||||
state: { status: "streaming", input: "" },
|
||||
@@ -603,7 +603,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
event.data.id,
|
||||
)
|
||||
if (match?.state.status === "streaming") match.state.input += event.data.delta
|
||||
})
|
||||
@@ -612,7 +612,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
event.data.id,
|
||||
)
|
||||
if (match?.state.status === "streaming") match.state.input = event.data.text
|
||||
})
|
||||
@@ -621,7 +621,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
event.data.id,
|
||||
)
|
||||
if (!match) return
|
||||
match.time.ran = event.created
|
||||
@@ -634,7 +634,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
event.data.id,
|
||||
)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state.metadata = event.data.metadata
|
||||
@@ -644,7 +644,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
event.data.id,
|
||||
)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state = {
|
||||
@@ -662,7 +662,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const match = message.latestTool(
|
||||
message.assistant(draft, index, event.data.assistantMessageID),
|
||||
event.data.callID,
|
||||
event.data.id,
|
||||
)
|
||||
if (!match || (match.state.status !== "streaming" && match.state.status !== "running")) return
|
||||
match.state = {
|
||||
|
||||
@@ -373,7 +373,7 @@ function askPermission(state: State, item: Permit): void {
|
||||
resources: item.patterns,
|
||||
metadata: item.metadata ?? {},
|
||||
save: item.always,
|
||||
source: { type: "tool", messageID: item.ref.msg, callID: item.ref.call },
|
||||
source: { type: "tool", messageID: item.ref.msg, id: item.ref.call },
|
||||
tool,
|
||||
},
|
||||
})
|
||||
@@ -805,7 +805,7 @@ function emitForm(state: State, kind: FormKind = "question"): void {
|
||||
title: form.title,
|
||||
metadata:
|
||||
kind === "question"
|
||||
? { kind: "question", tool: { messageID: ref.msg, callID: ref.call } }
|
||||
? { kind: "question", tool: { messageID: ref.msg, id: ref.call } }
|
||||
: { kind: "mcp", message: `Synthetic ${kind} MCP elicitation` },
|
||||
fields: form.fields,
|
||||
}
|
||||
|
||||
@@ -164,13 +164,13 @@ function text(value: unknown): string | undefined {
|
||||
return next || undefined
|
||||
}
|
||||
|
||||
function sourceKey(messageID: string, callID: string) {
|
||||
return `${messageID}\u0000${callID}`
|
||||
function sourceKey(messageID: string, id: string) {
|
||||
return `${messageID}\u0000${id}`
|
||||
}
|
||||
|
||||
function permissionTool(request: PermissionRequest, tools: Map<string, SessionMessageAssistantTool>) {
|
||||
if (request.source?.type !== "tool") return request
|
||||
const tool = tools.get(sourceKey(request.source.messageID, request.source.callID))
|
||||
const tool = tools.get(sourceKey(request.source.messageID, request.source.id))
|
||||
return tool ? { ...request, tool } : request
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
...new Set(
|
||||
permissions.flatMap((request) => {
|
||||
if (request.source?.type !== "tool") return []
|
||||
const key = sourceKey(request.source.messageID, request.source.callID)
|
||||
const key = sourceKey(request.source.messageID, request.source.id)
|
||||
return child.toolSources.has(key) ? [] : [request.source.messageID]
|
||||
}),
|
||||
),
|
||||
@@ -475,7 +475,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
permissions.some(
|
||||
(request) =>
|
||||
request.source?.type === "tool" &&
|
||||
!child.toolSources.has(sourceKey(request.source.messageID, request.source.callID)),
|
||||
!child.toolSources.has(sourceKey(request.source.messageID, request.source.id)),
|
||||
)
|
||||
)
|
||||
throw new Error("Permission source tool is unavailable")
|
||||
@@ -737,12 +737,12 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.input.started") {
|
||||
if (child.finishedTools.has(sourceKey(event.data.assistantMessageID, event.data.callID))) return
|
||||
if (child.finishedTools.has(sourceKey(event.data.assistantMessageID, event.data.id))) return
|
||||
childTool(
|
||||
child,
|
||||
{
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: event.data.name,
|
||||
state: { status: "streaming", input: "" },
|
||||
time: { created: event.created },
|
||||
@@ -752,7 +752,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.input.delta" || event.type === "session.tool.input.ended") {
|
||||
const current = child.tools.get(sourceKey(event.data.assistantMessageID, event.data.callID))
|
||||
const current = child.tools.get(sourceKey(event.data.assistantMessageID, event.data.id))
|
||||
if (!current || current.part.state.status !== "streaming") return
|
||||
childTool(
|
||||
child,
|
||||
@@ -769,14 +769,14 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.called") {
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.id)
|
||||
if (child.finishedTools.has(key)) return
|
||||
const current = child.tools.get(key)
|
||||
childTool(
|
||||
child,
|
||||
{
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: current?.part.name ?? "tool",
|
||||
executed: event.data.executed,
|
||||
providerState: event.data.state,
|
||||
@@ -790,7 +790,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.progress") {
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.id)
|
||||
if (child.finishedTools.has(key)) return
|
||||
const current = child.tools.get(key)
|
||||
const part = current?.part
|
||||
@@ -798,7 +798,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
child,
|
||||
{
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: part?.name ?? "tool",
|
||||
executed: part?.executed,
|
||||
providerState: part?.providerState,
|
||||
@@ -819,7 +819,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.success" || event.type === "session.tool.failed") {
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.id)
|
||||
if (child.finishedTools.has(key)) return
|
||||
const current = child.tools.get(key)
|
||||
const part = current?.part
|
||||
@@ -828,7 +828,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
child,
|
||||
{
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: part?.name ?? "tool",
|
||||
executed: event.data.executed,
|
||||
providerState: part?.providerState,
|
||||
@@ -947,11 +947,11 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
if (!active(signal)) return
|
||||
if (event.type === "session.tool.input.started") {
|
||||
if (canonicalToolName(event.data.name) === "subagent")
|
||||
pendingCalls.set(sourceKey(event.data.assistantMessageID, event.data.callID), {})
|
||||
pendingCalls.set(sourceKey(event.data.assistantMessageID, event.data.id), {})
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.called") {
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.id)
|
||||
if (pendingCalls.has(key)) pendingCalls.set(key, event.data.input)
|
||||
return
|
||||
}
|
||||
@@ -961,7 +961,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
event.type !== "session.tool.failed"
|
||||
)
|
||||
return
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = sourceKey(event.data.assistantMessageID, event.data.id)
|
||||
const pending = pendingCalls.get(key)
|
||||
if (event.type !== "session.tool.progress") pendingCalls.delete(key)
|
||||
const found = childSessionID(record(event.data.metadata))
|
||||
|
||||
@@ -91,12 +91,12 @@ type Wait = {
|
||||
}
|
||||
|
||||
// One active session.shell call. The HTTP response is the completion signal;
|
||||
// callID correlates the live shell events once shell.started is observed, and
|
||||
// id correlates the live shell events once shell.started is observed, and
|
||||
// abort cancels the blocking request when the user interrupts the turn.
|
||||
type ShellWait = {
|
||||
eventID: string
|
||||
messageID: string
|
||||
callID?: string
|
||||
id?: string
|
||||
resolve: () => void
|
||||
abort: () => void
|
||||
}
|
||||
@@ -291,27 +291,27 @@ function streamPartKey(messageID: string, partID: string) {
|
||||
return `${messageID}\u0000${partID}`
|
||||
}
|
||||
|
||||
function permissionSourceKey(messageID: string, callID: string) {
|
||||
return streamPartKey(messageID, callID)
|
||||
function permissionSourceKey(messageID: string, id: string) {
|
||||
return streamPartKey(messageID, id)
|
||||
}
|
||||
|
||||
function permissionTool(request: PermissionRequest, tools: Map<string, SessionMessageAssistantTool>) {
|
||||
if (request.source?.type !== "tool") return request
|
||||
const tool = tools.get(permissionSourceKey(request.source.messageID, request.source.callID))
|
||||
const tool = tools.get(permissionSourceKey(request.source.messageID, request.source.id))
|
||||
return tool ? { ...request, tool } : request
|
||||
}
|
||||
|
||||
// Direct shell calls use one "start" commit rendering `$ command` and one "progress"
|
||||
// commit rendering the merged output (see toolEntryBody in tool.ts).
|
||||
function shellCommit(
|
||||
callID: string,
|
||||
id: string,
|
||||
command: string,
|
||||
next: Pick<StreamCommit, "text" | "phase" | "toolState" | "toolError">,
|
||||
): StreamCommit {
|
||||
return {
|
||||
kind: "tool",
|
||||
source: "tool",
|
||||
partID: `shell:${callID}`,
|
||||
partID: `shell:${id}`,
|
||||
tool: "shell",
|
||||
shell: { command },
|
||||
...next,
|
||||
@@ -319,7 +319,7 @@ function shellCommit(
|
||||
}
|
||||
|
||||
function shellTerminal(
|
||||
callID: string,
|
||||
id: string,
|
||||
command: string,
|
||||
shell: { status: string; exit?: number | string },
|
||||
output: { output: string; cursor: number; size: number; truncated: boolean },
|
||||
@@ -332,10 +332,10 @@ function shellTerminal(
|
||||
: shell.status === "exited"
|
||||
? `Shell exited with code ${shell.exit ?? "unknown"}`
|
||||
: `Shell ${shell.status}`
|
||||
if (!error) return [shellCommit(callID, command, { text, phase: "progress", toolState: "completed" })]
|
||||
if (!error) return [shellCommit(id, command, { text, phase: "progress", toolState: "completed" })]
|
||||
return [
|
||||
...(text ? [shellCommit(callID, command, { text, phase: "progress", toolState: "running" })] : []),
|
||||
shellCommit(callID, command, { text: error, phase: "final", toolState: "error", toolError: error }),
|
||||
...(text ? [shellCommit(id, command, { text, phase: "progress", toolState: "running" })] : []),
|
||||
shellCommit(id, command, { text: error, phase: "final", toolState: "error", toolError: error }),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -570,7 +570,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
const sourcePending = (key: string) =>
|
||||
state.permissions.some(
|
||||
(request) =>
|
||||
request.source?.type === "tool" && permissionSourceKey(request.source.messageID, request.source.callID) === key,
|
||||
request.source?.type === "tool" && permissionSourceKey(request.source.messageID, request.source.id) === key,
|
||||
)
|
||||
|
||||
const pruneToolSources = () => {
|
||||
@@ -647,7 +647,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
}
|
||||
if (message.type === "shell") {
|
||||
state.shellCommands.set(message.shellID, message.command)
|
||||
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shellID
|
||||
if (state.shellWait?.messageID === message.id) state.shellWait.id = message.shellID
|
||||
const completed = message.time.completed !== undefined
|
||||
if (!render) {
|
||||
// Suppressed history: mark settled shells rendered so live redelivery
|
||||
@@ -673,7 +673,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
state.shellEnded.add(message.shellID)
|
||||
write(shellTerminal(message.shellID, message.command, message, message.output))
|
||||
}
|
||||
if (completed && state.shellWait?.callID === message.shellID) state.shellWait.resolve()
|
||||
if (completed && state.shellWait?.id === message.shellID) state.shellWait.resolve()
|
||||
return
|
||||
}
|
||||
if (message.type === "compaction") {
|
||||
@@ -776,14 +776,14 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
) => {
|
||||
const pending = new Set(
|
||||
permissions.flatMap((request) =>
|
||||
request.source?.type === "tool" ? [permissionSourceKey(request.source.messageID, request.source.callID)] : [],
|
||||
request.source?.type === "tool" ? [permissionSourceKey(request.source.messageID, request.source.id)] : [],
|
||||
),
|
||||
)
|
||||
const messageIDs = [
|
||||
...new Set(
|
||||
permissions.flatMap((request) => {
|
||||
if (request.source?.type !== "tool") return []
|
||||
const key = permissionSourceKey(request.source.messageID, request.source.callID)
|
||||
const key = permissionSourceKey(request.source.messageID, request.source.id)
|
||||
return state.toolSources.has(key) ? [] : [request.source.messageID]
|
||||
}),
|
||||
),
|
||||
@@ -992,7 +992,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
if (event.type === "session.shell.started") {
|
||||
state.shellCommands.set(event.data.shell.id, event.data.shell.command)
|
||||
const wait = state.shellWait
|
||||
if (wait?.eventID === event.id) wait.callID = event.data.shell.id
|
||||
if (wait?.eventID === event.id) wait.id = event.data.shell.id
|
||||
if (state.shellStarted.has(event.data.shell.id)) return
|
||||
state.shellStarted.add(event.data.shell.id)
|
||||
write(
|
||||
@@ -1025,7 +1025,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
commits.push(...shellTerminal(event.data.shell.id, command, event.data.shell, event.data.output))
|
||||
}
|
||||
const wait = state.shellWait
|
||||
const owned = wait?.callID === event.data.shell.id
|
||||
const owned = wait?.id === event.data.shell.id
|
||||
write(commits, owned || state.wait || state.shellWait ? undefined : { phase: "idle", status: "" })
|
||||
if (owned) wait.resolve()
|
||||
return
|
||||
@@ -1109,7 +1109,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
if (event.type === "session.tool.input.started") {
|
||||
renderTool(event.data.assistantMessageID, {
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: event.data.name,
|
||||
state: { status: "streaming", input: "" },
|
||||
time: { created: event.created },
|
||||
@@ -1117,7 +1117,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.input.delta" || event.type === "session.tool.input.ended") {
|
||||
const current = state.tools.get(streamPartKey(event.data.assistantMessageID, event.data.callID))
|
||||
const current = state.tools.get(streamPartKey(event.data.assistantMessageID, event.data.id))
|
||||
if (!current || current.part.state.status !== "streaming") return
|
||||
renderTool(event.data.assistantMessageID, {
|
||||
...current.part,
|
||||
@@ -1130,12 +1130,12 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.called") {
|
||||
const key = streamPartKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = streamPartKey(event.data.assistantMessageID, event.data.id)
|
||||
if (state.finishedTools.has(key)) return
|
||||
const current = state.tools.get(key)
|
||||
const item: SessionMessageAssistantTool = {
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: current?.part.name ?? "tool",
|
||||
executed: event.data.executed,
|
||||
providerState: event.data.state,
|
||||
@@ -1146,13 +1146,13 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.progress") {
|
||||
const key = streamPartKey(event.data.assistantMessageID, event.data.callID)
|
||||
const key = streamPartKey(event.data.assistantMessageID, event.data.id)
|
||||
if (state.finishedTools.has(key)) return
|
||||
const current = state.tools.get(key)
|
||||
const part = current?.part
|
||||
renderTool(event.data.assistantMessageID, {
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: part?.name ?? "tool",
|
||||
executed: part?.executed,
|
||||
providerState: part?.providerState,
|
||||
@@ -1166,12 +1166,12 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.success" || event.type === "session.tool.failed") {
|
||||
const current = state.tools.get(streamPartKey(event.data.assistantMessageID, event.data.callID))
|
||||
const current = state.tools.get(streamPartKey(event.data.assistantMessageID, event.data.id))
|
||||
const part = current?.part
|
||||
const failed = event.type === "session.tool.failed"
|
||||
const item: SessionMessageAssistantTool = {
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
id: event.data.id,
|
||||
name: part?.name ?? "tool",
|
||||
executed: event.data.executed,
|
||||
providerState: part?.providerState,
|
||||
|
||||
@@ -244,11 +244,11 @@ type MiniToolState =
|
||||
// Retained only for the noninteractive run JSON/V1 compatibility boundary.
|
||||
// Interactive Mini commits carry SessionMessageAssistantTool directly.
|
||||
export type MiniToolPart = {
|
||||
id: string
|
||||
partID: string
|
||||
sessionID: string
|
||||
messageID: string
|
||||
type?: "tool"
|
||||
callID: string
|
||||
id: string
|
||||
tool: string
|
||||
state: MiniToolState
|
||||
}
|
||||
|
||||
@@ -2227,7 +2227,7 @@ function useToolPermission(part: () => SessionMessageAssistantTool | undefined)
|
||||
return createMemo(() => {
|
||||
if (local.permission.mode === "auto") return false
|
||||
const request = data.session.permission.list(ctx.sessionID)?.[0]
|
||||
return request?.source?.type === "tool" && request.source.callID === part()?.id
|
||||
return request?.source?.type === "tool" && request.source.id === part()?.id
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
|
||||
if (!tool) return { input: undefined, metadata: undefined }
|
||||
const message = data.session.message.get(props.request.sessionID, tool.messageID)
|
||||
if (message?.type !== "assistant") return { input: undefined, metadata: undefined }
|
||||
const part = message.content.find((part) => part.type === "tool" && part.id === tool.callID)
|
||||
const part = message.content.find((part) => part.type === "tool" && part.id === tool.id)
|
||||
if (part?.type === "tool" && part.state.status !== "streaming") {
|
||||
return { input: part.state.input, metadata: part.state.metadata }
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||
function pendingPermissions() {
|
||||
return new Set(
|
||||
(data.session.permission.list(sessionID()) ?? []).flatMap((request) =>
|
||||
request.source?.type === "tool" ? [request.source.callID] : [],
|
||||
request.source?.type === "tool" ? [request.source.id] : [],
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -255,7 +255,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
||||
data.on("session.tool.input.started", (event) => {
|
||||
if (event.data.sessionID === sessionID())
|
||||
appendPart(
|
||||
{ messageID: event.data.assistantMessageID, partID: event.data.callID },
|
||||
{ messageID: event.data.assistantMessageID, partID: event.data.id },
|
||||
{ type: "tool", name: event.data.name },
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -880,7 +880,7 @@ test("completes exploration when a queued prompt is promoted", async () => {
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: "message-assistant",
|
||||
callID: "call-read",
|
||||
id: "call-read",
|
||||
name: "read",
|
||||
},
|
||||
})
|
||||
@@ -951,7 +951,7 @@ test("classifies live tool rows independently of their call ID", async () => {
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: "message-assistant",
|
||||
callID: "reasoning:0",
|
||||
id: "reasoning:0",
|
||||
name: "bash",
|
||||
},
|
||||
})
|
||||
@@ -2485,7 +2485,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
callID: "call-1",
|
||||
id: "call-1",
|
||||
name: "bash",
|
||||
},
|
||||
})
|
||||
@@ -2497,7 +2497,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
callID: "call-1",
|
||||
id: "call-1",
|
||||
input: {},
|
||||
executed: false,
|
||||
state: { call: true },
|
||||
@@ -2510,7 +2510,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
callID: "call-1",
|
||||
id: "call-1",
|
||||
metadata: { sessionID: "session-child", status: "running" },
|
||||
},
|
||||
})
|
||||
@@ -2533,7 +2533,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
callID: "call-1",
|
||||
id: "call-1",
|
||||
error: { type: "unknown", message: "aborted" },
|
||||
executed: false,
|
||||
resultState: { result: true },
|
||||
|
||||
@@ -89,7 +89,7 @@ describe("run permission shared", () => {
|
||||
permissionInfo(
|
||||
req({
|
||||
action: "shell",
|
||||
source: { type: "tool", messageID: "msg-shell", callID: "call-shell" },
|
||||
source: { type: "tool", messageID: "msg-shell", id: "call-shell" },
|
||||
tool: canonicalToolPart(
|
||||
"shell",
|
||||
{
|
||||
@@ -134,7 +134,7 @@ describe("run permission shared", () => {
|
||||
req({
|
||||
action: "websearch",
|
||||
metadata: { provider: "parallel" },
|
||||
source: { type: "tool", messageID: "msg-search", callID: "call-search" },
|
||||
source: { type: "tool", messageID: "msg-search", id: "call-search" },
|
||||
tool: canonicalToolPart(
|
||||
"websearch",
|
||||
{
|
||||
@@ -157,7 +157,7 @@ describe("run permission shared", () => {
|
||||
const request = req({
|
||||
action: "edit",
|
||||
resources: ["src/index.ts"],
|
||||
source: { type: "tool", messageID: "msg-edit", callID: "call-edit" },
|
||||
source: { type: "tool", messageID: "msg-edit", id: "call-edit" },
|
||||
tool: canonicalToolPart(
|
||||
"edit",
|
||||
{
|
||||
|
||||
@@ -416,7 +416,7 @@ describe("V2 mini transport", () => {
|
||||
sessionID: "ses_child",
|
||||
action: "shell",
|
||||
resources: ["git status --short"],
|
||||
source: { type: "tool", messageID: "msg_child_source", callID: "call_child_source" },
|
||||
source: { type: "tool", messageID: "msg_child_source", id: "call_child_source" },
|
||||
}
|
||||
const client = sdk({
|
||||
streams: [events],
|
||||
@@ -2032,7 +2032,7 @@ describe("V2 mini transport", () => {
|
||||
created: index * 3 + 1,
|
||||
type: "session.tool.input.started",
|
||||
durable: durable("ses_1", index * 3),
|
||||
data: { sessionID: "ses_1", assistantMessageID: messageID, callID: "call_repeated", name: "read" },
|
||||
data: { sessionID: "ses_1", assistantMessageID: messageID, id: "call_repeated", name: "read" },
|
||||
})
|
||||
events.push({
|
||||
id: `evt_repeated_called_${index}`,
|
||||
@@ -2042,7 +2042,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: messageID,
|
||||
callID: "call_repeated",
|
||||
id: "call_repeated",
|
||||
input: { path: `${index + 1}.txt` },
|
||||
executed: true,
|
||||
},
|
||||
@@ -2055,7 +2055,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: messageID,
|
||||
callID: "call_repeated",
|
||||
id: "call_repeated",
|
||||
metadata: {},
|
||||
content: [{ type: "text", text: "" }],
|
||||
executed: true,
|
||||
@@ -2098,7 +2098,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_progress",
|
||||
callID: "call_progress",
|
||||
id: "call_progress",
|
||||
name: "shell",
|
||||
},
|
||||
})
|
||||
@@ -2110,7 +2110,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_progress",
|
||||
callID: "call_progress",
|
||||
id: "call_progress",
|
||||
input: { command: "printf partial && false" },
|
||||
executed: true,
|
||||
},
|
||||
@@ -2122,7 +2122,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_progress",
|
||||
callID: "call_progress",
|
||||
id: "call_progress",
|
||||
metadata: { checkpoint: 1 },
|
||||
},
|
||||
})
|
||||
@@ -2134,7 +2134,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_progress",
|
||||
callID: "call_progress",
|
||||
id: "call_progress",
|
||||
error: { type: "unknown", message: "boom" },
|
||||
metadata: { checkpoint: 1 },
|
||||
content: [{ type: "text", text: "partial" }],
|
||||
@@ -2933,7 +2933,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_failed_subagent",
|
||||
callID: "call_failed_subagent",
|
||||
id: "call_failed_subagent",
|
||||
name: "subagent",
|
||||
},
|
||||
})
|
||||
@@ -2945,7 +2945,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_failed_subagent",
|
||||
callID: "call_failed_subagent",
|
||||
id: "call_failed_subagent",
|
||||
input: { agent: "explore", description: "Inspect failure", prompt: "inspect" },
|
||||
executed: true,
|
||||
},
|
||||
@@ -2958,7 +2958,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_failed_subagent",
|
||||
callID: "call_failed_subagent",
|
||||
id: "call_failed_subagent",
|
||||
error: { type: "unknown", message: "subagent failed" },
|
||||
metadata: { sessionID: "ses_child_failed", status: "running" },
|
||||
executed: true,
|
||||
@@ -2996,7 +2996,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_subagent",
|
||||
callID: "call_subagent",
|
||||
id: "call_subagent",
|
||||
name: "subagent",
|
||||
},
|
||||
})
|
||||
@@ -3008,7 +3008,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_subagent",
|
||||
callID: "call_subagent",
|
||||
id: "call_subagent",
|
||||
input: { agent: "explore", description: "Inspect progress", prompt: "inspect" },
|
||||
executed: true,
|
||||
},
|
||||
@@ -3020,7 +3020,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_subagent",
|
||||
callID: "call_subagent",
|
||||
id: "call_subagent",
|
||||
metadata: { sessionID: "ses_child_progress", status: "running" },
|
||||
},
|
||||
})
|
||||
@@ -3046,7 +3046,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_child_progress",
|
||||
assistantMessageID: "msg_child_tool",
|
||||
callID: "call_child_shell",
|
||||
id: "call_child_shell",
|
||||
name: "shell",
|
||||
},
|
||||
})
|
||||
@@ -3058,7 +3058,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_child_progress",
|
||||
assistantMessageID: "msg_child_tool",
|
||||
callID: "call_child_shell",
|
||||
id: "call_child_shell",
|
||||
input: { command: "printf child && false" },
|
||||
executed: true,
|
||||
},
|
||||
@@ -3070,7 +3070,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_child_progress",
|
||||
assistantMessageID: "msg_child_tool",
|
||||
callID: "call_child_shell",
|
||||
id: "call_child_shell",
|
||||
metadata: { checkpoint: "child" },
|
||||
},
|
||||
})
|
||||
@@ -3083,7 +3083,7 @@ describe("V2 mini transport", () => {
|
||||
sessionID: "ses_child_progress",
|
||||
action: "shell",
|
||||
resources: ["printf child && false"],
|
||||
source: { type: "tool", messageID: "msg_child_tool", callID: "call_child_shell" },
|
||||
source: { type: "tool", messageID: "msg_child_tool", id: "call_child_shell" },
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
@@ -3094,7 +3094,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_child_progress",
|
||||
assistantMessageID: "msg_child_tool",
|
||||
callID: "call_child_shell",
|
||||
id: "call_child_shell",
|
||||
error: { type: "unknown", message: "child boom" },
|
||||
metadata: { checkpoint: "child" },
|
||||
content: [{ type: "text", text: "child partial" }],
|
||||
@@ -3531,24 +3531,24 @@ describe("V2 mini transport", () => {
|
||||
footer: ui.api,
|
||||
})
|
||||
const states = () => ui.events.flatMap((event) => (event.type === "stream.subagent" ? [event.state] : []))
|
||||
const inputStarted = (callID: string, name: string, seq: number) =>
|
||||
const inputStarted = (id: string, name: string, seq: number) =>
|
||||
events.push({
|
||||
id: `evt_started_${callID}`,
|
||||
id: `evt_started_${id}`,
|
||||
created: seq,
|
||||
type: "session.tool.input.started",
|
||||
durable: durable("ses_child", seq),
|
||||
data: { sessionID: "ses_child", assistantMessageID: "msg_tool_projected", callID, name },
|
||||
data: { sessionID: "ses_child", assistantMessageID: "msg_tool_projected", id, name },
|
||||
})
|
||||
const called = (callID: string, input: Record<string, unknown>, seq: number) =>
|
||||
const called = (id: string, input: Record<string, unknown>, seq: number) =>
|
||||
events.push({
|
||||
id: `evt_called_${callID}`,
|
||||
id: `evt_called_${id}`,
|
||||
created: seq,
|
||||
type: "session.tool.called",
|
||||
durable: durable("ses_child", seq),
|
||||
data: {
|
||||
sessionID: "ses_child",
|
||||
assistantMessageID: "msg_tool_projected",
|
||||
callID,
|
||||
id,
|
||||
input,
|
||||
executed: true,
|
||||
},
|
||||
@@ -3567,7 +3567,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_child",
|
||||
assistantMessageID: "msg_tool_projected",
|
||||
callID: "call_terminal",
|
||||
id: "call_terminal",
|
||||
metadata: {},
|
||||
content: [{ type: "text", text: "found" }],
|
||||
executed: true,
|
||||
@@ -3705,7 +3705,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_parent_a",
|
||||
callID: "call_sub",
|
||||
id: "call_sub",
|
||||
name: "subagent",
|
||||
},
|
||||
})
|
||||
@@ -3717,7 +3717,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_parent_a",
|
||||
callID: "call_sub",
|
||||
id: "call_sub",
|
||||
input: { agent: "explore", description: "Find things", prompt: "go", background: true },
|
||||
executed: true,
|
||||
},
|
||||
@@ -3730,7 +3730,7 @@ describe("V2 mini transport", () => {
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_parent_a",
|
||||
callID: "call_sub",
|
||||
id: "call_sub",
|
||||
metadata: { sessionID: "ses_child", status: "running", output: "" },
|
||||
content: [{ type: "text", text: "" }],
|
||||
executed: true,
|
||||
|
||||
@@ -83,7 +83,7 @@ Add `compaction` to any [OpenCode configuration file](/config):
|
||||
"auto": true,
|
||||
"prune": false,
|
||||
"keep": {
|
||||
"tokens": 8000
|
||||
"tokens": 15000
|
||||
},
|
||||
"buffer": 20000
|
||||
}
|
||||
@@ -94,7 +94,7 @@ Add `compaction` to any [OpenCode configuration file](/config):
|
||||
| --- | ---: | --- |
|
||||
| `auto` | `true` | Runs the preflight context-size check. It does not disable manual compaction or one-shot provider-overflow recovery. |
|
||||
| `prune` | None | Accepted by the V2 schema, but currently has no runtime effect. V2 does not prune old tool outputs in place. |
|
||||
| `keep.tokens` | `8000` | Approximate number of tokens from the newest serialized conversation context to retain beside the summary. |
|
||||
| `keep.tokens` | `15000` | Approximate number of tokens from the newest serialized conversation context to retain beside the summary. |
|
||||
| `buffer` | `20000` | Safety reserve below an explicit input limit. Without one, it is the minimum context reserve and the model output allowance wins when larger. |
|
||||
|
||||
`keep.tokens` and `buffer` accept non-negative integers. Larger `keep.tokens`
|
||||
@@ -110,9 +110,12 @@ and relevant files.
|
||||
|
||||
The newest serialized context up to `keep.tokens` is retained separately. This
|
||||
is not a byte-for-byte transcript: tool output is limited to 2000 characters,
|
||||
and file or media attachments become textual descriptors rather than embedded
|
||||
data. On later compactions, V2 updates the previous summary and carries forward
|
||||
its retained recent context before selecting a new tail.
|
||||
and non-media attachments become textual descriptors. Media in the retained
|
||||
context is attached to the checkpoint in the same order as its descriptors.
|
||||
Tail selection budgets 1500 additional tokens per image or PDF as a
|
||||
provider-neutral planning estimate; it does not count base64 request bytes as
|
||||
text tokens. On later compactions, V2 updates the previous summary and carries
|
||||
forward its retained recent context before selecting a new tail.
|
||||
|
||||
The completed compaction is presented to the model as historical conversation
|
||||
context, explicitly not as new instructions. Running and failed compactions are
|
||||
|
||||
+2
-2
@@ -344,8 +344,8 @@ argument to `tools.add` to configure the registration with
|
||||
`execute` CodeMode tool. Set `codemode: false` to expose it directly to the
|
||||
provider.
|
||||
|
||||
The executor receives a second context argument containing `sessionID`,
|
||||
`agent`, `messageID`, `callID`, and `progress`. A tool with `output`
|
||||
The executor receives a second context argument containing `id`, `sessionID`,
|
||||
`agent`, `messageID`, and `progress`. A tool with `output`
|
||||
must return `output`; Effect and Standard Schema codecs validate it, while raw
|
||||
JSON Schema definitions enforce JSON compatibility only. A tool
|
||||
without `output` returns model-visible `content` instead.
|
||||
|
||||
@@ -329,7 +329,7 @@ Control automatic context compaction and how much recent context it preserves.
|
||||
"compaction": {
|
||||
"auto": true,
|
||||
"keep": {
|
||||
"tokens": 8000
|
||||
"tokens": 15000
|
||||
},
|
||||
"buffer": 20000
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user