Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # apps/cli/src/web.ts # apps/web/tests/smoke-fixture.e2e.ts # docs/architecture.i18n.yaml # packages/client/connection/src/client/fixture.ts # packages/client/ui-conversation/README.md # packages/client/ui-conversation/package.json # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-conversation/src/client/chat/ChatView.tsx # packages/client/ui-conversation/src/client/chat/register.ts # packages/client/ui-conversation/src/client/contract/slots.ts # packages/client/ui-conversation/src/client/contract/views.ts # packages/client/ui-conversation/src/client/service.ts # packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx # packages/client/ui-conversation/src/client/stores.ts # packages/client/ui-conversation/tests/skeleton-branches.spec.tsx # packages/cordis/tool-cordis/src/api-catalog.ts # packages/host/runtime/src/boot.ts # packages/host/runtime/tests/host-runtime.spec.ts # pnpm-lock.yaml
This commit is contained in:
@@ -6,7 +6,7 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, RequestErrorDecision } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -166,7 +166,7 @@ describe('bounded transient retry policy', () => {
|
||||
])
|
||||
;({ ctx: context } = await harness(adapter))
|
||||
let toolExecutions = 0
|
||||
context.tools.register(defineTool({
|
||||
context.tools.register(defineContentToolFixture({
|
||||
name: 'danger',
|
||||
description: 'must not run for a failed provider attempt',
|
||||
parameters: {},
|
||||
|
||||
@@ -58,7 +58,8 @@ export function isAgentLoopRequest(request: GenerateOptions): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep-freeze a value in place, guarding cycles, so later mutation throws.
|
||||
* Deep-freeze a value in place with an iterative traversal, guarding cycles,
|
||||
* so later mutation throws without imposing a JavaScript call-stack depth cap.
|
||||
* {@link AbortSignal} objects are deliberately skipped because they are the
|
||||
* request's live cancellation channel and freezing them breaks abort.
|
||||
* @param value - the value to freeze in place.
|
||||
@@ -66,16 +67,31 @@ export function isAgentLoopRequest(request: GenerateOptions): boolean {
|
||||
*/
|
||||
export function deepFreeze<T>(value: T): T {
|
||||
const seen = new WeakSet<object>()
|
||||
const walk = (node: unknown): void => {
|
||||
if (node === null || typeof node !== 'object') return
|
||||
if (node instanceof AbortSignal) return
|
||||
if (seen.has(node)) return
|
||||
const pending: (
|
||||
| { kind: 'visit'; node: unknown }
|
||||
| { kind: 'property'; source: Record<string, unknown>; key: string }
|
||||
)[] = [{ kind: 'visit', node: value }]
|
||||
while (pending.length > 0) {
|
||||
const task = pending.pop()
|
||||
/* v8 ignore next -- the loop condition guarantees one pending task. */
|
||||
if (task === undefined) continue
|
||||
if (task.kind === 'property') {
|
||||
pending.push({ kind: 'visit', node: task.source[task.key] })
|
||||
continue
|
||||
}
|
||||
const node = task.node
|
||||
if (node === null || typeof node !== 'object') continue
|
||||
if (node instanceof AbortSignal) continue
|
||||
if (seen.has(node)) continue
|
||||
seen.add(node)
|
||||
Object.freeze(node)
|
||||
for (const key of Object.keys(node)) {
|
||||
walk((node as Record<string, unknown>)[key])
|
||||
const keys = Object.keys(node)
|
||||
for (let index = keys.length - 1; index >= 0; index--) {
|
||||
const key = keys[index]
|
||||
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
||||
if (key === undefined) continue
|
||||
pending.push({ kind: 'property', source: node as Record<string, unknown>, key })
|
||||
}
|
||||
}
|
||||
walk(value)
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -56,6 +56,26 @@ describe('deepFreeze', () => {
|
||||
deepFreeze(cyclic)
|
||||
expect(Object.isFrozen(cyclic)).toBe(true)
|
||||
})
|
||||
|
||||
it('freezes nesting deeper than the JavaScript call stack', () => {
|
||||
const depth = 5_000
|
||||
const root: unknown[] = []
|
||||
let cursor = root
|
||||
for (let index = 0; index < depth; index++) {
|
||||
const child: unknown[] = []
|
||||
cursor.push(child)
|
||||
cursor = child
|
||||
}
|
||||
|
||||
deepFreeze(root)
|
||||
|
||||
cursor = root
|
||||
for (let index = 0; index < depth; index++) {
|
||||
expect(Object.isFrozen(cursor)).toBe(true)
|
||||
cursor = cursor[0] as unknown[]
|
||||
}
|
||||
expect(Object.isFrozen(cursor)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('agent-loop request identity', () => {
|
||||
|
||||
Reference in New Issue
Block a user