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:
@@ -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`)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user