Skip to content

Commit b4b9bb0

Browse files
committed
refactor(agent): shrink the public API surface
- drop the devframe/utils/valibot-json-schema subpath: AgentToolInput gains valibot args (the same shape RPC definitions carry); the agent host derives the JSON-Schema input internally, and the hub passes schemas through untouched — conversion is an implementation detail again - devframe/node exports only registerDevframeInstance (+ its two types) from the instance registry; the read/probe/prune helpers stay internal to the connector
1 parent d1d9373 commit b4b9bb0

16 files changed

Lines changed: 69 additions & 65 deletions

File tree

alias.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export const alias = {
3131
'devframe/utils/shared-state': r('devframe/src/utils/shared-state.ts'),
3232
'devframe/utils/streaming-channel': r('devframe/src/utils/streaming-channel.ts'),
3333
'devframe/utils/structured-clone': r('devframe/src/utils/structured-clone.ts'),
34-
'devframe/utils/valibot-json-schema': r('devframe/src/utils/valibot-json-schema.ts'),
3534
'devframe/utils/when': r('devframe/src/utils/when.ts'),
3635
'devframe/adapters/cac': r('devframe/src/adapters/cac.ts'),
3736
'devframe/adapters/cli': r('devframe/src/adapters/cli.ts'),

packages/devframe/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
"./utils/shared-state": "./dist/utils/shared-state.mjs",
5555
"./utils/streaming-channel": "./dist/utils/streaming-channel.mjs",
5656
"./utils/structured-clone": "./dist/utils/structured-clone.mjs",
57-
"./utils/valibot-json-schema": "./dist/utils/valibot-json-schema.mjs",
5857
"./utils/when": "./dist/utils/when.mjs",
5958
"./package.json": "./package.json"
6059
},
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Moved to `devframe/utils/valibot-json-schema` so hosts without the MCP SDK
2-
// (e.g. the hub's commands→agent bridge) can convert schemas too. This module
3-
// keeps the adapter-local import path stable.
4-
export { valibotArgsToJsonSchema, valibotReturnToJsonSchema } from 'devframe/utils/valibot-json-schema'
1+
// Shared internal conversion (also used by the agent host to project
2+
// `AgentToolInput.args`); this module keeps the adapter-local import path
3+
// stable.
4+
export { valibotArgsToJsonSchema, valibotReturnToJsonSchema } from '../../utils/valibot-json-schema'

packages/devframe/src/node/__tests__/host-agent.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,39 @@ describe('devToolsAgentHost', () => {
270270
})
271271
})
272272

273+
describe('valibot args on tool inputs', () => {
274+
it('derives the JSON-Schema input from a single object schema (unwrapped)', async () => {
275+
const v = await import('valibot')
276+
const ctx = createContext()
277+
ctx.agent.registerTool({
278+
id: 'schema:tool',
279+
description: 'Schema-typed.',
280+
args: [v.object({ name: v.optional(v.string()) })],
281+
handler: args => args,
282+
})
283+
284+
const tool = ctx.agent.getTool('schema:tool')!
285+
const schema = tool.inputSchema as { type: string, properties: Record<string, unknown> }
286+
expect(schema.type).toBe('object')
287+
expect(Object.keys(schema.properties)).toEqual(['name'])
288+
})
289+
290+
it('an explicit inputSchema override wins over args', async () => {
291+
const v = await import('valibot')
292+
const ctx = createContext()
293+
ctx.agent.registerTool({
294+
id: 'override:tool',
295+
description: 'Override.',
296+
args: [v.object({ ignored: v.string() })],
297+
inputSchema: { type: 'object', properties: { custom: { type: 'string' } } },
298+
handler: () => {},
299+
})
300+
301+
const schema = ctx.agent.getTool('override:tool')!.inputSchema as { properties: Record<string, unknown> }
302+
expect(Object.keys(schema.properties)).toEqual(['custom'])
303+
})
304+
})
305+
273306
describe('registerToolProvider()', () => {
274307
it('queries the provider lazily on list/getTool/invoke', async () => {
275308
const ctx = createContext()

packages/devframe/src/node/host-agent.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
RpcFunctionAgentOptions,
1717
} from 'devframe/types'
1818
import { createEventEmitter } from 'devframe/utils/events'
19+
import { valibotArgsToJsonSchema } from '../utils/valibot-json-schema'
1920
import { diagnostics } from './diagnostics'
2021

2122
interface RegisteredTool {
@@ -211,7 +212,10 @@ export class DevframeAgentHost implements DevframeAgentHostType {
211212
description: input.description,
212213
safety: input.safety ?? 'action',
213214
tags: input.tags,
214-
inputSchema: input.inputSchema,
215+
// Valibot args are the source of truth (mirroring RPC definitions);
216+
// an explicit JSON-Schema override wins when given.
217+
inputSchema: input.inputSchema
218+
?? (input.args?.length ? valibotArgsToJsonSchema(input.args).schema : undefined),
215219
outputSchema: input.outputSchema,
216220
examples: input.examples,
217221
}

packages/devframe/src/node/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export type { RpcFunctionsHost } from './host-functions'
99
export * from './host-h3'
1010
export * from './host-services'
1111
export * from './host-views'
12-
export * from './instance-registry'
12+
// Only registration is public — custom hosts (e.g. @devframes/next) record
13+
// themselves; the read/probe/prune helpers stay internal to the connector.
14+
export { registerDevframeInstance } from './instance-registry'
15+
export type { DevframeInstanceRecord, DevframeInstanceRegistration } from './instance-registry'
1316
export * from './rpc-shared-state'
1417
export * from './rpc-streaming'
1518
export * from './scope'

packages/devframe/src/types/agent.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { GenericSchema } from 'valibot'
12
import type { RpcFunctionAgentOptions } from '../rpc/types'
23
import type { EventEmitter } from './events'
34

@@ -44,6 +45,15 @@ export interface AgentToolInput {
4445
description: string
4546
safety?: 'read' | 'action' | 'destructive'
4647
tags?: readonly string[]
48+
/**
49+
* Positional valibot schemas describing the tool's arguments — the same
50+
* shape RPC definitions carry. The host derives the tool's JSON-Schema
51+
* input from them (a single `v.object(...)` schema is unwrapped — the
52+
* friendliest shape at the agent boundary). Purely descriptive: the
53+
* handler still receives the caller's args object as-is.
54+
*/
55+
args?: readonly GenericSchema[]
56+
/** Raw JSON-Schema input override. Prefer {@link args}. */
4757
inputSchema?: unknown
4858
outputSchema?: unknown
4959
examples?: readonly { args: unknown[], description?: string }[]

packages/devframe/tsdown.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ const serverEntries = {
9999
'utils/launch-editor': 'src/utils/launch-editor.ts',
100100
'utils/open': 'src/utils/open.ts',
101101
'utils/serve-static': 'src/utils/serve-static.ts',
102-
'utils/valibot-json-schema': 'src/utils/valibot-json-schema.ts',
103102
'adapters/cac': 'src/adapters/cac.ts',
104103
'adapters/cli': 'src/adapters/cli.ts',
105104
'adapters/dev': 'src/adapters/dev.ts',

packages/hub/src/node/host-commands.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { AgentToolInput, AgentToolProviderHandle } from 'devframe/types'
22
import type {
3+
DevframeCommandAgentOptions,
34
DevframeCommandHandle,
45
DevframeCommandsHost as DevframeCommandsHostType,
56
DevframeServerCommandEntry,
67
DevframeServerCommandInput,
78
} from '../types/commands'
89
import type { DevframeHubContext } from './context'
910
import { createEventEmitter } from 'devframe/utils/events'
10-
import { valibotArgsToJsonSchema } from 'devframe/utils/valibot-json-schema'
1111
import { diagnostics } from './diagnostics'
1212

1313
function findChildCommand(command: DevframeServerCommandInput, id: string): DevframeServerCommandInput | undefined {
@@ -179,16 +179,16 @@ export class DevframeCommandsHost implements DevframeCommandsHostType {
179179
const walk = (command: DevframeServerCommandInput): void => {
180180
const agent = command.agent
181181
if (agent && command.handler) {
182-
const { schema, unwrapped } = valibotArgsToJsonSchema(agent.args)
183182
tools.push({
184183
id: command.id,
185184
title: agent.title ?? command.title,
186185
description: agent.description,
187186
safety: agent.safety ?? 'action',
188187
tags: agent.tags,
189-
inputSchema: schema,
188+
// The agent host derives the tool's JSON-Schema input from these.
189+
args: agent.args,
190190
handler: async (args: unknown) =>
191-
this.execute(command.id, ...coercePositionalArgs(args, agent.args, unwrapped)),
191+
this.execute(command.id, ...coercePositionalArgs(args, agent.args)),
192192
})
193193
}
194194
for (const child of command.children ?? [])
@@ -203,17 +203,17 @@ export class DevframeCommandsHost implements DevframeCommandsHostType {
203203
/**
204204
* Map the single-object args an MCP client sends onto the command handler's
205205
* positional parameters, mirroring the agent host's RPC coercion: no declared
206-
* schemas → zero-arg call; a single unwrapped object schema → the object
207-
* itself; positional schemas → `arg0..argN` keys in order.
206+
* schemas → zero-arg call; a single `v.object(...)` schema (unwrapped at the
207+
* tool boundary) → the object itself; positional schemas → `arg0..argN` keys
208+
* in order.
208209
*/
209210
function coercePositionalArgs(
210211
args: unknown,
211-
schemas: readonly unknown[] | undefined,
212-
unwrapped: boolean,
212+
schemas: DevframeCommandAgentOptions['args'],
213213
): unknown[] {
214214
if (!schemas || schemas.length === 0)
215215
return []
216-
if (unwrapped)
216+
if (schemas.length === 1 && schemas[0]!.type === 'object')
217217
return [args ?? {}]
218218
const obj = (args ?? {}) as Record<string, unknown>
219219
return schemas.map((_, i) => obj[`arg${i}`])

plans/031-agent-native-mcp-wave.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ literal "/_next/mcp" shape on devframe primitives.
6969
resource projection (which stays — many clients only consume tools).
7070
7. **Hub commands → agent bridge** — opt-in `agent?: { description, safety?,
7171
args? }` on `DevframeServerCommandInput` (mirrors the RPC convention;
72-
description required; optional valibot args schema reusing
73-
`valibotArgsToJsonSchema`, zero-arg default). `createHubContext` projects
72+
description required; optional valibot args schemas carried through
73+
`AgentToolInput.args` — JSON-Schema conversion stays an internal detail of
74+
the agent host/MCP adapter; zero-arg default). `createHubContext` projects
7475
agent-flagged, handler-bearing server commands into `ctx.agent` tools,
7576
tracking register/update/unregister. `when` clauses evaluate client-side
7677
only and are **not** enforced for agent calls — documented caveat.

0 commit comments

Comments
 (0)