Merge origin/master: scope-aware fusion of the tools/execute seam, session-prefix, and tool-cordis

Master brought 50 commits (the tool-cordis group, dsh-code-runtime + worker,
the tools/execute around-dispatch seam + timeout-policy, repeat-tool-guard,
agent/session-prefix, the ui reorganization). Beyond the ten textual
conflicts, the merge reconciles master's new seams with this branch's
scoped-registration world:

- tools/execute (new waterfall around core dispatch): dispatched with the
  SAME exec.agent carrier as the pre/post waterfalls — an agent.ctx wrapper
  times/retries only its own agent's calls — and its base thunk resolves the
  tool through the caller's visible view (get(exec.name, exec.agent)), so a
  scoped/shadowed tool dispatches and a restricted-away global stays
  UNKNOWN_TOOL. Declared this: Scoped<ToolRegistry> with the scope-filtered
  doc sentence; invariants table + verify-scoped-dispatch pin it (21 events).
- agent/session-prefix (new waterfall, once per loop instance): composed via
  the fused agentEvents dispatcher (scope-filtered like every agent-subject
  event), declared this: Scoped<Agent>, table-pinned. agent/pre-step keeps
  master's new sessionPrefix parameter with this branch's Scoped this.
- timeout-policy reads the budget through the caller's visible view
  (get(exec.name, exec.agent)): a scoped tool's own timeoutMs governs its
  calls; a global name-twin's budget is never misapplied to a shadowing
  per-agent variant.
- tool-cordis: cordis_inspect's tools section lists the CALLING agent's view
  (its description promises "what you can call"); the sandbox tool façade's
  reads resolve through the mount's own scope, mirroring where its register
  lands writes; sandboxRegisterTool's return type carries the exact-disposer
  union honestly. dsh-scope declared as peer+dev with the project reference.
- doc-sync chain unions master's verify-cordis-api with this branch's
  verify-scoped-dispatch; the generated catalogs, event matrix (the
  zero-dispatcher guard passes over master's new events), module graph, and
  the cordis api-catalog are regenerated on the merged surface.

Full gate sequence green on the merged tree: typecheck, lint, per-file 100%
coverage (2668 tests), snapshots (38), doc-sync, module graph, build,
hygiene, demo smoke.
This commit is contained in:
Tianyi Cui
2026-07-09 23:24:42 +08:00
193 changed files with 12546 additions and 605 deletions

View File

@@ -0,0 +1,9 @@
# timeout/ — tool-call timeout policy
The tool-call timeout policy plugin. A single **product** package: it is a deployment-policy consumer of the `tools/execute` around-dispatch seam (owned by [`dsh-tools`](../core/tools)) and the pure [`dsh-timeout`](../util/timeout) library — not a swappable capability with an interface/implementation split, so it needs no seam trio.
| Package | Role | ctx key |
|---|---|---|
| `timeout-policy/` | A `tools/execute` wrapper: for each configured tool it arms a per-call deadline on `exec.signal` and returns a structured `TOOL_TIMEOUT` result when that deadline wins | (registers a `tools/execute` listener; injects nothing) |
Timeout is split across three layers: [`dsh-timeout`](../util/timeout) owns the pure timing/classification primitive (`deadline`/`timeoutOf`), each capability owns termination (bash kills its process group, the fetch provider tears down its socket), and this package owns the *model-facing tool-call budget as deployment policy* — no model-facing timeout argument, no global default. It is the middleware the [timeout-library RFC](../../docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md) foresaw. `bash` and hook command execution keep their own `BASH_TIMEOUT` backend timeout and do not route through this policy.

View File

@@ -0,0 +1,34 @@
# dsh-timeout-policy
Tool-call timeout enforcer: a single `tools/execute` around-dispatch listener that arms a per-call cooperative deadline on `exec.signal` for a tool declaring `timeoutMs` on its `ToolDefinition` and returns a structured `TOOL_TIMEOUT` result when that deadline wins. The budget is read from the tool's own declaration (`ToolDefinition.timeoutMs`, set by the owning tool plugin), so this plugin is **zero-config**. It is the reference `tools/execute` wrapper and the enforcement home for model-facing tool-call budgets (the timeout-library RFC's foreseen middleware).
## Plugin (namespace: `timeout-policy`)
A function/namespace plugin (`name` / `inject` / `apply`), not a service. It registers no tool and takes no config — it consumes `ctx.tools`'s `tools/execute` waterfall (which the `dsh-tools` registry always provides) and reads each dispatched tool's declared `timeoutMs` from the registry (`ctx.tools.get(exec.name)`).
```yaml
- id: timeout-policy
name: '@deepseek-ai/dsh-timeout-policy'
```
The per-tool budget is declared by the tool plugin (e.g. `dsh-tool-web`'s `fetchTimeoutMs`/`searchTimeoutMs` config, attached as `ToolDefinition.timeoutMs`); this plugin only enforces it, so a mistyped tool name is not possible.
### Behavior
For a tool that **declares a `timeoutMs`** the listener:
1. Reads the budget from the tool's own declaration in the registry (`ctx.tools.get(exec.name)?.timeoutMs`) and arms `deadline(exec.signal, timeoutMs, 'TOOL_TIMEOUT')` — one signal fusing the caller's abort with this plugin's timer (`@deepseek-ai/dsh-timeout`).
2. Swaps that derived signal onto `exec` for the downstream dispatch, then restores the caller's own signal afterward (cordis `next()` ignores passed arguments, so the wrapper mutates the shared `exec` in place; restoring keeps `tools/post-execute` seeing the caller's signal).
3. After dispatch, if `timeoutOf(d.signal, 'TOOL_TIMEOUT')` matches — this plugin's own timer fired — replaces the result with a structured `TOOL_TIMEOUT` tool result: `{ isError: true, error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' }, content: 'Error: tool call timed out after <ms>ms' }`.
A tool that **declares no budget** delegates untouched (no deadline).
The base `next()` of `tools/execute` is the registry's dispatch-with-normalization thunk, so when the timeout signal reaches a provider that throws its own upstream-abort error, dispatch first turns it into a normal error result, and this wrapper then replaces that with `TOOL_TIMEOUT`. That ordering is why the replacement is keyed off the signal (`timeoutOf`), not off the dispatched result's shape.
### Cooperative, not a hard kill
The derived signal only **notifies**; termination stays with the tool and the capability it forwards `exec.signal` to (the `dsh-timeout` library owns no kill). **Declaring `timeoutMs` therefore means "cooperative with `exec.signal`"**: a tool that ignores the signal will not stop on timeout. Only signal-forwarding tools should declare it — the shipped `web_fetch`/`web_search` (which forward through `ctx.web` to providers) are the reference. `TOOL_TIMEOUT` needs no session event for reconstructability: it is the final model-facing `tool/result`, already logged by the loop.
### Composing with other `tools/execute` wrappers
Multiple `tools/execute` listeners compose by cordis registration order. Combined with a future retry/sandbox/metrics wrapper, registration order chooses the semantics — "timeout covers the whole retry operation" (timeout registered outer) versus "timeout covers each attempt" (timeout registered inner).

View File

@@ -0,0 +1,36 @@
{
"name": "@deepseek-ai/dsh-timeout-policy",
"description": "Tool-call timeout policy: a tools/execute wrapper that arms a per-tool deadline on exec.signal and returns TOOL_TIMEOUT when it wins",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-timeout": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.6"
},
"devDependencies": {
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.6"
}
}

View File

@@ -0,0 +1,119 @@
/**
* `@deepseek-ai/dsh-timeout-policy`: the tool-call timeout ENFORCER. It registers
* ONE `tools/execute` around-dispatch listener that, for a tool declaring a
* `timeoutMs` on its {@link ToolDefinition}, arms a per-call deadline on
* `exec.signal` and returns a structured `TOOL_TIMEOUT` result when that deadline
* wins. The budget is DECLARED by the tool (see `ToolDefinition.timeoutMs`, set
* by the owning tool plugin from its own config); this plugin only enforces it,
* so it is zero-config and there is no tool-name map to mistype.
*
* This is a COOPERATIVE deadline, not a hard kill: the derived signal only
* NOTIFIES. A tool that declares `timeoutMs` (and the capability it forwards
* `exec.signal` to) must honor that signal and reach quiescence — the plugin
* never races the tool promise or terminates work itself (see the timeout-library
* RFC's rejection of `Promise.race`). Declaring `timeoutMs` therefore MEANS "this
* tool is cooperative with `exec.signal`": a tool that ignores the signal will
* not stop on timeout, so only signal-forwarding tools should declare it (the
* shipped web tools are the reference).
*
* Ownership of the `TOOL_TIMEOUT` code is entirely here: it is both the internal
* {@link deadline} code (so {@link timeoutOf} scopes the classification to THIS
* plugin's own timer, reading a foreign/nested outer deadline as an ordinary
* cancel) and the structured `{ name, code }` on the replacement tool result.
* No new session event is needed for reconstructability: the `TOOL_TIMEOUT`
* result IS the final model-facing `tool/result`, already logged by the loop.
*
* Why a `tools/execute` around seam and not a `pre`/`post` pair: the deadline
* needs ONE lexical scope — arm on `exec.signal`, delegate to dispatch, classify
* the result, dispose the timer — which the around seam gives directly. A
* pre/post split would spread one deadline's lifetime across two independent
* waterfalls (a call-id map, cleanup on every deny/throw/dispose path).
*
* @module @deepseek-ai/dsh-timeout-policy
*/
import type { Context } from 'cordis'
import type { CallId } from '@deepseek-ai/dsh-llm'
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
/**
* The code owned by this plugin, used BOTH as the internal {@link deadline}
* classification code AND as the structured error `code` on the replacement
* tool result. Scoping {@link timeoutOf} to it keeps a nested outer deadline
* (another `tools/execute` wrapper's timer that fired first) from being misread
* as this plugin's own timeout — it reads as an ordinary upstream cancel.
*/
export const TOOL_TIMEOUT = 'TOOL_TIMEOUT'
/** Cordis plugin name used by loader diagnostics. */
export const name = 'timeout-policy'
/** The tool registry seam this plugin wraps (`tools/execute`) and reads (`get`). */
export const inject = ['tools']
/**
* The structured result substituted when this plugin's deadline wins. `content`
* is the model-facing message; `error.code` is the same {@link TOOL_TIMEOUT}
* this plugin owns, so a retry/sandbox plugin (and replay) can route on it.
*
* @param callId - the timed-out call's id, carried onto the replacement result.
* @param timeoutMs - the elapsed budget, rendered into the model-facing message.
* @returns the `isError` {@link ToolExecutionResult} with a `TOOL_TIMEOUT` error.
*/
export function toolTimeoutResult(callId: CallId, timeoutMs: number): ToolExecutionResult {
return {
callId,
content: [{ type: 'text', text: `Error: tool call timed out after ${timeoutMs}ms` }],
isError: true,
error: { name: 'ToolTimeoutError', code: TOOL_TIMEOUT },
}
}
/**
* Register the tool-call timeout enforcer. For a tool whose {@link ToolDefinition}
* declares `timeoutMs`, the listener arms a {@link deadline} on the caller's
* `exec.signal`, swaps it onto `exec` for the downstream dispatch (cordis
* `next()` ignores passed arguments, so a wrapper mutates the shared `exec` in
* place), restores the original signal afterward so `tools/post-execute` sees the
* caller's own signal, and replaces the result with {@link toolTimeoutResult}
* when its own timer fired. A tool that declares no budget delegates untouched.
*
* The budget source is the tool's own declaration read from the registry
* (`ctx.tools.get(exec.name, exec.agent)?.timeoutMs`), NOT a plugin config map —
* `exec.name` is the tool being dispatched, so the lookup always resolves and
* there is no mistypable tool name and no unknown-name path to warn or throw
* about. Resolution goes through the CALLER's visible view (the `exec.agent`
* scope), exactly like dispatch itself: a scoped tool's own `timeoutMs` governs
* its calls, and a global name-twin's budget is never misapplied to a shadowing
* per-agent variant.
*/
export function apply(ctx: Context): void {
ctx.on('tools/execute', async (exec, next): Promise<ToolExecutionResult> => {
const timeoutMs = ctx.tools.get(exec.name, exec.agent)?.timeoutMs
// A tool that declares no budget: no deadline, delegate unchanged.
if (timeoutMs === undefined) return next()
using d = deadline(exec.signal, timeoutMs, TOOL_TIMEOUT)
// Swap the derived deadline onto exec for dispatch, then restore the
// caller's own signal so post-execute listeners never see this plugin's
// (possibly already-aborted) timeout signal. `undefined` is not assignable to
// the optional `signal` under exactOptionalPropertyTypes, so branch on it.
const upstream = exec.signal
exec.signal = d.signal
try {
const result = await next()
// If OUR timer fired (scoped by code — a nested outer deadline reads as
// undefined here), the tool/capability saw the abort and reached
// quiescence; replace whatever it returned (its own abort result) with the
// structured TOOL_TIMEOUT the model sees.
if (timeoutOf(d.signal, TOOL_TIMEOUT) !== undefined) {
return toolTimeoutResult(exec.callId, timeoutMs)
}
return result
} finally {
if (upstream === undefined) delete exec.signal
else exec.signal = upstream
}
})
}

View File

@@ -0,0 +1,200 @@
/**
* Unit + real-load-path coverage for @deepseek-ai/dsh-timeout-policy. The
* timeout-wins cases drive the deadline under fake timers (deterministic — no
* wall-clock race) and use a COOPERATIVE tool that settles only when its
* `exec.signal` aborts, mirroring how a real capability forwards the signal and
* reaches quiescence.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { CallId, HarnessError } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool, type ToolExecution, type ToolExecutionResult, type PostToolDecision } from '@deepseek-ai/dsh-tools'
import * as timeoutPolicy from '@deepseek-ai/dsh-timeout-policy'
import { TOOL_TIMEOUT, toolTimeoutResult } from '@deepseek-ai/dsh-timeout-policy'
/** Mount the registry + the zero-config timeout-policy enforcer. */
async function setup() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(timeoutPolicy)
return ctx
}
/** A cooperative tool that settles ONLY when its exec.signal aborts (returns text). */
const cooperativeTool = defineTool({
name: 'slow', description: 'stops when aborted', parameters: {}, timeoutMs: 100,
execute(_args, exec): Promise<{ type: 'text'; text: string }[]> {
const done = [{ type: 'text' as const, text: 'stopped cooperatively' }]
if (exec.signal?.aborted) return Promise.resolve(done)
return new Promise((resolve) => { exec.signal?.addEventListener('abort', () => { resolve(done) }) })
},
})
/** A cooperative tool that THROWS its own upstream-abort error when aborted (web-provider shape). */
const abortThrowingTool = defineTool({
name: 'aborter', description: 'throws WEB_ABORTED when aborted', parameters: {}, timeoutMs: 100,
execute(_args, exec): Promise<never> {
if (exec.signal?.aborted) return Promise.reject(new HarnessError('web fetch aborted', 'WEB_ABORTED'))
return new Promise((_resolve, reject) => { exec.signal?.addEventListener('abort', () => { reject(new HarnessError('web fetch aborted', 'WEB_ABORTED')) }) })
},
})
describe('timeout-policy delegation (unconfigured / fast)', () => {
it('delegates a tool with NO declared budget unchanged and does not touch exec.signal', async () => {
const ctx = await setup()
let seenSignal: AbortSignal | undefined
ctx.tools.register(defineTool({ name: 'probe', description: 'd', parameters: {},
async execute(_a, exec) { seenSignal = exec.signal; return [{ type: 'text' as const, text: 'ok' }] } }))
const upstream = new AbortController().signal
const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'probe', arguments: {}, signal: upstream })
expect(result.isError).toBe(false)
expect(seenSignal).toBe(upstream)
})
it('a tool with a budget that returns fast keeps its own result (no timeout)', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({ name: 'fast', description: 'd', parameters: {}, timeoutMs: 10_000,
async execute() { return [{ type: 'text' as const, text: 'ok' }] } }))
const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'fast', arguments: {} })
expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false })
})
it('a budgeted tool receives the DERIVED deadline signal (not the caller signal) during dispatch', async () => {
const ctx = await setup()
let seenSignal: AbortSignal | undefined
ctx.tools.register(defineTool({ name: 'probe', description: 'd', parameters: {}, timeoutMs: 10_000,
async execute(_a, exec) { seenSignal = exec.signal; return [{ type: 'text' as const, text: 'ok' }] } }))
const upstream = new AbortController().signal
await ctx.tools.execute({ callId: CallId('c1'), name: 'probe', arguments: {}, signal: upstream })
expect(seenSignal).toBeDefined()
expect(seenSignal).not.toBe(upstream)
})
})
describe('timeout-policy signal restoration', () => {
it('restores the caller signal for post-execute after wrapping', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({ name: 'fast', description: 'd', parameters: {}, timeoutMs: 10_000,
async execute() { return [{ type: 'text' as const, text: 'ok' }] } }))
let postSignal: AbortSignal | undefined | 'unset' = 'unset'
ctx.on('tools/post-execute', async (exec, _result, next): Promise<PostToolDecision> => { postSignal = exec.signal; return next() })
const upstream = new AbortController().signal
await ctx.tools.execute({ callId: CallId('c1'), name: 'fast', arguments: {}, signal: upstream })
expect(postSignal).toBe(upstream)
})
it('deletes exec.signal again when the caller passed none', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({ name: 'fast', description: 'd', parameters: {}, timeoutMs: 10_000,
async execute() { return [{ type: 'text' as const, text: 'ok' }] } }))
let hadSignal: boolean | undefined
ctx.on('tools/post-execute', async (exec, _result, next): Promise<PostToolDecision> => { hadSignal = 'signal' in exec && exec.signal !== undefined; return next() })
await ctx.tools.execute({ callId: CallId('c1'), name: 'fast', arguments: {} })
expect(hadSignal).toBe(false)
})
})
describe('timeout-policy TOOL_TIMEOUT replacement (deadline wins)', () => {
beforeEach(() => { vi.useFakeTimers() })
afterEach(() => { vi.useRealTimers() })
it('replaces a cooperative tool result with TOOL_TIMEOUT when its own deadline fires', async () => {
const ctx = await setup()
ctx.tools.register(cooperativeTool)
const pending = ctx.tools.execute({ callId: CallId('c1'), name: 'slow', arguments: {} })
await vi.advanceTimersByTimeAsync(150)
const result = await pending
expect(result).toEqual({
callId: CallId('c1'),
content: [{ type: 'text', text: 'Error: tool call timed out after 100ms' }],
isError: true,
error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' },
})
})
it('replaces a provider-owned abort ERROR result with TOOL_TIMEOUT when the signal was ours', async () => {
const ctx = await setup()
ctx.tools.register(abortThrowingTool)
const pending = ctx.tools.execute({ callId: CallId('c1'), name: 'aborter', arguments: {} })
await vi.advanceTimersByTimeAsync(150)
const result = await pending
expect(result.isError).toBe(true)
expect(result.error).toEqual({ name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' })
expect(result.content[0]).toMatchObject({ text: 'Error: tool call timed out after 100ms' })
})
it('does NOT replace when the caller aborts first (upstream cancel, not our timeout)', async () => {
const ctx = await setup()
ctx.tools.register(cooperativeTool)
const upstream = new AbortController()
const pending = ctx.tools.execute({ callId: CallId('c1'), name: 'slow', arguments: {}, signal: upstream.signal })
upstream.abort('user cancelled')
await vi.advanceTimersByTimeAsync(0)
const result = await pending
expect(result.isError).toBe(false)
expect(result.content[0]).toMatchObject({ text: 'stopped cooperatively' })
})
})
describe('toolTimeoutResult', () => {
it('builds the structured TOOL_TIMEOUT result', () => {
expect(toolTimeoutResult(CallId('c9'), 250)).toEqual({
callId: CallId('c9'),
content: [{ type: 'text', text: 'Error: tool call timed out after 250ms' }],
isError: true,
error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' },
} satisfies ToolExecutionResult)
})
it('exposes the owned code constant', () => {
expect(TOOL_TIMEOUT).toBe('TOOL_TIMEOUT')
})
})
describe('timeout-policy disposal (HMR safety)', () => {
it('removes its tools/execute listener when the plugin fiber disposes', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
let seenSignal: AbortSignal | undefined
ctx.tools.register(defineTool({ name: 'probe', description: 'd', parameters: {}, timeoutMs: 10_000,
async execute(_a, exec) { seenSignal = exec.signal; return [{ type: 'text' as const, text: 'ok' }] } }))
const fiber = await ctx.plugin(timeoutPolicy)
const upstream = new AbortController().signal
await ctx.tools.execute({ callId: CallId('c1'), name: 'probe', arguments: {}, signal: upstream })
expect(seenSignal).not.toBe(upstream)
await fiber.dispose()
await ctx.tools.execute({ callId: CallId('c2'), name: 'probe', arguments: {}, signal: upstream })
expect(seenSignal).toBe(upstream)
})
})
describe('dsh-timeout-policy real-load-path guard', () => {
it('has no default export and keeps name/inject through unwrapExports', () => {
expect('default' in timeoutPolicy).toBe(false)
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(timeoutPolicy) as Record<string, unknown>
expect(unwrapped).toBe(timeoutPolicy)
expect(unwrapped.name).toBe('timeout-policy')
expect(unwrapped.inject).toEqual(['tools'])
expect(typeof unwrapped.apply).toBe('function')
})
it('boots over ctx.tools through the unwrapped module and wraps a budgeted tool', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
ctx.tools.register(defineTool({ name: 'fast', description: 'd', parameters: {}, timeoutMs: 5_000,
async execute() { return [{ type: 'text' as const, text: 'ok' }] } }))
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(timeoutPolicy) as Parameters<Context['plugin']>[0]
const fiber = await ctx.plugin(unwrapped)
const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'fast', arguments: {} } satisfies ToolExecution)
expect(result.isError).toBe(false)
await fiber.dispose()
})
})

View File

@@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"],
"references": [
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../../vendor/schemastery" },
{ "path": "../../llm/llm" },
{ "path": "../../util/timeout" },
{ "path": "../../core/tools" }
]
}