@catheadowl/dsh-eval 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,118 +1,118 @@
1
- /**
2
- * Model-independent review experiment primitives.
3
- *
4
- * A review experiment separates four concerns:
5
- * - frozen inputs owned by the plugin;
6
- * - live observation/projection of those inputs;
7
- * - the blind prompt shown to an independent reviewer;
8
- * - the hidden human rubric used after the run.
9
- *
10
- * Nothing in this module knows how a model is invoked. Callers provide an
11
- * executor (dsh headless is one adapter) when they want to run the assembled
12
- * task.
13
- */
14
-
15
- const OBSERVATIONS_PLACEHOLDER = '{{EVAL_OBSERVATIONS}}'
16
-
17
- /** Define and validate a human-graded review experiment. */
18
- export function defineReviewExperiment(definition) {
19
- if (definition === null || typeof definition !== 'object') {
20
- throw new TypeError('review experiment must be an object')
21
- }
22
- if (!/^[a-z0-9][a-z0-9._-]*$/i.test(definition.id ?? '')) {
23
- throw new TypeError('review experiment id must be a non-empty path-safe string')
24
- }
25
- if (typeof definition.prompt !== 'string' || definition.prompt.split(OBSERVATIONS_PLACEHOLDER).length !== 2) {
26
- throw new TypeError(`review experiment '${definition.id}': prompt must contain exactly one ${OBSERVATIONS_PLACEHOLDER}`)
27
- }
28
- if (typeof definition.observe !== 'function') {
29
- throw new TypeError(`review experiment '${definition.id}': observe must be a function`)
30
- }
31
- if (!(typeof definition.rubric === 'string' || definition.rubric instanceof URL)) {
32
- throw new TypeError(`review experiment '${definition.id}': rubric must identify the hidden grading standard`)
33
- }
34
- const defaultRuns = definition.defaultRuns ?? 3
35
- if (!Number.isInteger(defaultRuns) || defaultRuns < 1) {
36
- throw new TypeError(`review experiment '${definition.id}': defaultRuns must be a positive integer`)
37
- }
38
- return Object.freeze({
39
- ...definition,
40
- kind: 'review',
41
- defaultRuns,
42
- })
43
- }
44
-
45
- /** Render the standard observation document consumed by a blind reviewer. */
46
- export function renderObservationSections(sections) {
47
- if (!Array.isArray(sections) || sections.length === 0) {
48
- throw new TypeError('review observations must contain at least one section')
49
- }
50
- const lines = []
51
- for (const section of sections) {
52
- if (typeof section?.heading !== 'string' || section.heading.length === 0) {
53
- throw new TypeError('every observation section needs a heading')
54
- }
55
- lines.push(`## ${section.heading}`)
56
- if (section.introduction) lines.push(String(section.introduction), '')
57
- for (const entry of section.entries ?? []) {
58
- if (typeof entry?.heading !== 'string' || entry.heading.length === 0) {
59
- throw new TypeError(`section '${section.heading}' contains an entry without a heading`)
60
- }
61
- lines.push(`### ${entry.heading}`)
62
- for (const paragraph of entry.paragraphs ?? []) lines.push(String(paragraph))
63
- if (entry.call !== undefined) lines.push(`Call: ${JSON.stringify(entry.call)}`)
64
- if (entry.json !== undefined) {
65
- lines.push('```json', JSON.stringify(entry.json, null, 2), '```')
66
- }
67
- lines.push('')
68
- }
69
- }
70
- return lines.join('\n').trimEnd()
71
- }
72
-
73
- /** Materialize live observations and assemble the blind reviewer task. */
74
- export async function materializeReviewExperiment(experiment) {
75
- const sections = await experiment.observe()
76
- const observations = renderObservationSections(sections)
77
- return {
78
- experimentId: experiment.id,
79
- observations,
80
- task: experiment.prompt.replace(OBSERVATIONS_PLACEHOLDER, observations),
81
- }
82
- }
83
-
84
- /**
85
- * Execute the same materialized task through N fresh executor calls.
86
- * The executor is intentionally generic: `(task, context) => result`.
87
- */
88
- export async function executeReviewExperiment(experiment, executor, options = {}) {
89
- if (typeof executor !== 'function') throw new TypeError('review executor must be a function')
90
- const runs = options.runs ?? experiment.defaultRuns
91
- if (!Number.isInteger(runs) || runs < 1) throw new TypeError('runs must be a positive integer')
92
-
93
- // Observe once. Every reviewer sees byte-identical evidence, so variance is
94
- // attributable to interpretation rather than fixture/projection drift.
95
- const materialized = await materializeReviewExperiment(experiment)
96
- const attempts = []
97
- for (let index = 1; index <= runs; index += 1) {
98
- try {
99
- const result = await executor(materialized.task, {
100
- experiment,
101
- experimentId: experiment.id,
102
- index,
103
- runs,
104
- })
105
- attempts.push({ index, ok: true, result })
106
- } catch (error) {
107
- attempts.push({
108
- index,
109
- ok: false,
110
- error: error instanceof Error ? error.message : String(error),
111
- result: error?.result,
112
- })
113
- }
114
- }
115
- return { ...materialized, runs, attempts }
116
- }
117
-
118
- export { OBSERVATIONS_PLACEHOLDER }
1
+ /**
2
+ * Model-independent review experiment primitives.
3
+ *
4
+ * A review experiment separates four concerns:
5
+ * - frozen inputs owned by the plugin;
6
+ * - live observation/projection of those inputs;
7
+ * - the blind prompt shown to an independent reviewer;
8
+ * - the hidden human rubric used after the run.
9
+ *
10
+ * Nothing in this module knows how a model is invoked. Callers provide an
11
+ * executor (dsh headless is one adapter) when they want to run the assembled
12
+ * task.
13
+ */
14
+
15
+ const OBSERVATIONS_PLACEHOLDER = '{{EVAL_OBSERVATIONS}}'
16
+
17
+ /** Define and validate a human-graded review experiment. */
18
+ export function defineReviewExperiment(definition) {
19
+ if (definition === null || typeof definition !== 'object') {
20
+ throw new TypeError('review experiment must be an object')
21
+ }
22
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(definition.id ?? '')) {
23
+ throw new TypeError('review experiment id must be a non-empty path-safe string')
24
+ }
25
+ if (typeof definition.prompt !== 'string' || definition.prompt.split(OBSERVATIONS_PLACEHOLDER).length !== 2) {
26
+ throw new TypeError(`review experiment '${definition.id}': prompt must contain exactly one ${OBSERVATIONS_PLACEHOLDER}`)
27
+ }
28
+ if (typeof definition.observe !== 'function') {
29
+ throw new TypeError(`review experiment '${definition.id}': observe must be a function`)
30
+ }
31
+ if (!(typeof definition.rubric === 'string' || definition.rubric instanceof URL)) {
32
+ throw new TypeError(`review experiment '${definition.id}': rubric must identify the hidden grading standard`)
33
+ }
34
+ const defaultRuns = definition.defaultRuns ?? 3
35
+ if (!Number.isInteger(defaultRuns) || defaultRuns < 1) {
36
+ throw new TypeError(`review experiment '${definition.id}': defaultRuns must be a positive integer`)
37
+ }
38
+ return Object.freeze({
39
+ ...definition,
40
+ kind: 'review',
41
+ defaultRuns,
42
+ })
43
+ }
44
+
45
+ /** Render the standard observation document consumed by a blind reviewer. */
46
+ export function renderObservationSections(sections) {
47
+ if (!Array.isArray(sections) || sections.length === 0) {
48
+ throw new TypeError('review observations must contain at least one section')
49
+ }
50
+ const lines = []
51
+ for (const section of sections) {
52
+ if (typeof section?.heading !== 'string' || section.heading.length === 0) {
53
+ throw new TypeError('every observation section needs a heading')
54
+ }
55
+ lines.push(`## ${section.heading}`)
56
+ if (section.introduction) lines.push(String(section.introduction), '')
57
+ for (const entry of section.entries ?? []) {
58
+ if (typeof entry?.heading !== 'string' || entry.heading.length === 0) {
59
+ throw new TypeError(`section '${section.heading}' contains an entry without a heading`)
60
+ }
61
+ lines.push(`### ${entry.heading}`)
62
+ for (const paragraph of entry.paragraphs ?? []) lines.push(String(paragraph))
63
+ if (entry.call !== undefined) lines.push(`Call: ${JSON.stringify(entry.call)}`)
64
+ if (entry.json !== undefined) {
65
+ lines.push('```json', JSON.stringify(entry.json, null, 2), '```')
66
+ }
67
+ lines.push('')
68
+ }
69
+ }
70
+ return lines.join('\n').trimEnd()
71
+ }
72
+
73
+ /** Materialize live observations and assemble the blind reviewer task. */
74
+ export async function materializeReviewExperiment(experiment) {
75
+ const sections = await experiment.observe()
76
+ const observations = renderObservationSections(sections)
77
+ return {
78
+ experimentId: experiment.id,
79
+ observations,
80
+ task: experiment.prompt.replace(OBSERVATIONS_PLACEHOLDER, observations),
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Execute the same materialized task through N fresh executor calls.
86
+ * The executor is intentionally generic: `(task, context) => result`.
87
+ */
88
+ export async function executeReviewExperiment(experiment, executor, options = {}) {
89
+ if (typeof executor !== 'function') throw new TypeError('review executor must be a function')
90
+ const runs = options.runs ?? experiment.defaultRuns
91
+ if (!Number.isInteger(runs) || runs < 1) throw new TypeError('runs must be a positive integer')
92
+
93
+ // Observe once. Every reviewer sees byte-identical evidence, so variance is
94
+ // attributable to interpretation rather than fixture/projection drift.
95
+ const materialized = await materializeReviewExperiment(experiment)
96
+ const attempts = []
97
+ for (let index = 1; index <= runs; index += 1) {
98
+ try {
99
+ const result = await executor(materialized.task, {
100
+ experiment,
101
+ experimentId: experiment.id,
102
+ index,
103
+ runs,
104
+ })
105
+ attempts.push({ index, ok: true, result })
106
+ } catch (error) {
107
+ attempts.push({
108
+ index,
109
+ ok: false,
110
+ error: error instanceof Error ? error.message : String(error),
111
+ result: error?.result,
112
+ })
113
+ }
114
+ }
115
+ return { ...materialized, runs, attempts }
116
+ }
117
+
118
+ export { OBSERVATIONS_PLACEHOLDER }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Escape-hatch entry: mechanism primitives for ad-hoc diagnostic scripts and
3
+ * self-built execution faces. Reachable as `<pkg>/experimental`.
4
+ *
5
+ * Contract: NO compatibility promise — symbols here may change or move in
6
+ * any minor release. The stable surface for eval/review case authors is the
7
+ * package root entry (src/index.mjs); import from here only when you are
8
+ * building your own runner/driver and accept the follow-up cost.
9
+ */
10
+
11
+ // --- host CLI resolution (modern three-segment chain) ---
12
+ export { resolveDshCliChain } from './cli.mjs'
13
+
14
+ // --- sandbox / overlay mechanism ---
15
+ export { stageProfileStore } from './sandbox.mjs'
16
+ export { buildOverlayYaml, overlayDisableRows } from './overlay.mjs'
17
+
18
+ // --- session-trace primitives ---
19
+ export { parseSessionLog, buildTrace, loadTraceDir } from './trace.mjs'
20
+
21
+ // --- review experiment execution layer ---
22
+ export {
23
+ executeReviewExperiment,
24
+ materializeReviewExperiment,
25
+ renderObservationSections,
26
+ OBSERVATIONS_PLACEHOLDER,
27
+ } from './experiment/review.mjs'
28
+
29
+ // --- dsh review executors ---
30
+ export {
31
+ createDshHeadlessReviewExecutor,
32
+ runDshReviewExperiment,
33
+ } from './adapters/dsh/review.mjs'
34
+
35
+ // --- tool-boundary validation ---
36
+ export {
37
+ validateToolBoundary,
38
+ renderToolBoundaryEvidence,
39
+ } from './tool-validation.mjs'
package/src/index.mjs CHANGED
@@ -1,49 +1,38 @@
1
- /**
2
- * @catheadowl/dsh-eval public surface — what plugin eval cases import:
3
- * matchers for the `expect` list and mock-script step builders. The runner
4
- * and trace parser are bin/runner internals, also exported for ad-hoc use.
5
- */
6
-
7
- export {
8
- toolCalled,
9
- toolNotCalled,
10
- firstTool,
11
- toolSequence,
12
- toolCallArgs,
13
- toolResultFor,
14
- toolResultIsError,
15
- toolResultSucceeded,
16
- toolResultTextIncludes,
17
- finalTextIncludes,
18
- finalTextMatches,
19
- assistantTextIncludes,
20
- systemPromptIncludes,
21
- toolMounted,
22
- userMessageTextIncludes,
23
- userMessageTextExcludes,
24
- } from './assertions.mjs'
25
-
26
- export { toolCallStep, textStep } from './mock/script.mjs'
27
-
28
- export { runEvalCase, buildOverlayYaml, looksLikeDshRepo, stageProfileStore, FRAMEWORK_ROOT } from './runner.mjs'
29
-
30
- export { parseSessionLog, buildTrace, loadTraceDir } from './trace.mjs'
31
-
32
- export {
33
- defineReviewExperiment,
34
- executeReviewExperiment,
35
- materializeReviewExperiment,
36
- renderObservationSections,
37
- OBSERVATIONS_PLACEHOLDER,
38
- } from './experiment/review.mjs'
39
-
40
- export {
41
- createDshHeadlessReviewExecutor,
42
- resolveDshCli,
43
- runDshReviewExperiment,
44
- } from './adapters/dsh/review.mjs'
45
-
46
- export {
47
- validateToolBoundary,
48
- renderToolBoundaryEvidence,
49
- } from './tool-validation.mjs'
1
+ /**
2
+ * @catheadowl/dsh-eval stable SDK surface — what eval/review case authors
3
+ * import: assertion matchers, mock-script step builders, the review
4
+ * experiment DSL, and the programmatic case runner.
5
+ *
6
+ * Mechanism primitives (sandbox/overlay/trace, review executors, CLI chain)
7
+ * live behind the `./experimental` subpath with no compatibility promise;
8
+ * everything else is bin-internal.
9
+ */
10
+
11
+ export {
12
+ toolCalled,
13
+ toolNotCalled,
14
+ firstTool,
15
+ toolSequence,
16
+ toolCallArgs,
17
+ toolResultFor,
18
+ toolResultIsError,
19
+ toolResultSucceeded,
20
+ toolResultTextIncludes,
21
+ finalTextIncludes,
22
+ finalTextMatches,
23
+ assistantTextIncludes,
24
+ systemPromptIncludes,
25
+ toolMounted,
26
+ userMessageTextIncludes,
27
+ userMessageTextExcludes,
28
+ subagentDispatched,
29
+ subagentCompleted,
30
+ subagentDispatchCount,
31
+ subagentCompletedCount,
32
+ } from './assertions.mjs'
33
+
34
+ export { toolCallStep, textStep } from './mock/script.mjs'
35
+
36
+ export { runEvalCase } from './runner.mjs'
37
+
38
+ export { defineReviewExperiment } from './experiment/review.mjs'
@@ -1,73 +1,73 @@
1
- /**
2
- * The eval mock-LLM adapter, loaded as a Cordis plugin through the eval
3
- * overlay (`name: file://...` insert). It registers the keyless `eval-mock`
4
- * provider and replays a scripted StreamChunk sequence: one script step per
5
- * model call, so a case controls exactly which tool calls and final text the
6
- * "model" produces. Pattern precedent:
7
- * deepseek-harness/examples/headless-agent/tests/fixtures/cli-mock-llm.ts.
8
- *
9
- * Deterministic layer purpose: verify the eval runner/trace/assertion
10
- * pipeline without an API key, and drive the plugin's tool execution path
11
- * (a scripted tool call runs through the real tool pipeline).
12
- */
13
-
14
- import { readFileSync } from 'node:fs'
15
- import { LlmAdapter } from '@deepseek-ai/dsh-llm'
16
-
17
- /** Environment variable pointing at this run's mock script JSON. */
18
- const SCRIPT_ENV = 'DSH_EVAL_MOCK_SCRIPT'
19
-
20
- /** Adapter that yields one scripted chunk list per stream() call. */
21
- class EvalMockAdapter extends LlmAdapter {
22
- constructor(script) {
23
- super()
24
- this.steps = script.steps ?? []
25
- this.cursor = 0
26
- }
27
-
28
- async resolveModel(provider, model) {
29
- return {
30
- provider,
31
- id: model,
32
- name: model,
33
- reasoning: {
34
- efforts: [
35
- { id: 'off', name: 'Off' },
36
- { id: 'high', name: 'High' },
37
- ],
38
- defaultEffort: 'off',
39
- },
40
- }
41
- }
42
-
43
- async * stream(_options) {
44
- const step = this.steps[this.cursor]
45
- this.cursor += 1
46
- if (step === undefined) {
47
- // Script exhausted: finish the loop loudly-but-gracefully so the run
48
- // still produces a trace a case can assert on.
49
- const text = `eval-mock: script exhausted after ${this.cursor - 1} step(s)`
50
- yield { type: 'block-start', index: 0, blockType: 'text' }
51
- yield { type: 'text-delta', index: 0, text }
52
- yield { type: 'block-end', index: 0, block: { type: 'text', text } }
53
- yield { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }
54
- yield { type: 'finish', reason: { kind: 'stop' } }
55
- return
56
- }
57
- for (const chunk of step) yield chunk
58
- }
59
- }
60
-
61
- export const name = 'eval-mock-llm'
62
-
63
- export const inject = ['llm']
64
-
65
- /** Register the `eval-mock` adapter from the scripted run. */
66
- export function apply(ctx) {
67
- const scriptPath = process.env[SCRIPT_ENV]
68
- if (scriptPath === undefined) {
69
- throw new Error(`eval-mock-llm: ${SCRIPT_ENV} must point at the run's mock script JSON`)
70
- }
71
- const script = JSON.parse(readFileSync(scriptPath, 'utf8'))
72
- ctx.llm.registerAdapter(['eval-mock'], new EvalMockAdapter(script))
73
- }
1
+ /**
2
+ * The eval mock-LLM adapter, loaded as a Cordis plugin through the eval
3
+ * overlay (`name: file://...` insert). It registers the keyless `eval-mock`
4
+ * provider and replays a scripted StreamChunk sequence: one script step per
5
+ * model call, so a case controls exactly which tool calls and final text the
6
+ * "model" produces. Pattern precedent:
7
+ * deepseek-harness/examples/headless-agent/tests/fixtures/cli-mock-llm.ts.
8
+ *
9
+ * Deterministic layer purpose: verify the eval runner/trace/assertion
10
+ * pipeline without an API key, and drive the plugin's tool execution path
11
+ * (a scripted tool call runs through the real tool pipeline).
12
+ */
13
+
14
+ import { readFileSync } from 'node:fs'
15
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm'
16
+
17
+ /** Environment variable pointing at this run's mock script JSON. */
18
+ const SCRIPT_ENV = 'DSH_EVAL_MOCK_SCRIPT'
19
+
20
+ /** Adapter that yields one scripted chunk list per stream() call. */
21
+ class EvalMockAdapter extends LlmAdapter {
22
+ constructor(script) {
23
+ super()
24
+ this.steps = script.steps ?? []
25
+ this.cursor = 0
26
+ }
27
+
28
+ async resolveModel(provider, model) {
29
+ return {
30
+ provider,
31
+ id: model,
32
+ name: model,
33
+ reasoning: {
34
+ efforts: [
35
+ { id: 'off', name: 'Off' },
36
+ { id: 'high', name: 'High' },
37
+ ],
38
+ defaultEffort: 'off',
39
+ },
40
+ }
41
+ }
42
+
43
+ async * stream(_options) {
44
+ const step = this.steps[this.cursor]
45
+ this.cursor += 1
46
+ if (step === undefined) {
47
+ // Script exhausted: finish the loop loudly-but-gracefully so the run
48
+ // still produces a trace a case can assert on.
49
+ const text = `eval-mock: script exhausted after ${this.cursor - 1} step(s)`
50
+ yield { type: 'block-start', index: 0, blockType: 'text' }
51
+ yield { type: 'text-delta', index: 0, text }
52
+ yield { type: 'block-end', index: 0, block: { type: 'text', text } }
53
+ yield { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }
54
+ yield { type: 'finish', reason: { kind: 'stop' } }
55
+ return
56
+ }
57
+ for (const chunk of step) yield chunk
58
+ }
59
+ }
60
+
61
+ export const name = 'eval-mock-llm'
62
+
63
+ export const inject = ['llm']
64
+
65
+ /** Register the `eval-mock` adapter from the scripted run. */
66
+ export function apply(ctx) {
67
+ const scriptPath = process.env[SCRIPT_ENV]
68
+ if (scriptPath === undefined) {
69
+ throw new Error(`eval-mock-llm: ${SCRIPT_ENV} must point at the run's mock script JSON`)
70
+ }
71
+ const script = JSON.parse(readFileSync(scriptPath, 'utf8'))
72
+ ctx.llm.registerAdapter(['eval-mock'], new EvalMockAdapter(script))
73
+ }
@@ -1,49 +1,49 @@
1
- /**
2
- * Script builders for the eval mock-LLM layer. A mock script is
3
- * `{ steps: ChunkStep[] }` where each step is the exact StreamChunk list one
4
- * model call yields (its `finish` chunk included); the adapter consumes one
5
- * step per stream() call. Cases build steps with these helpers instead of
6
- * hand-writing chunk JSON.
7
- */
8
-
9
- let autoId = 0
10
-
11
- /** Next deterministic call id for generated steps. */
12
- function nextCallId() {
13
- autoId += 1
14
- return `eval-mock-call-${autoId}`
15
- }
16
-
17
- /**
18
- * One model call that requests a single tool invocation, then finishes with
19
- * `kind: 'tool-calls'` so the loop executes the tool and calls back.
20
- * @param {string} name - the model-facing tool name to call.
21
- * @param {object} [args] - the tool arguments (serialized as the model's raw JSON).
22
- * @param {{ id?: string }} [options] - override the generated call id.
23
- */
24
- export function toolCallStep(name, args = {}, options = {}) {
25
- const id = options.id ?? nextCallId()
26
- const json = JSON.stringify(args)
27
- return [
28
- { type: 'block-start', index: 0, blockType: 'tool-call' },
29
- { type: 'tool-call-delta', index: 0, id, name, argumentsDelta: json },
30
- { type: 'block-end', index: 0, block: { type: 'tool-call', id, name, arguments: json } },
31
- { type: 'usage', usage: { inputTokens: 5, outputTokens: 5 } },
32
- { type: 'finish', reason: { kind: 'tool-calls' } },
33
- ]
34
- }
35
-
36
- /**
37
- * One model call that answers plain text and stops the loop
38
- * (`kind: 'stop'`).
39
- * @param {string} text - the assistant reply.
40
- */
41
- export function textStep(text) {
42
- return [
43
- { type: 'block-start', index: 0, blockType: 'text' },
44
- { type: 'text-delta', index: 0, text },
45
- { type: 'block-end', index: 0, block: { type: 'text', text } },
46
- { type: 'usage', usage: { inputTokens: 5, outputTokens: 5 } },
47
- { type: 'finish', reason: { kind: 'stop' } },
48
- ]
49
- }
1
+ /**
2
+ * Script builders for the eval mock-LLM layer. A mock script is
3
+ * `{ steps: ChunkStep[] }` where each step is the exact StreamChunk list one
4
+ * model call yields (its `finish` chunk included); the adapter consumes one
5
+ * step per stream() call. Cases build steps with these helpers instead of
6
+ * hand-writing chunk JSON.
7
+ */
8
+
9
+ let autoId = 0
10
+
11
+ /** Next deterministic call id for generated steps. */
12
+ function nextCallId() {
13
+ autoId += 1
14
+ return `eval-mock-call-${autoId}`
15
+ }
16
+
17
+ /**
18
+ * One model call that requests a single tool invocation, then finishes with
19
+ * `kind: 'tool-calls'` so the loop executes the tool and calls back.
20
+ * @param {string} name - the model-facing tool name to call.
21
+ * @param {object} [args] - the tool arguments (serialized as the model's raw JSON).
22
+ * @param {{ id?: string }} [options] - override the generated call id.
23
+ */
24
+ export function toolCallStep(name, args = {}, options = {}) {
25
+ const id = options.id ?? nextCallId()
26
+ const json = JSON.stringify(args)
27
+ return [
28
+ { type: 'block-start', index: 0, blockType: 'tool-call' },
29
+ { type: 'tool-call-delta', index: 0, id, name, argumentsDelta: json },
30
+ { type: 'block-end', index: 0, block: { type: 'tool-call', id, name, arguments: json } },
31
+ { type: 'usage', usage: { inputTokens: 5, outputTokens: 5 } },
32
+ { type: 'finish', reason: { kind: 'tool-calls' } },
33
+ ]
34
+ }
35
+
36
+ /**
37
+ * One model call that answers plain text and stops the loop
38
+ * (`kind: 'stop'`).
39
+ * @param {string} text - the assistant reply.
40
+ */
41
+ export function textStep(text) {
42
+ return [
43
+ { type: 'block-start', index: 0, blockType: 'text' },
44
+ { type: 'text-delta', index: 0, text },
45
+ { type: 'block-end', index: 0, block: { type: 'text', text } },
46
+ { type: 'usage', usage: { inputTokens: 5, outputTokens: 5 } },
47
+ { type: 'finish', reason: { kind: 'stop' } },
48
+ ]
49
+ }