Merge branch 'codex/tool-json-schema-dsl' into codex/canonical-tool-output

# Conflicts:
#	docs/architecture.md
#	docs/config-catalog.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-write/session.jsonl
#	packages/ui/tui/src/index.ts
#	scripts/type-equiv.manifest.json
This commit is contained in:
Tianyi Cui
2026-07-22 00:23:00 +08:00
252 changed files with 11596 additions and 6037 deletions

View File

@@ -39,7 +39,7 @@ Streaming is a raw chunk protocol (`block-start`, `text-delta`, `reasoning-delta
### Call configuration (`call-config.ts`)
`LlmCallConfig` is the provider + model + sampling scalars of one conversation's requests (`provider`, `model`, `temperature`, `maxTokens`, `stop` — each mapping 1:1 onto the same-named `GenerateOptions` field). It is per-conversation state recorded in the session log as part of the request header (see the dsh-session `request/header` events), never a silently-adjustable per-call knob: the `agent/request` waterfall proposes a replacement and the loop logs a real change. `callConfigEquals(a, b)` is the field-wise real-change detector; `deepFreeze(value)` is the ownership helper the loop applies to every built request before dispatch (`llm/stream` listeners and adapters read, never rewrite).
`LlmCallConfig` is the provider + model + sampling scalars of one conversation's requests (`provider`, `model`, `temperature`, `maxTokens`, `stop` — each mapping 1:1 onto the same-named `GenerateOptions` field). It is per-conversation state recorded in the session log as part of the request header (see the dsh-session `request/header` events), never a silently-adjustable per-call knob: the `agent/request` waterfall proposes a replacement and the loop logs a real change. `callConfigEquals(a, b)` is the field-wise real-change detector; `deepFreeze(value)` is the ownership helper the loop applies to every built request before dispatch (`llm/stream` listeners and adapters read, never rewrite). `markAgentLoopRequest()` gives that exact object process-local loop provenance, and `isAgentLoopRequest()` lets observers distinguish it from independently logged auxiliary calls that may also be frozen and session-associated.
### App attribution (`attribution.ts`)

View File

@@ -6,6 +6,11 @@
* @module dsh-llm/call-config
*/
import type { GenerateOptions } from './types.ts'
/** Process-local identities of request objects assembled by dsh-agent-loop. */
const AGENT_LOOP_REQUESTS = new WeakSet<GenerateOptions>()
/**
* Provider + model + sampling scalars of one conversation's requests. Every field maps
* 1:1 onto the same-named `GenerateOptions` field; the loop builds requests
@@ -33,6 +38,25 @@ export function callConfigEquals(a: LlmCallConfig, b: LlmCallConfig): boolean {
return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i])
}
/**
* Mark one exact request object as assembled by dsh-agent-loop.
* @param request - loop-owned request envelope before LLM dispatch.
* @returns the same request object with process-local loop provenance.
*/
export function markAgentLoopRequest<T extends GenerateOptions>(request: T): T {
AGENT_LOOP_REQUESTS.add(request)
return request
}
/**
* Test whether the exact request object was assembled by dsh-agent-loop.
* @param request - request envelope observed at the LLM waterfall.
* @returns whether {@link markAgentLoopRequest} recorded this object.
*/
export function isAgentLoopRequest(request: GenerateOptions): boolean {
return AGENT_LOOP_REQUESTS.has(request)
}
/**
* Deep-freeze a value in place, guarding cycles, so later mutation throws.
* {@link AbortSignal} objects are deliberately skipped because they are the

View File

@@ -28,7 +28,7 @@ export * from './never.ts'
export * from './error.ts'
export * from './types.ts'
export { BlockAssembler } from './assembler.ts'
export { callConfigEquals, deepFreeze } from './call-config.ts'
export { callConfigEquals, deepFreeze, isAgentLoopRequest, markAgentLoopRequest } from './call-config.ts'
export type { LlmCallConfig } from './call-config.ts'
export { isLlmAdapterFailure, llmFailureOf } from './adapter-failure.ts'
@@ -42,11 +42,11 @@ declare module 'cordis' {
* Waterfall around every streaming model call (retry, replay, routing).
* Bound to the {@link LlmService}; call `next()` to reach the resolved
* adapter's stream, or yield your own chunks to short-circuit.
* @param options - the full request. A LOOP-built request arrives
* deep-frozen (mutation throws): its content is a pure function of the
* session log (the reconstructability Agent Note), so listeners read it, never
* rewrite it. A hand-built one-shot (compaction summarize) is the
* caller's own object and stays mutable here.
* @param options - the full request. A LOOP-built request carries the
* process-local {@link markAgentLoopRequest} identity and arrives deep-frozen
* (mutation throws): its content is a pure function of the session log (the
* reconstructability Agent Note), so listeners read it, never rewrite it.
* Hand-built calls own their mutability policy and do not carry that marker.
* @mode waterfall
*/
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>

View File

@@ -5,7 +5,8 @@
*/
import { describe, expect, it } from 'vitest'
import { callConfigEquals, deepFreeze } from '../src/call-config.ts'
import { callConfigEquals, deepFreeze, isAgentLoopRequest, markAgentLoopRequest } from '../src/call-config.ts'
import type { GenerateOptions } from '../src/types.ts'
describe('callConfigEquals', () => {
it('compares every field, including the stop list element-wise', () => {
@@ -56,3 +57,19 @@ describe('deepFreeze', () => {
expect(Object.isFrozen(cyclic)).toBe(true)
})
})
describe('agent-loop request identity', () => {
it('marks only the exact request object and preserves its identity', () => {
const request: GenerateOptions = {
provider: 'mock',
model: 'model',
messages: [],
}
const copy = { ...request }
expect(isAgentLoopRequest(request)).toBe(false)
expect(markAgentLoopRequest(request)).toBe(request)
expect(isAgentLoopRequest(request)).toBe(true)
expect(isAgentLoopRequest(copy)).toBe(false)
})
})