Compare commits

...
Author SHA1 Message Date
Filip Hejmowski 7b58f08e65 expose compact tool 2026-09-17 15:22:19 +02:00
9 changed files with 535 additions and 1 deletions
+1
View File
@@ -540,6 +540,7 @@ export const make = Effect.fn("PluginHost.make")(function* (
yield* sessions.setPermissions({ sessionID: input.sessionID, permissions: input.permissions })
}),
move: sessions.move,
compact: sessions.compact,
synthetic: sessions.synthetic,
interrupt: (input) =>
sessions
+115
View File
@@ -3,11 +3,41 @@ export * as OpenCodeTools from "./opencode.js"
import { SystemPart, ToolFailure } from "@opencode/ai"
import type { Context } from "@opencode/plugin/effect/plugin"
import type { SessionHooks } from "@opencode/plugin/effect/session"
import { Agent } from "@opencode/schema/agent"
import { Model } from "@opencode/schema/model"
import { AbsolutePath } from "@opencode/schema/schema"
import { Session } from "@opencode/schema/session"
import { SessionMessage } from "@opencode/schema/session-message"
import { TokenUsage } from "@opencode/schema/token-usage"
import { Effect, Schema } from "effect"
export const SessionInfoInput = Schema.Struct({
sessionID: Schema.optionalKey(Session.ID).annotate({ description: "Omit to inspect the current session." }),
})
const SessionInfoOutput = Schema.Struct({
sessionID: Session.ID,
parentID: Schema.NullOr(Session.ID),
title: Schema.NullOr(Schema.String),
directory: AbsolutePath,
agent: Schema.NullOr(Agent.ID),
model: Schema.NullOr(Model.Ref),
limits: Schema.NullOr(Model.Info.fields.limit).annotate({
description: "Model token limits. Null when the model is unavailable in this Location's catalog.",
}),
cost: Session.Info.fields.cost.annotate({ description: "Cumulative session cost in USD." }),
context: Schema.Struct({
tokens: Schema.NullOr(Schema.Finite),
percent: Schema.NullOr(Schema.Finite),
remaining: Schema.NullOr(Schema.Finite),
source: Schema.Literals(["last_completed_step", "unavailable"]),
messageID: Schema.NullOr(SessionMessage.ID),
}).annotate({
description:
"Latest completed step's input (including cache), output, and reasoning tokens; excludes subsequent messages and tool results. Not cumulative usage. Null values mean unknown, including after compaction or a model switch until fresh usage is recorded. Percentage and remaining tokens use limits.context.",
}),
})
export const RenameInput = Schema.Struct({
sessionID: Schema.optionalKey(Session.ID).annotate({ description: "Omit to rename the current session." }),
title: Schema.String.check(Schema.isMinLength(1)).annotate({ description: "New session title." }),
@@ -24,6 +54,12 @@ export const MoveInput = Schema.Struct({
const MoveOutput = Schema.Struct({ sessionID: Session.ID, directory: AbsolutePath })
export const CompactInput = Schema.Struct({
sessionID: Schema.optionalKey(Session.ID).annotate({ description: "Omit to compact the current session." }),
})
const CompactOutput = Schema.Struct({ sessionID: Session.ID, id: SessionMessage.ID })
export const ModelsInput = Schema.Struct({
query: Schema.optionalKey(Schema.String).annotate({
description: "Text to search for in model names and IDs.",
@@ -86,6 +122,65 @@ export const Plugin = {
description:
"Tools for managing OpenCode itself, such as working with sessions and searching the available models.",
})
draft.add({
name: "session_info",
description:
"Get session identity, model limits, cost, and the latest measured context usage. Omit sessionID for the current session. Check this before deciding to compact; low usage generally does not warrant compaction. Usage excludes work since the source message; unavailable does not mean zero.",
input: SessionInfoInput,
output: SessionInfoOutput,
options: { namespace: "opencode", codemode: true, pinned: true },
execute: (input, context) =>
Effect.gen(function* () {
const sessionID = input.sessionID ?? context.sessionID
const session = yield* ctx.session.get({ sessionID })
const messages = yield* ctx.session.context({ sessionID })
const latest = messages.findLast((message) => message.type === "assistant")
const model = session.model ?? latest?.model
const limits =
session.location.directory === ctx.location.directory &&
session.location.workspaceID === ctx.location.workspaceID
? (yield* ctx.model.list()).data.find(
(item) => item.providerID === model?.providerID && item.id === model?.id,
)?.limit
: undefined
const last = messages.findLast(
(message) =>
message.type === "assistant" &&
message.time.completed !== undefined &&
!message.error &&
message.tokens !== undefined &&
message.tokens.input + message.tokens.cache.read + message.tokens.cache.write > 0,
)
const measured =
!session.revert &&
last?.type === "assistant" &&
last.model.providerID === model?.providerID &&
last.model.id === model?.id
? last
: undefined
const tokens = measured?.tokens ? TokenUsage.total(measured.tokens) : null
const capacity = limits && limits.context > 0 ? limits.context : undefined
return {
output: {
sessionID,
parentID: session.parentID ?? null,
title: session.title ?? null,
directory: session.location.directory,
agent: session.agent ?? (sessionID === context.sessionID ? context.agent : latest?.agent) ?? null,
model: model ?? null,
limits: limits ?? null,
cost: session.cost,
context: {
tokens,
percent: tokens !== null && capacity ? Math.round((tokens / capacity) * 1000) / 10 : null,
remaining: tokens !== null && capacity ? Math.max(0, capacity - tokens) : null,
source: tokens === null ? ("unavailable" as const) : ("last_completed_step" as const),
messageID: measured?.id ?? null,
},
},
}
}).pipe(Effect.mapError((error) => new ToolFailure({ message: "Unable to get session info", error }))),
})
draft.add({
name: "session_rename",
description:
@@ -131,6 +226,26 @@ export const Plugin = {
),
),
})
draft.add({
name: "session_compact",
description:
"Request compaction of a session, or omit sessionID to compact the current session. Check session_info first; avoid compaction at low context usage unless the user explicitly requests it. Returns after the request is admitted; compaction runs at the next step boundary, after current tool calls finish.",
input: CompactInput,
output: CompactOutput,
options: { namespace: "opencode", codemode: true, pinned: true },
execute: (input, context) => {
const sessionID = input.sessionID ?? context.sessionID
return ctx.session.compact({ sessionID, delivery: "steer" }).pipe(
Effect.map((request) => ({
output: { sessionID, id: request.id },
content: `Requested compaction of session ${sessionID}. It will run at the next step boundary.`,
})),
Effect.mapError(
(error) => new ToolFailure({ message: `Unable to request compaction of session ${sessionID}`, error }),
),
)
},
})
draft.add({
name: "models",
description:
@@ -0,0 +1,93 @@
import { Plugin } from "@opencode/plugin"
import { Schema } from "effect"
// Public-API-only prototype for evaluating phase-aware compaction plugins.
export const PhaseCompactionPlugin = Plugin.define({
id: "phase-compaction-prototype",
async setup(ctx) {
const options = Schema.decodeUnknownSync(
Schema.Struct({
minimumPercent: Schema.optionalKey(Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 100 }))),
}),
)(ctx.options)
await ctx.session.hook("context", (event) => {
event.system.push({
type: "text",
text: "At a meaningful phase boundary, use tools.phase.checkpoint with the next action and facts to preserve. It checks context usage before requesting compaction. Continue working after the tool returns; do not checkpoint after every small step.",
})
})
await ctx.session.hook("compaction", (event) => {
event.system.push({
type: "text",
text: "When summarizing a phase checkpoint, preserve its next action and essential handoff facts in the checkpoint. Treat completed phases as completed work.",
})
})
await ctx.tool.transform((editor) => {
editor.namespace({ name: "phase", description: "Checkpoint completed phases of a task." })
editor.add({
name: "checkpoint",
description: "Record a phase handoff and request compaction when measured context usage is high enough.",
input: Schema.Struct({
next: Schema.String.check(Schema.isMinLength(1)),
preserve: Schema.Array(Schema.String),
}),
output: Schema.Struct({
status: Schema.Literals(["skipped", "requested"]),
reason: Schema.String,
percent: Schema.NullOr(Schema.Finite),
id: Schema.optionalKey(Schema.String),
next: Schema.String,
preserve: Schema.Array(Schema.String),
}),
options: { namespace: "phase", codemode: true, pinned: true },
async execute(input, tool) {
const session = await ctx.session.get({ sessionID: tool.sessionID })
const messages = await ctx.session.context({ sessionID: tool.sessionID })
const last = messages.findLast(
(message) =>
message.type === "assistant" && message.time.completed !== undefined && !message.error && message.tokens,
)
const selected = session.model ?? (last?.type === "assistant" ? last.model : undefined)
const model = (await ctx.model.list()).data.find(
(model) => model.providerID === selected?.providerID && model.id === selected?.id,
)
const usage =
!session.revert &&
last?.type === "assistant" &&
last.model.providerID === selected?.providerID &&
last.model.id === selected?.id
? last.tokens
: undefined
const percent =
usage && usage.input + usage.cache.read + usage.cache.write > 0 && model && model.limit.context > 0
? ((usage.input + usage.cache.read + usage.cache.write + usage.output + usage.reasoning) /
model.limit.context) *
100
: null
if (percent === null || percent < (options.minimumPercent ?? 50))
return {
output: {
...input,
status: "skipped" as const,
percent,
reason: percent === null ? "Usage unavailable" : "Low context usage",
},
}
const request = await ctx.session.compact({ sessionID: tool.sessionID })
// The handoff remains in the durable tool exchange; the API has no per-request summary prompt.
return {
output: {
...input,
status: "requested" as const,
percent,
id: request.id,
reason: "Phase checkpoint admitted",
},
}
},
})
})
},
})
+1
View File
@@ -171,6 +171,7 @@ export function host(overrides: Overrides = {}): Plugin.Context {
command: overrides.session?.command ?? (() => Effect.die("unused session.command")),
update: overrides.session?.update ?? (() => Effect.die("unused session.update")),
move: overrides.session?.move ?? (() => Effect.die("unused session.move")),
compact: overrides.session?.compact ?? (() => Effect.die("unused session.compact")),
synthetic: overrides.session?.synthetic ?? (() => Effect.die("unused session.synthetic")),
interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")),
wait: overrides.session?.wait ?? (() => Effect.die("unused session.wait")),
+142
View File
@@ -21,6 +21,7 @@ import { AnthropicMessages, OpenAIResponses } from "@opencode/ai/protocols"
import { compileRequest } from "@opencode/ai/route/client"
import { TestLLM } from "@opencode/ai/testing"
import type { SessionHooks } from "@opencode/plugin/effect/session"
import { fromPromise } from "@opencode/plugin/promise/adapter"
import { Database } from "@opencode/core/database/database"
import { makeLocationNode } from "@opencode/util/effect/app-node"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
@@ -57,6 +58,7 @@ import { PluginHooks } from "@opencode/core/plugin/hooks"
import { OptimizePlugin } from "@opencode/core/plugin/optimize"
import { IdentityPlugin } from "@opencode/core/plugin/identity"
import { QuestionTool } from "@opencode/core/tool/plugin/question"
import { OpenCodeTools } from "@opencode/core/tool/plugin/opencode"
import { Agent } from "@opencode/core/agent"
import { Config } from "@opencode/core/config"
import { Document, Info } from "@opencode/schema/config"
@@ -80,7 +82,9 @@ import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, Queue, Schema, Sc
import { TestClock } from "effect/testing"
import { asc, desc, eq, sql } from "drizzle-orm"
import { testEffect } from "./lib/effect"
import { registerToolPlugin } from "./lib/tool"
import { promptLocationNode } from "./fixture/prompt-location"
import { PhaseCompactionPlugin } from "./fixture/phase-compaction-plugin"
import { LocationServiceMap } from "@opencode/core/location-service-map"
import { Expected } from "./lib/session-message"
import { permissionLayer } from "./lib/permission"
@@ -568,6 +572,34 @@ const setup = Effect.gen(function* () {
})
type Scenario = Effect.Success<typeof setup>
const installSessionPlugin = Effect.fn(function* (s: Scenario, plugin: Parameters<typeof fromPromise>[0]) {
const hooks = yield* PluginHooks.Service
const models = yield* Model.Service
const session = yield* s.session.get(sessionID)
const info = new Location.Info({
...session.location,
project: { id: session.projectID, directory: session.location.directory, canonical: session.location.directory },
})
yield* registerToolPlugin(fromPromise(plugin), {
location: info,
model: {
...modelHost(models),
list: () =>
Effect.succeed({
location: info,
data: [Model.Info.default(Provider.ID.make(s.currentModel.provider), Model.ID.make(s.currentModel.id))],
}),
},
session: {
get: (input) => s.session.get(input.sessionID),
context: (input) => s.session.context(input.sessionID),
compact: s.session.compact,
hook: (name, callback) => hooks.register("session", name, callback),
},
})
})
const scenario = (
name: string,
body: (s: Scenario) => Effect.gen.Return<void, unknown, Layer.Success<typeof layer> | Scope.Scope>,
@@ -2437,6 +2469,116 @@ describe("SessionRunnerLLM", () => {
})
})
scenario("lets an agent compact its own session through Code Mode and continue", function* (s) {
s.currentModel = recoveryModel
yield* registerToolPlugin(OpenCodeTools.Plugin, { session: { compact: s.session.compact } })
yield* s.llm.push(
TestLLM.tool("call-self-compact", "execute", { code: "return await tools.opencode.session_compact({})" }),
[LLMEvent.textDelta({ id: "summary", text: "## Objective\n- Continue after self-compaction" })],
TestLLM.text("Continued after compaction", "text-self-compact-continued"),
)
yield* s.runPrompt("Compact this session and continue working")
expect(s.requests).toHaveLength(3)
expect(yield* s.inbox).toEqual([])
const messages = (yield* s.messages).toReversed()
expect(messages.filter((message) => message.type === "assistant" || message.type === "compaction")).toMatchObject([
Expected.assistant({}, [Expected.completedTool({ id: "call-self-compact" }, {})]),
{
type: "compaction",
status: "completed",
reason: "manual",
summary: "## Objective\n- Continue after self-compaction",
},
Expected.assistant({}, [Expected.text("Continued after compaction")]),
])
expect(JSON.stringify(s.requests[2].messages)).toContain("Continue after self-compaction")
})
for (const inputTokens of [undefined, 4_000, 120_000]) {
scenario(`public Promise phase plugin: ${inputTokens ?? "unknown"} input tokens`, function* (s) {
s.currentModel = recoveryModel
yield* s.llm.push(
inputTokens === undefined
? TestLLM.text("Research finished", "phase-research")
: TestLLM.textWithUsage("Research finished", "phase-research", inputTokens),
)
yield* s.runPrompt("Research the change")
yield* installSessionPlugin(s, PhaseCompactionPlugin)
s.requests.length = 0
yield* s.llm.push(
TestLLM.tool("call-phase-checkpoint", "execute", {
code: 'return await tools.phase.checkpoint({ next: "Implement validation", preserve: ["Reuse the existing parser"] })',
}),
...(inputTokens === 120_000 ? [TestLLM.text("## Objective\n- Complete the change", "phase-summary")] : []),
TestLLM.text("Implementation continued", "phase-continued"),
)
yield* s.runPrompt("Proceed to implementation")
expect(s.requests).toHaveLength(inputTokens === 120_000 ? 3 : 2)
expect(s.requests[0].system.some((part) => part.text.includes("At a meaningful phase boundary"))).toBe(true)
const messages = yield* s.messages
const checkpoint = messages.find((message) => message.type === "compaction")
const call = messages
.flatMap((message) => (message.type === "assistant" ? message.content : []))
.find((part) => part.type === "tool" && part.id === "call-phase-checkpoint")
expect(call).toMatchObject(
Expected.completedTool(
{ id: "call-phase-checkpoint" },
{
content: [Expected.text(expect.stringContaining(inputTokens === 120_000 ? '"requested"' : '"skipped"'))],
},
),
)
expect(yield* s.inbox).toEqual([])
if (inputTokens !== 120_000) {
expect(checkpoint).toBeUndefined()
return
}
expect(checkpoint).toMatchObject({ type: "compaction", status: "completed" })
expect(s.requests[1].system.some((part) => part.text.includes("essential handoff facts"))).toBe(true)
expect(JSON.stringify(s.requests[2].messages)).toContain("Implement validation")
expect(JSON.stringify(s.requests[2].messages)).toContain("Reuse the existing parser")
})
}
scenario("compaction requested by a Promise context hook follows the prepared step", function* (s) {
s.currentModel = recoveryModel
yield* s.llm.push(TestLLM.textWithUsage("Research finished", "hook-research", 120_000))
yield* s.runPrompt("Research the change")
let requested = false
yield* installSessionPlugin(s, {
id: "context-compaction-probe",
async setup(ctx) {
await ctx.session.hook("context", async (event) => {
if (requested) return
requested = true
await ctx.session.compact({ sessionID: event.sessionID })
})
},
})
s.requests.length = 0
yield* s.llm.push(
TestLLM.text("The prepared step still ran", "before-hook-checkpoint"),
TestLLM.text("## Objective\n- Continue the task", "hook-checkpoint"),
)
yield* s.runPrompt("Continue the task")
expect(s.requests).toHaveLength(2)
expect(
(yield* s.messages)
.toReversed()
.filter((message) => message.type === "assistant" || message.type === "compaction"),
).toMatchObject([
Expected.assistant({}, [Expected.text("Research finished")]),
Expected.assistant({}, [Expected.text("The prepared step still ran")]),
{ type: "compaction", status: "completed" },
])
})
scenario("preserves provider errors from manual compaction", function* (s) {
yield* s.llm.push(TestLLM.text("Earlier answer", "text-manual-provider-history"))
yield* s.runPrompt("Earlier question")
+180 -1
View File
@@ -1,13 +1,18 @@
import { expect } from "bun:test"
import { Bus } from "@opencode/core/bus"
import { Location } from "@opencode/core/location"
import { Plugin } from "@opencode/core/plugin"
import { PluginHost } from "@opencode/core/plugin/host"
import { Provider } from "@opencode/core/provider"
import { Session } from "@opencode/core/session"
import { SessionEvent } from "@opencode/core/session/event"
import { Tool } from "@opencode/core/tool"
import { OpenCodeTools } from "@opencode/core/tool/plugin/opencode"
import { Model } from "@opencode/schema/model"
import { Effect } from "effect"
import { Money } from "@opencode/schema/money"
import { SessionMessage } from "@opencode/schema/session-message"
import type { TokenUsage } from "@opencode/schema/token-usage"
import { Effect, Schema } from "effect"
import { testEffect } from "./lib/effect"
import { executeTool, toolIdentity } from "./lib/tool"
import { PluginTestLayer } from "./plugin/fixture"
@@ -26,6 +31,116 @@ const gammaOld = {
status: "active",
}
it.live("reports the latest context measurement separately from cumulative session usage", () =>
Effect.gen(function* () {
const fixture = yield* sessionInfoFixture
expect(yield* fixture.get()).toMatchObject({
sessionID: fixture.session.id,
title: "Context info",
directory: fixture.session.location.directory,
agent: "build",
model: fixture.model,
limits: { context: 100_000, input: 80_000, output: 10_000 },
cost: 0,
context: { tokens: null, percent: null, remaining: null, source: "unavailable", messageID: null },
})
yield* fixture.record({ input: 50_000, output: 1_000, reasoning: 0, cache: { read: 0, write: 0 } })
const messageID = yield* fixture.record({
input: 200,
output: 100,
reasoning: 100,
cache: { read: 1_500, write: 100 },
})
// An in-flight step has no settled usage yet; its presence must not turn the reading into zero.
yield* fixture.bus.publish(SessionEvent.Step.Started, {
sessionID: fixture.session.id,
assistantMessageID: SessionMessage.ID.create(),
agent: toolIdentity.agent,
model: fixture.model,
})
expect(yield* fixture.get()).toMatchObject({
cost: 0.2,
context: { tokens: 2_000, percent: 2, remaining: 98_000, source: "last_completed_step", messageID },
})
yield* fixture.catalog.transform((editor) =>
editor.models.update(fixture.model.providerID, fixture.model.id, (model) => {
model.limit.context = 0
}),
)
expect(yield* fixture.get({ sessionID: fixture.session.id })).toMatchObject({
context: { tokens: 2_000, percent: null, remaining: null, source: "last_completed_step", messageID },
})
}),
)
it.live("invalidates context usage after compaction and a model switch", () =>
Effect.gen(function* () {
const fixture = yield* sessionInfoFixture
const tokens = { input: 70_000, output: 100, reasoning: 0, cache: { read: 0, write: 0 } }
yield* fixture.record(tokens)
yield* fixture.bus.publish(SessionEvent.Compaction.Started, {
sessionID: fixture.session.id,
reason: "manual",
recent: "",
})
yield* fixture.bus.publish(SessionEvent.Compaction.Ended, {
sessionID: fixture.session.id,
reason: "manual",
model: fixture.model,
text: "## Objective\n- Keep working",
recent: "",
tokens,
})
expect(yield* fixture.get()).toMatchObject({
context: { tokens: null, percent: null, remaining: null, source: "unavailable", messageID: null },
})
yield* fixture.record({ ...tokens, input: 2_000 })
const other = Model.Ref.make({ ...fixture.model, id: Model.ID.make("other") })
yield* fixture.sessions.switchModel({ sessionID: fixture.session.id, model: other })
expect(yield* fixture.get()).toMatchObject({
model: other,
limits: null,
context: { tokens: null, percent: null, remaining: null, source: "unavailable", messageID: null },
})
yield* fixture.record(tokens, other)
yield* fixture.sessions.switchModel({ sessionID: fixture.session.id, model: fixture.model })
// Switching back must not resurrect an older sample from before the intervening model's work.
expect(yield* fixture.get()).toMatchObject({ context: { tokens: null, source: "unavailable" } })
}),
)
it.live("reports an unknown compaction target through the plugin host", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const sessions = yield* Session.Service
const location = yield* Location.Service
const registry = yield* Tool.Service
const pluginHost = yield* PluginHost.make(plugins)
yield* OpenCodeTools.Plugin.effect(pluginHost)
const session = yield* sessions.create({ location: Location.Ref.make({ directory: location.directory }) })
const result = yield* executeTool(registry, {
sessionID: session.id,
...toolIdentity,
call: {
type: "tool-call",
id: "call-compact-missing",
name: "execute",
input: { code: 'return await tools.opencode.session_compact({ sessionID: "ses_missing" })' },
},
})
expect(result).toMatchObject({
metadata: { error: true },
content: [{ type: "text", text: "Unable to request compaction of session ses_missing" }],
})
expect(yield* sessions.inbox(session.id)).toEqual([])
}),
)
it.effect("groups available models by provider with paging", () =>
Effect.gen(function* () {
const catalog = yield* Provider.Service
@@ -132,3 +247,67 @@ it.effect("groups available models by provider with paging", () =>
expect(yield* run({ provider: "other", query: "alpha" })).toEqual({ providers: [], total: 0, next: null })
}),
)
const sessionInfoFixture = Effect.gen(function* () {
const bus = yield* Bus.Service
const catalog = yield* Provider.Service
const plugins = yield* Plugin.Service
const sessions = yield* Session.Service
const location = yield* Location.Service
const registry = yield* Tool.Service
const pluginHost = yield* PluginHost.make(plugins)
yield* OpenCodeTools.Plugin.effect(pluginHost)
const model = Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("alpha") })
yield* catalog.transform((editor) =>
editor.models.update(model.providerID, model.id, (model) => {
model.limit = { context: 100_000, input: 80_000, output: 10_000 }
}),
)
const session = yield* sessions.create({
title: "Context info",
model,
location: Location.Ref.make({ directory: location.directory }),
})
return {
bus,
catalog,
sessions,
session,
model,
get: (input: { sessionID?: string } = {}) =>
executeTool(registry, {
sessionID: session.id,
...toolIdentity,
call: {
type: "tool-call",
id: "call-session-info",
name: "execute",
input: { code: `return await tools.opencode.session_info(${JSON.stringify(input)})` },
},
}).pipe(
Effect.map((result) => {
expect(result.metadata?.error).not.toBe(true)
return Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(
result.content?.[0]?.type === "text" ? result.content[0].text : "",
)
}),
),
record: Effect.fn(function* (tokens: TokenUsage.Info, selected = model) {
const assistantMessageID = SessionMessage.ID.create()
yield* bus.publish(SessionEvent.Step.Started, {
sessionID: session.id,
assistantMessageID,
agent: toolIdentity.agent,
model: selected,
})
yield* bus.publish(SessionEvent.Step.Ended, {
sessionID: session.id,
assistantMessageID,
finish: "stop",
cost: Money.USD.make(0.1),
tokens,
})
return assistantMessageID
}),
}
})
+1
View File
@@ -136,6 +136,7 @@ export type SessionDomain = Pick<
| "interrupt"
| "update"
| "move"
| "compact"
| "wait"
| "context"
> & {
+1
View File
@@ -584,6 +584,7 @@ export function fromPromise(plugin: Plugin) {
interrupt: adaptApiMethod(SessionEndpoints["session.interrupt"], host.session.interrupt),
update: adaptApiMethod(SessionEndpoints["session.update"], host.session.update),
move: adaptApiMethod(SessionEndpoints["session.move"], host.session.move),
compact: adaptApiMethod(SessionEndpoints["session.compact"], host.session.compact),
wait: adaptApiMethod(SessionEndpoints["session.wait"], host.session.wait),
context: adaptApiMethod(SessionEndpoints["session.context"], host.session.context),
},
+1
View File
@@ -136,6 +136,7 @@ export type SessionDomain = Pick<
| "interrupt"
| "update"
| "move"
| "compact"
| "wait"
| "context"
> & {