2026-07-26 13:07:27 +08:00
|
|
|
import type { GenerateOptions, LlmModelReasoningInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
|
2026-06-11 15:17:56 +08:00
|
|
|
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
|
Implement the agent loop plugin
@deepseek-ai/dsh-agent-loop: LoopAgent (inbox with queued + steering
FIFOs, per-step AbortController) and the streaming-first
session/turn/step loop. Extension seams: agent/request,
agent/step-result, agent/turn-continuation waterfalls; raw chunks
logged for replay while BlockAssembler builds the assembled message;
steering drains between steps; session/flush awaited at turn end.
16 tests with a scripted mock adapter cover turn lifecycle ordering,
tool round-trips, steering, inject(), continuation override/veto,
mid-stream abort, queued turn chaining, replay equivalence, and
mid-turn fiber disposal (HMR safety).
2026-06-11 10:54:31 +08:00
|
|
|
|
|
|
|
|
/** Helpers to write scripted responses tersely. */
|
|
|
|
|
export function textResponse(text: string): StreamChunk[] {
|
|
|
|
|
return [
|
|
|
|
|
{ type: 'block-start', index: 0, blockType: 'text' },
|
Add ESLint: typescript-eslint strict-type-checked + stylistic formatting
Flat config with two layers. Correctness (type-checked): the headline
rules for this codebase are no-floating-promises / no-misused-promises
(a lost promise in the agent loop is our primary bug class),
switch-exhaustiveness-check (we switch over merge-extensible unions
everywhere), no-unnecessary-condition, require-await, and
no-explicit-any. Style (@stylistic): 2-space, no semicolons, single
quotes, trailing commas, max-len 140 — the existing house style, now
enforced instead of drifting between agents. vendor/ is excluded
(vendored source keeps upstream style); tests relax the rules that
fight test ergonomics (non-null assertions after expects, async mock
signatures, non-Error throws).
Code adjusted to pass: registry disposers wrap ctx.effect's
promise-returning disposer behind a sync () => void (our public API),
BlockAssembler gains an invariant-checking mustGet instead of non-null
assertions, lastTurnNumber uses findLast, waterfall tails return
Promise.resolve instead of async-without-await arrows, and the two
deliberate suppressions (non-exhaustive derivation switch, unbound
execute pass-through) carry justification comments.
yarn lint / yarn lint:fix added.
2026-06-11 14:17:58 +08:00
|
|
|
...Array.from(text, (char): StreamChunk => ({ type: 'text-delta', index: 0, text: char })),
|
Implement the agent loop plugin
@deepseek-ai/dsh-agent-loop: LoopAgent (inbox with queued + steering
FIFOs, per-step AbortController) and the streaming-first
session/turn/step loop. Extension seams: agent/request,
agent/step-result, agent/turn-continuation waterfalls; raw chunks
logged for replay while BlockAssembler builds the assembled message;
steering drains between steps; session/flush awaited at turn end.
16 tests with a scripted mock adapter cover turn lifecycle ordering,
tool round-trips, steering, inject(), continuation override/veto,
mid-stream abort, queued turn chaining, replay equivalence, and
mid-turn fiber disposal (HMR safety).
2026-06-11 10:54:31 +08:00
|
|
|
{ type: 'block-end', index: 0, block: { type: 'text', text } },
|
|
|
|
|
{ type: 'usage', usage: { inputTokens: 10, outputTokens: text.length } },
|
|
|
|
|
{ type: 'finish', reason: { kind: 'stop' } },
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 23:53:47 +08:00
|
|
|
/**
|
|
|
|
|
* Like {@link textResponse} but the stream ends with a `max-tokens` finish —
|
|
|
|
|
* the model was cut off at the output-token ceiling (DeepSeek's `length`).
|
|
|
|
|
* Used to exercise the turn-end `max-tokens` surfacing rule.
|
|
|
|
|
*/
|
|
|
|
|
export function maxTokensResponse(text: string): StreamChunk[] {
|
|
|
|
|
return [
|
|
|
|
|
{ type: 'block-start', index: 0, blockType: 'text' },
|
|
|
|
|
...Array.from(text, (char): StreamChunk => ({ type: 'text-delta', index: 0, text: char })),
|
|
|
|
|
{ type: 'block-end', index: 0, block: { type: 'text', text } },
|
|
|
|
|
{ type: 'usage', usage: { inputTokens: 10, outputTokens: text.length } },
|
|
|
|
|
{ type: 'finish', reason: { kind: 'max-tokens' } },
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 15:17:56 +08:00
|
|
|
export function toolCallResponse(rawCallId: string, name: string, args: object, text?: string): StreamChunk[] {
|
|
|
|
|
const callId = CallId(rawCallId)
|
Implement the agent loop plugin
@deepseek-ai/dsh-agent-loop: LoopAgent (inbox with queued + steering
FIFOs, per-step AbortController) and the streaming-first
session/turn/step loop. Extension seams: agent/request,
agent/step-result, agent/turn-continuation waterfalls; raw chunks
logged for replay while BlockAssembler builds the assembled message;
steering drains between steps; session/flush awaited at turn end.
16 tests with a scripted mock adapter cover turn lifecycle ordering,
tool round-trips, steering, inject(), continuation override/veto,
mid-stream abort, queued turn chaining, replay equivalence, and
mid-turn fiber disposal (HMR safety).
2026-06-11 10:54:31 +08:00
|
|
|
const argumentsJson = JSON.stringify(args)
|
|
|
|
|
const chunks: StreamChunk[] = []
|
|
|
|
|
let index = 0
|
|
|
|
|
if (text) {
|
|
|
|
|
chunks.push(
|
|
|
|
|
{ type: 'block-start', index, blockType: 'text' },
|
|
|
|
|
{ type: 'text-delta', index, text },
|
|
|
|
|
{ type: 'block-end', index, block: { type: 'text', text } },
|
|
|
|
|
)
|
|
|
|
|
index += 1
|
|
|
|
|
}
|
|
|
|
|
chunks.push(
|
|
|
|
|
{ type: 'block-start', index, blockType: 'tool-call' },
|
|
|
|
|
{ type: 'tool-call-delta', index, id: callId, name, argumentsDelta: argumentsJson.slice(0, 5) },
|
|
|
|
|
{ type: 'tool-call-delta', index, id: callId, argumentsDelta: argumentsJson.slice(5) },
|
|
|
|
|
{
|
|
|
|
|
type: 'block-end',
|
|
|
|
|
index,
|
|
|
|
|
block: { type: 'tool-call', id: callId, name, arguments: argumentsJson },
|
|
|
|
|
},
|
|
|
|
|
{ type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } },
|
|
|
|
|
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
|
|
|
|
)
|
|
|
|
|
return chunks
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mock adapter driven by a script: each model call consumes the next entry.
|
|
|
|
|
* Records every request it receives for assertions. An entry may be a
|
|
|
|
|
* function to compute chunks from the request, or a 'hang' marker that
|
|
|
|
|
* streams one chunk then waits until aborted.
|
|
|
|
|
*/
|
|
|
|
|
export class MockAdapter extends LlmAdapter {
|
|
|
|
|
requests: GenerateOptions[] = []
|
|
|
|
|
|
2026-07-25 07:47:51 +08:00
|
|
|
constructor(
|
|
|
|
|
private script: (StreamChunk[] | ((options: GenerateOptions) => StreamChunk[]) | 'hang')[],
|
|
|
|
|
private readonly reasoning?: LlmModelReasoningInfo,
|
|
|
|
|
) {
|
Implement the agent loop plugin
@deepseek-ai/dsh-agent-loop: LoopAgent (inbox with queued + steering
FIFOs, per-step AbortController) and the streaming-first
session/turn/step loop. Extension seams: agent/request,
agent/step-result, agent/turn-continuation waterfalls; raw chunks
logged for replay while BlockAssembler builds the assembled message;
steering drains between steps; session/flush awaited at turn end.
16 tests with a scripted mock adapter cover turn lifecycle ordering,
tool round-trips, steering, inject(), continuation override/veto,
mid-stream abort, queued turn chaining, replay equivalence, and
mid-turn fiber disposal (HMR safety).
2026-06-11 10:54:31 +08:00
|
|
|
super()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 13:07:27 +08:00
|
|
|
override resolveModel(
|
|
|
|
|
provider: string,
|
|
|
|
|
model: string,
|
|
|
|
|
): Promise<LlmResolvedModelInfo> {
|
|
|
|
|
return Promise.resolve({
|
|
|
|
|
provider,
|
|
|
|
|
id: model,
|
|
|
|
|
name: model,
|
|
|
|
|
...this.reasoning === undefined ? {} : { reasoning: this.reasoning },
|
|
|
|
|
})
|
2026-07-25 07:47:51 +08:00
|
|
|
}
|
|
|
|
|
|
Implement the agent loop plugin
@deepseek-ai/dsh-agent-loop: LoopAgent (inbox with queued + steering
FIFOs, per-step AbortController) and the streaming-first
session/turn/step loop. Extension seams: agent/request,
agent/step-result, agent/turn-continuation waterfalls; raw chunks
logged for replay while BlockAssembler builds the assembled message;
steering drains between steps; session/flush awaited at turn end.
16 tests with a scripted mock adapter cover turn lifecycle ordering,
tool round-trips, steering, inject(), continuation override/veto,
mid-stream abort, queued turn chaining, replay equivalence, and
mid-turn fiber disposal (HMR safety).
2026-06-11 10:54:31 +08:00
|
|
|
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
|
|
|
this.requests.push(options)
|
|
|
|
|
const entry = this.script.shift()
|
|
|
|
|
if (!entry) throw new Error('MockAdapter: script exhausted')
|
|
|
|
|
if (entry === 'hang') {
|
|
|
|
|
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
|
|
|
yield { type: 'text-delta', index: 0, text: 'partial' }
|
Enable maximum-strict TypeScript across our packages
tsconfig.base.json adds noUncheckedIndexedAccess,
exactOptionalPropertyTypes, noImplicitOverride,
noFallthroughCasesInSwitch, noUnusedLocals, and noUnusedParameters on
top of strict. Vendored packages opt out of the new flags locally
(their tsconfigs are ours to regenerate; their source is not), keeping
upstream-sync friendliness.
Our code fixed accordingly: index accesses acknowledge undefined
(assembler flush cursors, lastTurnNumber); optional properties are
omitted instead of set-to-undefined (GenerateResult.usage,
ToolDefinition.strict, GenerateOptions.system/tools, error payloads
via an errorData helper); Session.onAppend is explicitly
`(…) => void | undefined`; tests and examples updated for unused
parameters and indexed access.
2026-06-11 14:02:47 +08:00
|
|
|
await new Promise<void>((_resolve, reject) => {
|
Add ESLint: typescript-eslint strict-type-checked + stylistic formatting
Flat config with two layers. Correctness (type-checked): the headline
rules for this codebase are no-floating-promises / no-misused-promises
(a lost promise in the agent loop is our primary bug class),
switch-exhaustiveness-check (we switch over merge-extensible unions
everywhere), no-unnecessary-condition, require-await, and
no-explicit-any. Style (@stylistic): 2-space, no semicolons, single
quotes, trailing commas, max-len 140 — the existing house style, now
enforced instead of drifting between agents. vendor/ is excluded
(vendored source keeps upstream style); tests relax the rules that
fight test ergonomics (non-null assertions after expects, async mock
signatures, non-Error throws).
Code adjusted to pass: registry disposers wrap ctx.effect's
promise-returning disposer behind a sync () => void (our public API),
BlockAssembler gains an invariant-checking mustGet instead of non-null
assertions, lastTurnNumber uses findLast, waterfall tails return
Promise.resolve instead of async-without-await arrows, and the two
deliberate suppressions (non-exhaustive derivation switch, unbound
execute pass-through) carry justification comments.
yarn lint / yarn lint:fix added.
2026-06-11 14:17:58 +08:00
|
|
|
if (options.signal?.aborted) { reject(new Error('aborted')); return }
|
|
|
|
|
options.signal?.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
|
Implement the agent loop plugin
@deepseek-ai/dsh-agent-loop: LoopAgent (inbox with queued + steering
FIFOs, per-step AbortController) and the streaming-first
session/turn/step loop. Extension seams: agent/request,
agent/step-result, agent/turn-continuation waterfalls; raw chunks
logged for replay while BlockAssembler builds the assembled message;
steering drains between steps; session/flush awaited at turn end.
16 tests with a scripted mock adapter cover turn lifecycle ordering,
tool round-trips, steering, inject(), continuation override/veto,
mid-stream abort, queued turn chaining, replay equivalence, and
mid-turn fiber disposal (HMR safety).
2026-06-11 10:54:31 +08:00
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const chunks = typeof entry === 'function' ? entry(options) : entry
|
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
|
if (options.signal?.aborted) throw new Error('aborted')
|
|
|
|
|
yield chunk
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|