Skip to content

Commit caead4d

Browse files
committed
fix(dev): restore session metadata surfaces
1 parent 9c8d781 commit caead4d

17 files changed

Lines changed: 283 additions & 40 deletions

‎build-local.sh‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export PATH="$PATH:$REPO_ROOT/node_modules/.bin"
4949
echo "==> Installing dependencies..."
5050
bun install --minimum-release-age 0 --frozen-lockfile
5151

52+
echo "==> Regenerating JavaScript SDK..."
53+
bun "$REPO_ROOT/packages/sdk/js/script/build.ts"
54+
5255
echo "==> Building opencode (native linux-x64, --single)..."
5356
cd "$PKG_DIR"
5457
bun run script/build.ts --single --skip-install

‎packages/app/src/components/session/session-context-breakdown.test.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ const user = (id: string) => {
1010
} as unknown as Message
1111
}
1212

13-
const assistant = (id: string) => {
13+
const assistant = (id: string, toolDefs?: string) => {
1414
return {
1515
id,
1616
role: "assistant",
1717
time: { created: 1 },
18+
tool_defs: toolDefs,
1819
} as unknown as Message
1920
}
2021

@@ -58,4 +59,25 @@ describe("estimateSessionContextBreakdown", () => {
5859
expect(total).toBeLessThanOrEqual(10)
5960
expect(output.every((segment) => segment.width <= 100)).toBeTrue()
6061
})
62+
63+
test("counts the latest assistant tool definitions separately", () => {
64+
const firstToolDefs = "old tool definitions"
65+
const latestToolDefs = "new tool definitions"
66+
const messages = [user("u1"), assistant("a1", firstToolDefs), assistant("a2", latestToolDefs)]
67+
const parts = {
68+
u1: [{ type: "text", text: "hello" }] as unknown as Part[],
69+
a1: [{ type: "text", text: "first response" }] as unknown as Part[],
70+
a2: [{ type: "text", text: "latest response" }] as unknown as Part[],
71+
}
72+
73+
const output = estimateSessionContextBreakdown({
74+
messages,
75+
parts,
76+
input: 100,
77+
systemPrompt: "system",
78+
})
79+
80+
const map = Object.fromEntries(output.map((segment) => [segment.key, segment.tokens]))
81+
expect(map.toolDefs).toBe(Math.ceil(latestToolDefs.length / 4))
82+
})
6183
})

‎packages/app/src/components/session/session-context-breakdown.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
22

3-
export type SessionContextBreakdownKey = "system" | "user" | "assistant" | "tool" | "other"
3+
export type SessionContextBreakdownKey = "system" | "user" | "assistant" | "tool" | "toolDefs" | "other"
44

55
export type SessionContextBreakdownSegment = {
66
key: SessionContextBreakdownKey
@@ -33,14 +33,18 @@ const charsFromAssistantPart = (part: Part) => {
3333
}
3434

3535
const build = (
36-
tokens: { system: number; user: number; assistant: number; tool: number; other: number },
36+
tokens: { system: number; user: number; assistant: number; tool: number; toolDefs: number; other: number },
3737
input: number,
3838
) => {
3939
return [
4040
{
4141
key: "system",
4242
tokens: tokens.system,
4343
},
44+
{
45+
key: "toolDefs",
46+
tokens: tokens.toolDefs,
47+
},
4448
{
4549
key: "user",
4650
tokens: tokens.user,
@@ -75,6 +79,14 @@ export function estimateSessionContextBreakdown(args: {
7579
}) {
7680
if (!args.input) return []
7781

82+
const toolDefsChars = (() => {
83+
for (let i = args.messages.length - 1; i >= 0; i--) {
84+
const msg = args.messages[i]
85+
if (msg.role === "assistant" && msg.tool_defs) return msg.tool_defs.length
86+
}
87+
return 0
88+
})()
89+
7890
const counts = args.messages.reduce(
7991
(acc, msg) => {
8092
const parts = args.parts[msg.id] ?? []
@@ -105,6 +117,7 @@ export function estimateSessionContextBreakdown(args: {
105117
user: 0,
106118
assistant: 0,
107119
tool: 0,
120+
toolDefs: toolDefsChars,
108121
},
109122
)
110123

@@ -113,8 +126,9 @@ export function estimateSessionContextBreakdown(args: {
113126
user: estimateTokens(counts.user),
114127
assistant: estimateTokens(counts.assistant),
115128
tool: estimateTokens(counts.tool),
129+
toolDefs: estimateTokens(counts.toolDefs),
116130
}
117-
const estimated = tokens.system + tokens.user + tokens.assistant + tokens.tool
131+
const estimated = tokens.system + tokens.user + tokens.assistant + tokens.tool + tokens.toolDefs
118132

119133
if (estimated <= args.input) {
120134
return build({ ...tokens, other: args.input - estimated }, args.input)
@@ -126,7 +140,8 @@ export function estimateSessionContextBreakdown(args: {
126140
user: Math.floor(tokens.user * scale),
127141
assistant: Math.floor(tokens.assistant * scale),
128142
tool: Math.floor(tokens.tool * scale),
143+
toolDefs: Math.floor(tokens.toolDefs * scale),
129144
}
130-
const total = scaled.system + scaled.user + scaled.assistant + scaled.tool
145+
const total = scaled.system + scaled.user + scaled.assistant + scaled.tool + scaled.toolDefs
131146
return build({ ...scaled, other: Math.max(0, args.input - total) }, args.input)
132147
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, test } from "bun:test"
2+
import type { Message } from "@opencode-ai/sdk/v2/client"
3+
import { getSessionSystemPrompt } from "./session-context-system"
4+
5+
const user = (id: string, system?: string) => {
6+
return {
7+
id,
8+
role: "user",
9+
system,
10+
time: { created: 1 },
11+
} as unknown as Message
12+
}
13+
14+
const assistant = (id: string, systemPrompt?: string) => {
15+
return {
16+
id,
17+
role: "assistant",
18+
system_prompt: systemPrompt,
19+
time: { created: 1 },
20+
} as unknown as Message
21+
}
22+
23+
describe("getSessionSystemPrompt", () => {
24+
test("prefers the latest stored assistant system prompt", () => {
25+
expect(getSessionSystemPrompt([user("u1", "user system"), assistant("a1", " assistant system ")] as Message[])).toBe(
26+
"assistant system",
27+
)
28+
})
29+
30+
test("falls back to the latest user system prompt", () => {
31+
expect(getSessionSystemPrompt([user("u1", "old"), user("u2", " current ")] as Message[])).toBe("current")
32+
})
33+
})
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Message } from "@opencode-ai/sdk/v2/client"
2+
3+
export function getSessionSystemPrompt(messages: Message[]) {
4+
for (let i = messages.length - 1; i >= 0; i--) {
5+
const msg = messages[i]
6+
if (msg.role !== "assistant") continue
7+
const trimmed = msg.system_prompt?.trim()
8+
if (trimmed) return trimmed
9+
}
10+
11+
for (let i = messages.length - 1; i >= 0; i--) {
12+
const msg = messages[i]
13+
if (msg.role !== "user") continue
14+
const trimmed = msg.system?.trim()
15+
if (trimmed) return trimmed
16+
}
17+
}

‎packages/app/src/components/session/session-context-tab.tsx‎

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,28 @@ import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
22
import type { JSX } from "solid-js"
33
import { useSync } from "@/context/sync"
44
import { checksum } from "@opencode-ai/core/util/encode"
5-
import { findLast } from "@opencode-ai/core/util/array"
65
import { same } from "@/utils/same"
76
import { Icon } from "@opencode-ai/ui/icon"
87
import { Accordion } from "@opencode-ai/ui/accordion"
98
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
109
import { File } from "@opencode-ai/ui/file"
1110
import { Markdown } from "@opencode-ai/ui/markdown"
1211
import { ScrollView } from "@opencode-ai/ui/scroll-view"
13-
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
12+
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
1413
import { useLanguage } from "@/context/language"
1514
import { useProviders } from "@/hooks/use-providers"
1615
import { useSessionLayout } from "@/pages/session/session-layout"
1716
import { getSessionContextMetrics } from "./session-context-metrics"
1817
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
1918
import { createSessionContextFormatter } from "./session-context-format"
19+
import { getSessionSystemPrompt } from "./session-context-system"
2020

2121
const BREAKDOWN_COLOR: Record<SessionContextBreakdownKey, string> = {
2222
system: "var(--syntax-info)",
2323
user: "var(--syntax-success)",
2424
assistant: "var(--syntax-property)",
2525
tool: "var(--syntax-warning)",
26+
toolDefs: "var(--syntax-keyword)",
2627
other: "var(--syntax-comment)",
2728
}
2829

@@ -88,7 +89,6 @@ function RawMessage(props: {
8889
}
8990

9091
const emptyMessages: Message[] = []
91-
const emptyUserMessages: UserMessage[] = []
9292

9393
export function SessionContextTab() {
9494
const sync = useSync()
@@ -108,22 +108,6 @@ export function SessionContextTab() {
108108
{ equals: same },
109109
)
110110

111-
const userMessages = createMemo(
112-
() => messages().filter((m) => m.role === "user") as UserMessage[],
113-
emptyUserMessages,
114-
{ equals: same },
115-
)
116-
117-
const visibleUserMessages = createMemo(
118-
() => {
119-
const revert = info()?.revert?.messageID
120-
if (!revert) return userMessages()
121-
return userMessages().filter((m) => m.id < revert)
122-
},
123-
emptyUserMessages,
124-
{ equals: same },
125-
)
126-
127111
const usd = createMemo(
128112
() =>
129113
new Intl.NumberFormat(language.intl(), {
@@ -151,14 +135,7 @@ export function SessionContextTab() {
151135
}
152136
})
153137

154-
const systemPrompt = createMemo(() => {
155-
const msg = findLast(visibleUserMessages(), (m) => !!m.system)
156-
const system = msg?.system
157-
if (!system) return
158-
const trimmed = system.trim()
159-
if (!trimmed) return
160-
return trimmed
161-
})
138+
const systemPrompt = createMemo(() => getSessionSystemPrompt(messages()))
162139

163140
const providerLabel = createMemo(() => {
164141
const c = ctx()
@@ -190,6 +167,7 @@ export function SessionContextTab() {
190167

191168
const breakdownLabel = (key: SessionContextBreakdownKey) => {
192169
if (key === "system") return language.t("context.breakdown.system")
170+
if (key === "toolDefs") return language.t("context.breakdown.toolDefs")
193171
if (key === "user") return language.t("context.breakdown.user")
194172
if (key === "assistant") return language.t("context.breakdown.assistant")
195173
if (key === "tool") return language.t("context.breakdown.tool")

‎packages/app/src/i18n/en.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ export const dict = {
374374
"context.breakdown.user": "User",
375375
"context.breakdown.assistant": "Assistant",
376376
"context.breakdown.tool": "Tool Calls",
377+
"context.breakdown.toolDefs": "Tool Definitions",
377378
"context.breakdown.other": "Other",
378379

379380
"context.systemPrompt.title": "System Prompt",

‎packages/opencode/src/server/routes/instance/httpapi/public.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ function applyLegacySchemaOverrides(spec: OpenApiSpec) {
260260
if (!schemas) return
261261
if (schemas.AgentConfig) schemas.AgentConfig.additionalProperties = {}
262262
if (schemas.Command?.properties?.template) schemas.Command.properties.template = { type: "string" }
263+
if (schemas.AssistantMessage?.properties) {
264+
schemas.AssistantMessage.properties.tool_defs = { type: "string" }
265+
schemas.AssistantMessage.properties.system_prompt = { type: "string" }
266+
}
263267
if (schemas.Workspace?.properties) {
264268
schemas.Workspace.properties.branch = nullable(schemas.Workspace.properties.branch)
265269
schemas.Workspace.properties.directory = nullable(schemas.Workspace.properties.directory)

‎packages/opencode/src/session/message-v2.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { EventV2 } from "@opencode-ai/core/event"
22
import { SessionID, MessageID, PartID } from "./schema"
33
import { SessionV1 } from "@opencode-ai/core/v1/session"
44
import { ProviderV2 } from "@opencode-ai/core/provider"
5+
import { Effect, Schema } from "effect"
56
import {
67
APIError,
78
AbortedError,
@@ -14,7 +15,6 @@ import {
1415
StructuredOutputError,
1516
SubtaskPart,
1617
User as CoreUser,
17-
WithParts as CoreWithParts,
1818
type ToolPart,
1919
} from "@opencode-ai/core/v1/session"
2020
export {
@@ -32,7 +32,11 @@ export {
3232
type ToolStateError,
3333
} from "@opencode-ai/core/v1/session"
3434

35-
export const Assistant = CoreAssistant
35+
export const Assistant = Schema.Struct({
36+
...CoreAssistant.fields,
37+
tool_defs: Schema.optional(Schema.String),
38+
system_prompt: Schema.optional(Schema.String),
39+
}).annotate({ identifier: "AssistantMessage" })
3640
export type Assistant = CoreAssistant & {
3741
tool_defs?: string
3842
system_prompt?: string
@@ -41,9 +45,13 @@ export type Assistant = CoreAssistant & {
4145
export const User = CoreUser
4246
export type User = CoreUser & { delivery?: "immediate" | "deferred" }
4347

48+
export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
4449
export type Info = User | Assistant
4550

46-
export const WithParts = CoreWithParts
51+
export const WithParts = Schema.Struct({
52+
info: Info,
53+
parts: Schema.Array(Part),
54+
})
4755
export type WithParts = {
4856
info: Info
4957
parts: Part[]
@@ -66,7 +74,6 @@ import { errorMessage } from "@/util/error"
6674
import { isMedia } from "@/util/media"
6775
import type { SystemError } from "bun"
6876
import type { Provider } from "@/provider/provider"
69-
import { Effect, Schema } from "effect"
7077
import * as EffectLogger from "@opencode-ai/core/effect/logger"
7178

7279
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */

‎packages/opencode/test/server/httpapi-public-openapi.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ describe("PublicApi OpenAPI v2 errors", () => {
105105
}
106106
})
107107

108+
test("documents assistant context metadata for generated clients", () => {
109+
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
110+
111+
expect(spec.components.schemas.AssistantMessage?.properties).toMatchObject({
112+
tool_defs: { type: "string" },
113+
system_prompt: { type: "string" },
114+
})
115+
})
116+
108117
test("preserves required request bodies for v2 mutations", () => {
109118
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
110119

0 commit comments

Comments
 (0)