feat: add canonical typed tool outputs

This commit is contained in:
Tianyi Cui
2026-07-21 03:08:35 +08:00
parent 8500974fd4
commit 66c36e7325
173 changed files with 3298 additions and 954 deletions

View File

@@ -8,7 +8,7 @@ The model-facing `ralph` tool runs a fixed foreground workflow that gives one im
Each child receives only the immutable objective, its current Ralph round and cap, a shared-workspace-as-authority instruction, and the previous structured handoff. The workspace is long-term memory; parent conversation and prior child sessions are not seeded. Reports have `status: continue | complete | blocked`, a non-empty summary, evidence, next steps, and blocker text. Status-specific semantics and the serialized `maxHandoffChars` ceiling are validated inside the fixed workflow and again at the consumer boundary. Invalid, missing, or oversized reports fail the workflow instead of being truncated or mistaken for cap exhaustion.
The successful terminal tool result is `complete`, `blocked`, or `budget-limited`, with the last bounded report and number of rounds started. Completion and blocker labels explicitly say that a worker reported the outcome; they are not independent certification. `maxResultChars` bounds the complete successful text including its envelope and truncation marker, without altering the validated report used as a cross-round handoff.
The successful terminal tool result is `complete`, `blocked`, or `budget-limited`, with the last bounded report and number of rounds started. The canonical envelope is `{ runId, agentsStarted, result }`; completion and blocker labels in its Native renderer explicitly say that a worker reported the outcome, not independent certification. `maxResultChars` bounds only that rendered text including its truncation marker, without altering the validated report in the canonical value or the cross-round handoff.
An ordinary child failure produces an error naming the failed round and retaining the last successful handoff when one exists. Ralph does not retry that round. Fatal provider-start, transport, worker, or workflow failures remain workflow errors and may settle before the fixed script can return a handoff. Cancellation is also an error; partial output is never success.

View File

@@ -8,6 +8,7 @@
import type { Context } from 'cordis'
import z from 'schemastery'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools'
@@ -374,6 +375,13 @@ function renderResult(result: RalphRunResult, maxChars: number): string {
return boundResult(text, maxChars)
}
/** Canonical Ralph result fields shared by schema inference and rendering. */
const RALPH_OUTPUT_PROPERTIES = {
runId: { type: 'string', required: true },
agentsStarted: { type: 'integer', required: true },
result: { type: 'json', required: true },
} as const
/** Render an ordinary child failure with the most recent durable handoff. */
function renderRoundFailure(result: RalphRoundFailure, maxChars: number): string {
const header = `Ralph round ${result.roundsStarted} child failed before producing a structured report.`
@@ -415,7 +423,18 @@ export function apply(ctx: Context, config: Config): void {
description: 'Optional positive safe-integer round cap, bounded by the deployment ceiling.',
},
},
async execute(args, exec): Promise<ContentBlock[]> {
output: {
schema: {
type: 'object',
additionalProperties: false,
properties: RALPH_OUTPUT_PROPERTIES,
},
render: (_args, value) => [{
type: 'text',
text: renderResult(value.result as unknown as RalphRunResult, resolved.maxResultChars),
}],
},
async execute(args, exec) {
const parent = exec.agent
if (parent === undefined) {
throw new Error('Ralph tool requires a calling agent (exec.agent was undefined)')
@@ -444,7 +463,11 @@ export function apply(ctx: Context, config: Config): void {
if (error !== undefined) throw new Error(error)
const value = readRunResult(settled.value, maxRounds, resolved.maxHandoffChars)
if (value.status === 'round-failed') throw new Error(renderRoundFailure(value, resolved.maxResultChars))
return [{ type: 'text', text: renderResult(value, resolved.maxResultChars) }]
return {
runId: run.id,
agentsStarted: settled.agentsStarted,
result: value as unknown as JsonValue,
}
} finally {
exec.signal?.removeEventListener('abort', onAbort)
await run.dispose()

View File

@@ -156,6 +156,12 @@ describe('dsh-tool-ralph', () => {
report: COMPLETE,
})
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected Ralph success')
expect(result.value).toEqual({
runId: 'ralph-1',
agentsStarted: 1,
result: { status: 'complete', roundsStarted: 1, report: COMPLETE },
})
expect((result.content[0] as { text: string }).text)
.toContain('Ralph worker reported completion after 1 round.')
expect((result.content[0] as { text: string }).text).toContain('All required gates pass.')
@@ -279,7 +285,7 @@ describe('dsh-tool-ralph', () => {
expect((await execute(ctx, { objective: 'Work.', maxRounds }, { agent: parent })).isError).toBe(true)
}
const missing = await execute(ctx, {}, { agent: parent })
expect(missing.error?.code).toBe('INVALID_ARGS')
expect(missing.error?.info?.code).toBe('INVALID_ARGS')
expect(engine.requests).toHaveLength(0)
})