mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-19 03:37:02 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f945b57a5 |
@@ -0,0 +1,121 @@
|
||||
export * as GptDelegationPlugin from "./gpt-delegation.js"
|
||||
|
||||
import { Message } from "@opencode/ai"
|
||||
import { define } from "@opencode/plugin/effect/plugin"
|
||||
import { Model } from "@opencode/schema/model"
|
||||
import type { SessionEvent } from "@opencode/schema/session-event"
|
||||
import { Effect, Stream } from "effect"
|
||||
|
||||
const proactive = `<system-reminder>
|
||||
Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning subagents no longer applies. This mode remains active until a later reminder changes it. User requests override this hint.
|
||||
|
||||
If at any point you can parallelize work by delegating tasks to a subagent, you should do so if it could save time or improve quality.
|
||||
</system-reminder>`
|
||||
const explicit = `<system-reminder>
|
||||
Any earlier instruction enabling proactive multi-agent delegation no longer applies. Do not spawn subagents unless the user or applicable AGENTS.md/skill instructions explicitly ask for subagents, delegation, or parallel agent work.
|
||||
</system-reminder>`
|
||||
const removed = `<system-reminder>
|
||||
The previous delegation-mode instructions no longer apply.
|
||||
</system-reminder>`
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.gpt-delegation",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
for (const provider of catalog.provider.list()) {
|
||||
for (const model of provider.models.values()) {
|
||||
const selected = effort(model)
|
||||
if (!selected) continue
|
||||
const variant = model.variants.find((item) => item.id === selected)
|
||||
if (!variant) continue
|
||||
catalog.model.update(model.providerID, model.id, (draft) => {
|
||||
draft.variants = [
|
||||
...draft.variants.filter((item) => item.id !== "ultra"),
|
||||
{ ...variant, id: Model.VariantID.make("ultra") },
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Synthetic reminders are durable. Render them in place as chronological system
|
||||
// messages, and restore the selected policy if compaction or revert removed it.
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = (yield* ctx.catalog.model.list()).data
|
||||
const last = event.messages.reduce<string | undefined>((last, message, index) => {
|
||||
const text = reminder(message)
|
||||
if (!text) return last
|
||||
event.messages[index] = Message.make({ id: message.id, role: "system", content: text })
|
||||
return text
|
||||
}, undefined)
|
||||
const text = policy(event.model, catalog) ?? (last ? removed : undefined)
|
||||
if (!text || text === last) return
|
||||
const at = event.messages.at(-1)?.role === "user" ? event.messages.length - 1 : event.messages.length
|
||||
event.messages.splice(at, 0, Message.system(text))
|
||||
yield* ctx.session
|
||||
.synthetic({ sessionID: event.sessionID, text, resume: false })
|
||||
.pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logWarning("failed to persist GPT delegation reminder", { sessionID: event.sessionID, cause }),
|
||||
),
|
||||
)
|
||||
}).pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logWarning("failed to reconcile GPT delegation policy", { sessionID: event.sessionID, error }),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter(
|
||||
(event): event is SessionEvent.Created | SessionEvent.ModelSelected =>
|
||||
event.type === "session.created" || event.type === "session.model.selected",
|
||||
),
|
||||
Stream.runForEach((event) =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = (yield* ctx.catalog.model.list()).data
|
||||
const current = policy(event.data.model, catalog)
|
||||
const previous = event.type === "session.model.selected" ? policy(event.data.previous, catalog) : undefined
|
||||
if (current === previous) return
|
||||
yield* ctx.session
|
||||
.synthetic({ sessionID: event.data.sessionID, text: current ?? removed, resume: false })
|
||||
.pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logWarning("failed to inject GPT delegation reminder", {
|
||||
sessionID: event.data.sessionID,
|
||||
cause,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
function effort(model: Model.Info) {
|
||||
const family = model.family?.toLowerCase()
|
||||
const id = model.modelID.toLowerCase()
|
||||
if (family === "gpt-astra" || id.includes("gpt-6-astra")) return "xhigh"
|
||||
if (family === "gpt-sol" || family === "gpt-terra" || id.includes("gpt-5.6-sol") || id.includes("gpt-5.6-terra"))
|
||||
return "max"
|
||||
}
|
||||
|
||||
function policy(ref: Model.Ref | undefined, catalog: ReadonlyArray<Model.Info>) {
|
||||
if (!ref) return
|
||||
const model = catalog.find((model) => model.providerID === ref.providerID && model.id === ref.id)
|
||||
if (!model || ![model.modelID, model.family].some((id) => id?.toLowerCase().includes("gpt"))) return
|
||||
return ref.variant === "ultra" && effort(model) && model.variants.some((item) => item.id === "ultra")
|
||||
? proactive
|
||||
: explicit
|
||||
}
|
||||
|
||||
function reminder(message: Message) {
|
||||
if (message.role !== "user" && message.role !== "system") return
|
||||
if (message.content.length !== 1) return
|
||||
const part = message.content[0]
|
||||
if (part?.type !== "text") return
|
||||
return part.text === proactive || part.text === explicit || part.text === removed ? part.text : undefined
|
||||
}
|
||||
@@ -83,6 +83,7 @@ import BrowserPlugin from "@opencode/plugin-browser"
|
||||
import { CommandPlugin } from "./command.js"
|
||||
import { PlanPlugin } from "./plan.js"
|
||||
import { ModelsDevPlugin } from "./models-dev.js"
|
||||
import { GptDelegationPlugin } from "./gpt-delegation.js"
|
||||
import { McpCodeModeExclusionPlugin } from "./mcp-codemode-exclusion.js"
|
||||
import { ProviderPlugins } from "./provider.js"
|
||||
import { WebSearchPlugins } from "./websearch/index.js"
|
||||
@@ -241,6 +242,7 @@ const post = [
|
||||
ConfigWebSearchPlugin.Plugin,
|
||||
ConfigWorktreePlugin.Plugin,
|
||||
VariantPlugin.Plugin,
|
||||
GptDelegationPlugin.Plugin,
|
||||
ConfigPolicyPlugin.Plugin,
|
||||
] as const satisfies readonly InternalPlugin[]
|
||||
|
||||
|
||||
@@ -38,8 +38,3 @@ In your final answer back to the user, focus on the most important information.
|
||||
- Do not introduce unsolicited warnings, disclaimers, approval flows, or safety/compliance checklists due to hypothetical risk.
|
||||
- Do not write tests for reversible, low-impact changes or that mirror the implementation. If you do choose to verify your work with tests, make sure that the tests are meaningful and necessary to verify implementation.
|
||||
- Run tests appropriate to the change and complete required checks. Once those pass, broaden or repeat testing only when new changes, failures, or unresolved concerns justify it; otherwise, continue toward completing the task.
|
||||
|
||||
|
||||
# Delegation
|
||||
|
||||
Do not spawn subagents unless the user or applicable AGENTS.md/skill instructions explicitly ask for subagents, delegation, or parallel agent work.
|
||||
|
||||
@@ -26,10 +26,6 @@ In the final answer, lead with the outcome, not the steps you took to reach it.
|
||||
- Keep changes consistent with the structure, naming, style, and patterns of the surrounding code.
|
||||
- Treat unfamiliar files or changes as potential user work and investigate before deleting or overwriting them.
|
||||
|
||||
# Delegation
|
||||
|
||||
Do not spawn subagents unless the user or applicable AGENTS.md/skill instructions explicitly ask for subagents, delegation, or parallel agent work.
|
||||
|
||||
# Destructive actions
|
||||
|
||||
Do not revert, reset, or discard changes you did not make. Never run destructive commands such as `git reset --hard`, `git checkout --`, or recursive deletes on broad paths unless the user clearly asked for that operation; if the target or scope is unclear, ask first. Prefer non-interactive git commands.
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"You are an AI agent powered by OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available.\\n\\n# Harness\\n- Responses are rendered as GitHub-flavored Markdown.\\n- `<system-reminder>` blocks are harness instructions, not user-authored content. Read and follow them.\\n- Prefer parallelizing independent tool calls.\\n\\n\\n# Communication\\n\\nUse clear file paths when referring to files. Keep responses clear and concise, and avoid unnecessary technical jargon.\\n\\n## Intermediate Commentary\\n\\nAs you work, you send messages to the commentary channel. These are how you collaborate with the user while you work: stating assumptions and providing updates. Keep them concise and quickly scannable, and send them only when they add real information, such as a discovery, a tradeoff, or a blocker. Do not narrate routine reads, searches, or edits.\\n\\nBy default, treat new messages received during ongoing work as steering the active task rather than replacing it. Incorporate corrections and constraints, and answer questions briefly in commentary before continuing. Replace the task only when the user clearly cancels it or requests an incompatible objective.\\n\\nDo not put a final response, such as a blocking or clarifying question, in the commentary channel. The final answer must always be fully self-contained.\\n\\n## Final Answer\\n\\nIn the final answer, lead with the outcome, not the steps you took to reach it. Cover the most important information, use only as much structure as the answer needs, and avoid long-winded explanations unless necessary. Include technical detail only where it helps.\\n\\n# Working in codebases\\n- Keep changes consistent with the structure, naming, style, and patterns of the surrounding code.\\n- Treat unfamiliar files or changes as potential user work and investigate before deleting or overwriting them.\\n\\n# Delegation\\n\\nDo not spawn subagents unless the user or applicable AGENTS.md/skill instructions explicitly ask for subagents, delegation, or parallel agent work.\\n\\n# Destructive actions\\n\\nDo not revert, reset, or discard changes you did not make. Never run destructive commands such as `git reset --hard`, `git checkout --`, or recursive deletes on broad paths unless the user clearly asked for that operation; if the target or scope is unclear, ask first. Prefer non-interactive git commands.\\n\\n# Autonomy\\n\\nDo not infer authorization for work beyond the user's request. Assumptions that help you make progress are fine as long as they stay within the user's intent and the scope of the task.\\n\"},{\"role\":\"user\",\"content\":\"Say hello in one short sentence.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"store\":false,\"prompt_cache_key\":\"ses_runner_recorded\",\"max_completion_tokens\":20,\"temperature\":0}"
|
||||
"body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"You are an AI agent powered by OpenCode, a coding agent harness. Help the user accomplish their goals using the tools you have available.\\n\\n# Harness\\n- Responses are rendered as GitHub-flavored Markdown.\\n- `<system-reminder>` blocks are harness instructions, not user-authored content. Read and follow them.\\n- Prefer parallelizing independent tool calls.\\n\\n\\n# Communication\\n\\nUse clear file paths when referring to files. Keep responses clear and concise, and avoid unnecessary technical jargon.\\n\\n## Intermediate Commentary\\n\\nAs you work, you send messages to the commentary channel. These are how you collaborate with the user while you work: stating assumptions and providing updates. Keep them concise and quickly scannable, and send them only when they add real information, such as a discovery, a tradeoff, or a blocker. Do not narrate routine reads, searches, or edits.\\n\\nBy default, treat new messages received during ongoing work as steering the active task rather than replacing it. Incorporate corrections and constraints, and answer questions briefly in commentary before continuing. Replace the task only when the user clearly cancels it or requests an incompatible objective.\\n\\nDo not put a final response, such as a blocking or clarifying question, in the commentary channel. The final answer must always be fully self-contained.\\n\\n## Final Answer\\n\\nIn the final answer, lead with the outcome, not the steps you took to reach it. Cover the most important information, use only as much structure as the answer needs, and avoid long-winded explanations unless necessary. Include technical detail only where it helps.\\n\\n# Working in codebases\\n- Keep changes consistent with the structure, naming, style, and patterns of the surrounding code.\\n- Treat unfamiliar files or changes as potential user work and investigate before deleting or overwriting them.\\n\\n# Destructive actions\\n\\nDo not revert, reset, or discard changes you did not make. Never run destructive commands such as `git reset --hard`, `git checkout --`, or recursive deletes on broad paths unless the user clearly asked for that operation; if the target or scope is unclear, ask first. Prefer non-interactive git commands.\\n\\n# Autonomy\\n\\nDo not infer authorization for work beyond the user's request. Assumptions that help you make progress are fine as long as they stay within the user's intent and the scope of the task.\\n\"},{\"role\":\"user\",\"content\":\"Say hello in one short sentence.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"store\":false,\"prompt_cache_key\":\"ses_runner_recorded\",\"max_completion_tokens\":20,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Message, SystemPart } from "@opencode/ai"
|
||||
import { Catalog } from "@opencode/core/catalog"
|
||||
import { Config } from "@opencode/core/config"
|
||||
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
|
||||
import { Location } from "@opencode/core/location"
|
||||
import { GptDelegationPlugin } from "@opencode/core/plugin/gpt-delegation"
|
||||
import { PluginHooks } from "@opencode/core/plugin/hooks"
|
||||
import { Agent } from "@opencode/schema/agent"
|
||||
import { Event } from "@opencode/schema/event"
|
||||
import { Model } from "@opencode/schema/model"
|
||||
import { Provider } from "@opencode/schema/provider"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import type { SessionEvent } from "@opencode/schema/session-event"
|
||||
import { SessionInbox } from "@opencode/schema/session-inbox"
|
||||
import { SessionMessage } from "@opencode/schema/session-message"
|
||||
import type { SessionContext } from "@opencode/plugin/effect/session"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import { DateTime, Deferred, Effect, Stream } from "effect"
|
||||
import { tempLocationLayer } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { catalogHost, host } from "./host"
|
||||
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(LayerNode.group([Catalog.node, PluginHooks.node]), [
|
||||
Config.node.replace(Config.testLayer()),
|
||||
Location.node.replace(tempLocationLayer),
|
||||
]),
|
||||
)
|
||||
const provider = Provider.ID.make("test")
|
||||
const sessionID = Session.ID.make("ses_gpt_delegation")
|
||||
const max = { id: Model.VariantID.make("max"), settings: { reasoningEffort: "max" } }
|
||||
const xhigh = { id: Model.VariantID.make("xhigh"), settings: { reasoningEffort: "xhigh" } }
|
||||
const model = (id: string, variant: string | undefined) =>
|
||||
Model.Ref.make({
|
||||
providerID: provider,
|
||||
id: Model.ID.make(id),
|
||||
...(variant ? { variant: Model.VariantID.make(variant) } : {}),
|
||||
})
|
||||
|
||||
const selected = (current: Model.Ref, previous: Model.Ref): SessionEvent.ModelSelected => ({
|
||||
id: Event.ID.create(),
|
||||
created: 0,
|
||||
durable: { aggregateID: sessionID, seq: Event.Seq.make(0), version: Event.Version.make(1) },
|
||||
type: "session.model.selected",
|
||||
data: { sessionID, model: current, previous },
|
||||
})
|
||||
|
||||
const setup = Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const persisted: SessionInbox.Synthetic[] = []
|
||||
const completed = yield* Deferred.make<void>()
|
||||
yield* catalog.transform((editor) => {
|
||||
editor.model.update(provider, Model.ID.make("sol"), (item) => {
|
||||
item.modelID = Model.ID.make("gpt-5.6-sol")
|
||||
item.family = Model.Family.make("custom-family")
|
||||
item.variants = [
|
||||
{ ...max, headers: { "x-reasoning": "max" }, body: { include: ["reasoning.encrypted_content"] } },
|
||||
{ id: Model.VariantID.make("ultra"), settings: { reasoningEffort: "stale" } },
|
||||
]
|
||||
})
|
||||
editor.model.update(provider, Model.ID.make("terra"), (item) => {
|
||||
item.family = Model.Family.make("gpt-terra")
|
||||
item.variants = [max]
|
||||
})
|
||||
editor.model.update(provider, Model.ID.make("astra"), (item) => {
|
||||
item.modelID = Model.ID.make("gpt-6-astra")
|
||||
item.variants = [max, { ...xhigh, headers: { "x-reasoning": "xhigh" } }]
|
||||
})
|
||||
editor.model.update(provider, Model.ID.make("no-xhigh"), (item) => {
|
||||
item.family = Model.Family.make("gpt-astra")
|
||||
item.variants = [max]
|
||||
})
|
||||
editor.model.update(provider, Model.ID.make("claude"), (item) => {
|
||||
item.variants = [{ id: Model.VariantID.make("ultra") }]
|
||||
})
|
||||
})
|
||||
return {
|
||||
catalog,
|
||||
persisted,
|
||||
completed: Deferred.await(completed),
|
||||
activate: (events: ReadonlyArray<SessionEvent.Created | SessionEvent.ModelSelected>) =>
|
||||
GptDelegationPlugin.Plugin.effect(
|
||||
host({
|
||||
catalog: catalogHost(catalog),
|
||||
event: {
|
||||
subscribe: () => Stream.fromIterable(events).pipe(Stream.ensuring(Deferred.succeed(completed, undefined))),
|
||||
},
|
||||
session: {
|
||||
hook: (name, callback, options) => hooks.register("session", name, callback, options),
|
||||
synthetic: (input) =>
|
||||
Effect.sync(() => {
|
||||
expect(input.resume).toBe(false)
|
||||
const item = SessionInbox.Synthetic.make({
|
||||
id: SessionMessage.ID.create(),
|
||||
sessionID: input.sessionID,
|
||||
timeCreated: DateTime.makeUnsafe(0),
|
||||
type: "synthetic",
|
||||
payload: { text: input.text },
|
||||
delivery: "steer",
|
||||
})
|
||||
persisted.push(item)
|
||||
return item
|
||||
}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
prepare: (ref: Model.Ref, messages: Message[]) => {
|
||||
const event: SessionContext = {
|
||||
sessionID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: ref,
|
||||
system: [SystemPart.make("baseline")],
|
||||
messages,
|
||||
tools: {},
|
||||
options: {},
|
||||
}
|
||||
return hooks.trigger("session", "context", event)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe("GptDelegationPlugin", () => {
|
||||
it.effect("copies complete max or xhigh variants using either model identity signal", () =>
|
||||
Effect.gen(function* () {
|
||||
const fixture = yield* setup
|
||||
yield* fixture.activate([])
|
||||
const sol = yield* fixture.catalog.model.get(provider, Model.ID.make("sol"))
|
||||
const terra = yield* fixture.catalog.model.get(provider, Model.ID.make("terra"))
|
||||
const astra = yield* fixture.catalog.model.get(provider, Model.ID.make("astra"))
|
||||
const noXhigh = yield* fixture.catalog.model.get(provider, Model.ID.make("no-xhigh"))
|
||||
expect(sol?.variants.filter((item) => item.id === "ultra")).toEqual([
|
||||
{
|
||||
...max,
|
||||
headers: { "x-reasoning": "max" },
|
||||
body: { include: ["reasoning.encrypted_content"] },
|
||||
id: Model.VariantID.make("ultra"),
|
||||
},
|
||||
])
|
||||
expect(terra?.variants.find((item) => item.id === "ultra")).toEqual({ ...max, id: Model.VariantID.make("ultra") })
|
||||
expect(astra?.variants.find((item) => item.id === "ultra")).toEqual({
|
||||
...xhigh,
|
||||
headers: { "x-reasoning": "xhigh" },
|
||||
id: Model.VariantID.make("ultra"),
|
||||
})
|
||||
expect(noXhigh?.variants.some((item) => item.id === "ultra")).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("reconciles missing and stale policies as system messages without changing the baseline", () =>
|
||||
Effect.gen(function* () {
|
||||
const fixture = yield* setup
|
||||
yield* fixture.activate([])
|
||||
expect((yield* fixture.prepare(model("claude", "ultra"), [Message.user("hello")])).messages).toEqual([
|
||||
Message.user("hello"),
|
||||
])
|
||||
const normal = yield* fixture.prepare(model("sol", undefined), [Message.user("hello")])
|
||||
const explicit = fixture.persisted[0]
|
||||
if (!explicit) throw new Error("Missing explicit-only reminder")
|
||||
expect(normal.messages.map((message) => message.role)).toEqual(["system", "user"])
|
||||
expect(explicit.payload.text).toContain("Do not spawn subagents unless")
|
||||
|
||||
const ultra = yield* fixture.prepare(model("sol", "ultra"), [
|
||||
Message.user(explicit.payload.text),
|
||||
Message.user("continue"),
|
||||
])
|
||||
const proactive = fixture.persisted[1]
|
||||
if (!proactive) throw new Error("Missing proactive reminder")
|
||||
expect(ultra.system).toEqual(normal.system)
|
||||
expect(ultra.messages.map((message) => message.role)).toEqual(["system", "system", "user"])
|
||||
expect(proactive.payload.text).toContain("Proactive multi-agent delegation is active")
|
||||
expect((yield* fixture.prepare(model("astra", "ultra"), [...ultra.messages])).messages).toEqual(ultra.messages)
|
||||
expect(fixture.persisted).toHaveLength(2)
|
||||
|
||||
const other = yield* fixture.prepare(model("claude", undefined), [...ultra.messages])
|
||||
expect(fixture.persisted[2]?.payload.text).toContain("previous delegation-mode instructions no longer apply")
|
||||
expect((yield* fixture.prepare(model("claude", undefined), [...other.messages])).messages).toEqual(other.messages)
|
||||
expect(fixture.persisted).toHaveLength(3)
|
||||
|
||||
yield* fixture.prepare(model("sol", undefined), [Message.user(proactive.payload.text), Message.user("continue")])
|
||||
expect(fixture.persisted.at(-1)?.payload.text).toBe(explicit.payload.text)
|
||||
// A checkpoint quote is history, not a live mode reminder.
|
||||
const compacted = yield* fixture.prepare(model("sol", "ultra"), [
|
||||
Message.user(`<conversation-checkpoint>\n${proactive.payload.text}\n</conversation-checkpoint>`),
|
||||
])
|
||||
expect(compacted.messages[0]).toEqual(Message.system(proactive.payload.text))
|
||||
expect(fixture.persisted).toHaveLength(5)
|
||||
yield* fixture.prepare(model("sol", undefined), [Message.user("reverted history")])
|
||||
expect(fixture.persisted.at(-1)?.payload.text).toBe(explicit.payload.text)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("persists model-switch reminders and renders them in place on later requests", () =>
|
||||
Effect.gen(function* () {
|
||||
const fixture = yield* setup
|
||||
yield* fixture.activate([
|
||||
selected(model("sol", undefined), model("claude", undefined)),
|
||||
selected(model("sol", "ultra"), model("sol", undefined)),
|
||||
selected(model("astra", "ultra"), model("sol", "ultra")),
|
||||
selected(model("claude", undefined), model("astra", "ultra")),
|
||||
])
|
||||
yield* fixture.completed
|
||||
expect(fixture.persisted.map((item) => item.payload.text)).toEqual([
|
||||
expect.stringContaining("Do not spawn subagents unless"),
|
||||
expect.stringContaining("Proactive multi-agent delegation is active"),
|
||||
expect.stringContaining("previous delegation-mode instructions no longer apply"),
|
||||
])
|
||||
const request = yield* fixture.prepare(model("claude", undefined), [
|
||||
...fixture.persisted.map((item) => Message.make({ id: item.id, role: "user", content: item.payload.text })),
|
||||
Message.user("continue"),
|
||||
])
|
||||
expect(request.messages).toEqual([
|
||||
...fixture.persisted.map((item) => Message.make({ id: item.id, role: "system", content: item.payload.text })),
|
||||
Message.user("continue"),
|
||||
])
|
||||
expect(fixture.persisted).toHaveLength(3)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -63,12 +63,7 @@ import { Document, Info } from "@opencode/schema/config"
|
||||
import { ConfigCompaction } from "@opencode/schema/config/compaction"
|
||||
import { Tool } from "@opencode/core/tool"
|
||||
import type { Info as ToolInfo } from "@opencode/schema/tool"
|
||||
import {
|
||||
InstructionStateTable,
|
||||
SessionInboxTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
} from "@opencode/core/session/sql"
|
||||
import { InstructionStateTable, SessionInboxTable, SessionMessageTable, SessionTable } from "@opencode/core/session/sql"
|
||||
import { InstructionEntry } from "@opencode/core/session/instruction-entry"
|
||||
import { SessionStore } from "@opencode/core/session/store"
|
||||
import { Instructions } from "@opencode/core/instructions/index"
|
||||
@@ -1700,7 +1695,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* s.resume
|
||||
|
||||
expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual([
|
||||
expect.stringContaining("# Delegation"),
|
||||
expect.stringContaining("## Final Answer"),
|
||||
"Initial context",
|
||||
])
|
||||
})
|
||||
@@ -1720,7 +1715,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* s.resume
|
||||
|
||||
expect(s.requests.at(-1)?.system.map((part) => part.text)).toEqual([
|
||||
expect.stringContaining("# Delegation"),
|
||||
expect.stringContaining("## Final Answer"),
|
||||
"Initial context",
|
||||
])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user