docs: call waterfall interception short-circuiting
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
|
||||
2026-06-11-microkernel-event-taxonomy.md: 202595fed125966a5d77920536e7f4ee88f875fe
|
||||
2026-06-11-microkernel-event-taxonomy.zh.md: 1365910157c36699bfe1fdb3a10aa0785948e0fa
|
||||
2026-06-11-microkernel-event-taxonomy.md: fe6c242acbc0b64123711a17a528f35c02d91087
|
||||
2026-06-11-microkernel-event-taxonomy.zh.md: aa3db53d640145836817f9835a30c69815f70d23
|
||||
@@ -12,7 +12,7 @@ The product principle is "everything is a plugin": hooks, /goal, /loop, dynamic
|
||||
|
||||
Pure Cordis event taxonomy. The loop's extension seams are typed events with deliberate dispatch modes:
|
||||
|
||||
- **waterfall** (around-middleware) where plugins transform, veto, recover, or wrap: `agent/pre-step`, `agent/request`, `agent/request-error`, `tools/pre-execute`, `tools/execute`, `tools/post-execute`, `llm/stream`, `system-prompt/assemble`.
|
||||
- **waterfall** (around-middleware) where plugins transform, short-circuit, recover, or wrap: `agent/pre-step`, `agent/request`, `agent/request-error`, `tools/pre-execute`, `tools/execute`, `tools/post-execute`, `llm/stream`, `system-prompt/assemble`.
|
||||
- **serial** (awaited in listener order) for ordered checkpoints such as `agent/turn-stopping`.
|
||||
- **parallel** (awaited fan-out) where every listener must get an independent chance: the `session/flush` durability checkpoint.
|
||||
- **emit** (synchronous fire-and-forget) for notifications: inbox transitions, lifecycle, errors, and the contained immutable `tools/result` observation. Durable session events own turn and step boundaries.
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
纯 Cordis 事件分类体系。agent loop(智能体循环)的扩展 seam 是带类型的事件,具有明确的分发模式:
|
||||
|
||||
- **waterfall(瀑布式事件)**(around-middleware):插件可变换、否决、恢复或包装:`agent/pre-step`、`agent/request`、`agent/request-error`、`tools/pre-execute`、`tools/execute`、`tools/post-execute`、`llm/stream`、`system-prompt/assemble`。
|
||||
- **waterfall(瀑布式事件)**(around-middleware):插件可变换、短路、恢复或包装:`agent/pre-step`、`agent/request`、`agent/request-error`、`tools/pre-execute`、`tools/execute`、`tools/post-execute`、`llm/stream`、`system-prompt/assemble`。
|
||||
- **serial**(按监听器顺序依次 await):用于 `agent/turn-stopping` 等有序检查点。
|
||||
- **parallel**(await 扇出):每个监听器都必须获得独立执行的机会:`session/flush` 持久性检查点。
|
||||
- **emit**(同步 fire-and-forget):用于通知:inbox 转换、生命周期、错误,以及受错误隔离的 `tools/result` 观测;该观测接收不可变的最终结果。轮次与步骤边界由持久会话事件拥有。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
|
||||
2026-07-12-agent-scope-runtime-design.md: b93c291957fab98ab9d0d04eb816bb01a15a0b51
|
||||
2026-07-12-agent-scope-runtime-design.zh.md: e0c01f31eb2664608139f95dc60025a5b33e4618
|
||||
2026-07-12-agent-scope-runtime-design.md: b8f265551c4ebbb94d32b3599959fe6cc4da974d
|
||||
2026-07-12-agent-scope-runtime-design.zh.md: 22cd35a601bc011c27a748098be11990f5ee4653
|
||||
@@ -34,7 +34,7 @@ The [July 8 Agent Note](2026-07-08-agent-scope-contexts.md) remains the contribu
|
||||
|
||||
## Cordis model: context, fiber, effect, receiver, and waterfall
|
||||
|
||||
Five Cordis ideas are required to understand the implementation. A context selects services and registration ownership; a fiber is one live plugin or child lifecycle; an effect attaches cleanup to a fiber; an event receiver selects listeners; and a waterfall lets listeners transform or veto an operation in sequence.
|
||||
Five Cordis ideas are required to understand the implementation. A context selects services and registration ownership; a fiber is one live plugin or child lifecycle; an effect attaches cleanup to a fiber; an event receiver selects listeners; and a waterfall lets listeners transform or short-circuit an operation in sequence.
|
||||
|
||||
### A context is an ownership path through one service graph
|
||||
|
||||
@@ -56,7 +56,7 @@ Cordis filters listeners using the dispatch receiver (`this`), while harness lis
|
||||
|
||||
Product helpers therefore construct the carrier and pass the domain subject separately. This prevents listener routing from becoming an alternate object model and keeps event signatures understandable without knowledge of carrier internals.
|
||||
|
||||
A Cordis waterfall is middleware-style dispatch. Each listener receives `next()`: calling it delegates to the remaining listeners and base operation, while returning without it vetoes or replaces the downstream result. Waterfalls power prompt assembly and tool policy; ordinary emit events notify synchronously, and parallel events await all listeners without a veto result.
|
||||
A Cordis waterfall is middleware-style dispatch. Each listener receives `next()`: calling it delegates to the remaining listeners and base operation, while returning without it short-circuits or replaces the downstream result. Waterfalls power prompt assembly and tool policy; ordinary emit events notify synchronously, and parallel events await all listeners without a veto result.
|
||||
|
||||
## Scope routing: one opaque key selects one layer
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Status: implemented
|
||||
|
||||
## Cordis 模型:上下文、fiber、effect、receiver 与 waterfall
|
||||
|
||||
理解实现需要五个 Cordis 概念。上下文选择服务和注册所有权;fiber 是一个活跃的插件或子生命周期;effect 将清理逻辑附加到 fiber;事件接收器选择监听器;waterfall(瀑布式事件)让监听器按顺序变换或否决一个操作。
|
||||
理解实现需要五个 Cordis 概念。上下文选择服务和注册所有权;fiber 是一个活跃的插件或子生命周期;effect 将清理逻辑附加到 fiber;事件接收器选择监听器;waterfall(瀑布式事件)让监听器按顺序变换或短路一个操作。
|
||||
|
||||
### 上下文是贯穿单个服务图的所有权路径
|
||||
|
||||
@@ -56,7 +56,7 @@ Cordis 使用 dispatch receiver(`this`)过滤监听器,而 harness 的监
|
||||
|
||||
因此,产品辅助函数构造载体并单独传递领域主体。这防止监听器路由变成另一套对象模型,并使事件签名在不了解载体内部的情况下也可理解。
|
||||
|
||||
Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 `next()`:调用它则委托给剩余监听器和基础操作,不调用则否决或替换下游结果。Waterfall 驱动提示词组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。
|
||||
Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 `next()`:调用它则委托给剩余监听器和基础操作,不调用则短路或替换下游结果。Waterfall 驱动提示词组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。
|
||||
|
||||
## 作用域路由:一个不透明键选择一层
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
|
||||
2026-07-08-self-referential-cordis-toolset.md: 29d331008d4111ae00147f2ddd19a628135339ac
|
||||
2026-07-08-self-referential-cordis-toolset.zh.md: d61ba794781226437a5d7499c20e5cd33119fe78
|
||||
2026-07-08-self-referential-cordis-toolset.md: a2f614cc5a236e45622eae2b30f518181331cc79
|
||||
2026-07-08-self-referential-cordis-toolset.zh.md: 6c56c2bf0c8cc8d175f451290620d9386bb69a23
|
||||
@@ -81,4 +81,4 @@ The correctness investment therefore goes where it pays for every capability at
|
||||
|
||||
## Consequences
|
||||
|
||||
The toolset is a deliberate opt-in with a fully-privileged `ctx`, so a deployment adopts it as consciously as a bash tool. Several facts follow that the tool descriptions warn the model about directly: a waterfall listener (e.g. `tools/pre-execute`) that returns without calling `next()` vetoes the chain, so a mounted listener can lobotomize the agent's own tool dispatch ([waterfall semantics](../../../../docs/cordis-primer.md#cordis-waterfall-semantics)); mount code runs inside a tool call of the current turn, so awaiting anything that resolves only after the turn deadlocks; `vmTimeoutMs` bounds synchronous evaluation only; and mounts do not survive session resume.
|
||||
The toolset is a deliberate opt-in with a fully-privileged `ctx`, so a deployment adopts it as consciously as a bash tool. Several facts follow that the tool descriptions warn the model about directly: a waterfall listener (e.g. `tools/pre-execute`) that returns without calling `next()` short-circuits the chain, so a mounted listener can stop the agent's own tool dispatch ([waterfall semantics](../../../../docs/cordis-primer.md#cordis-waterfall-semantics)); mount code runs inside a tool call of the current turn, so awaiting anything that resolves only after the turn deadlocks; `vmTimeoutMs` bounds synchronous evaluation only; and mounts do not survive session resume.
|
||||
@@ -81,4 +81,4 @@ vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节
|
||||
|
||||
## 后果
|
||||
|
||||
该工具集是刻意的显式启用设计,具有完全特权的 `ctx`,因此部署方采用它的意识程度应与 bash 工具相当。以下几个事实由工具描述直接告知模型:一个 waterfall(瀑布式事件)监听器(如 `tools/pre-execute`)如果不调用 `next()` 就返回,会否决整条链,因此一个挂载的监听器可以瘫痪 agent 自身的工具分发([waterfall 语义](../../../../docs/cordis-primer.md#cordis-waterfall-semantics));挂载代码在当前轮次的工具调用内运行,因此 await 任何只在该轮次结束后才 resolve 的东西会导致死锁;`vmTimeoutMs` 仅约束同步执行;挂载不会在会话恢复后存活。
|
||||
该工具集是刻意的显式启用设计,具有完全特权的 `ctx`,因此部署方采用它的意识程度应与 bash 工具相当。以下几个事实由工具描述直接告知模型:一个 waterfall(瀑布式事件)监听器(如 `tools/pre-execute`)如果不调用 `next()` 就返回,会短路整条链,因此一个挂载的监听器可以阻止 agent 自身的工具分发([waterfall 语义](../../../../docs/cordis-primer.md#cordis-waterfall-semantics));挂载代码在当前轮次的工具调用内运行,因此 await 任何只在该轮次结束后才 resolve 的东西会导致死锁;`vmTimeoutMs` 仅约束同步执行;挂载不会在会话恢复后存活。
|
||||
@@ -101,7 +101,7 @@ Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`,
|
||||
- **Runtime invariants assert owned relationships.** Check authoritative event streams or mutable data, not service or method presence, plugin metadata or effects, or fixed pure examples. If a package has no plausible relationship, an explained empty companion is correct ([package contract](packages/AGENTS.md)).
|
||||
- **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`; scoped keys absent from payloads need `@dshScopeScan unsupported`. Public service methods document parameters and non-void returns.
|
||||
- **Switch on discriminant tags.** Closed unions end in `assertNever`; merge-extensible unions fall through a documented default.
|
||||
- **Waterfall listeners MUST call `next()`** to delegate; returning without it is the veto ([semantics](docs/cordis-primer.md#cordis-waterfall-semantics)).
|
||||
- **Waterfall listeners MUST call `next()`** to delegate; returning without it short-circuits the chain ([semantics](docs/cordis-primer.md#cordis-waterfall-semantics)).
|
||||
- **Model-visible ⟺ logged**: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event.
|
||||
- **Plugins, not loop changes**: new behavior goes on the documented extension seams; changing `agent-loop` requires updating docs/architecture.md.
|
||||
- **Capability seams are three packages** — interface / implementation / consumer; don't split preemptively.
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/architecture.md
|
||||
architecture.md: f593ee5c54e5bd2cbdaa5da9f2de54b7b08ec14b
|
||||
architecture.zh.md: 80028f33c620b35ffc549b4f4828a23a5f930e93
|
||||
architecture.md: 06c15f204b3a695de49df29938781f8f033c4a7e
|
||||
architecture.zh.md: 29fe94f5a94c317854366e811dc6ceb74d2fddfe
|
||||
@@ -63,7 +63,7 @@ Events are the service extension API ([subsystems](subsystems/core.md), [produce
|
||||
|
||||
### Interception Semantics
|
||||
|
||||
Waterfalls are around-middleware: listeners delegate with `next()`; returning without it vetoes or takes over ([semantics](cordis-primer.md#cordis-waterfall-semantics)).
|
||||
Waterfalls are around-middleware: listeners delegate with `next()`; returning without it short-circuits or takes over ([semantics](cordis-primer.md#cordis-waterfall-semantics)).
|
||||
|
||||
## Default Loop Lifecycle
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
### 拦截语义
|
||||
|
||||
waterfall(瀑布式事件)是环绕中间件:监听器通过 `next()` 委托;不调用它而直接返回会否决或接管([语义](cordis-primer.md#cordis-waterfall-semantics))。
|
||||
waterfall(瀑布式事件)是环绕中间件:监听器通过 `next()` 委托;不调用它而直接返回会短路或接管([语义](cordis-primer.md#cordis-waterfall-semantics))。
|
||||
|
||||
## 默认循环生命周期
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verifie
|
||||
## Inherited `ctx` members (cordis core + loader/hmr/timer)
|
||||
|
||||
- `ctx.on / ctx.once` — Register an event listener (disposable). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts))
|
||||
- `ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall` — Dispatch an event (sync / awaited / first-bail / veto-chain). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts))
|
||||
- `ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall` — Dispatch an event (sync / awaited / first-bail / short-circuit chain). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts))
|
||||
- `ctx.plugin / ctx.inject` — Load a plugin / declare required services. ([`vendor/cordis/src/registry.ts:164`](../../vendor/cordis/src/registry.ts))
|
||||
- `ctx.effect` — Register a disposable side effect tied to the fiber. ([`vendor/cordis/src/fiber.ts:9`](../../vendor/cordis/src/fiber.ts))
|
||||
- `ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin` — Low-level service-store access and binding. ([`vendor/cordis/src/reflect.ts:7`](../../vendor/cordis/src/reflect.ts))
|
||||
|
||||
@@ -284,7 +284,7 @@ Source: [`packages/self-modification/tool-cordis/src/index.ts`](../packages/self
|
||||
|
||||
### `cordis_mount`
|
||||
|
||||
Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` VETOES the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.
|
||||
Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/user/develop/framework/events.md
|
||||
events.md: c70150b4d9070063513021e420f253ff6d4e5de1
|
||||
events.zh.md: b3b79a86d249eed0df15380e17a1ee71766b1a72
|
||||
events.md: 8a8c076d9c7b40d73182db074c4f494fded8c6dd
|
||||
events.zh.md: 9649d89d575a1b05fa524bd460043da71dc9ae43
|
||||
@@ -63,7 +63,7 @@ await ctx.serial('setup-phase', context)
|
||||
|
||||
### waterfall — pipeline
|
||||
|
||||
Each listener may wrap the downstream result to form a processing chain. A listener **must call `next()` to delegate downstream**; omitting the call vetoes the pipeline:
|
||||
Each listener may wrap the downstream result to form a processing chain. A listener **must call `next()` to delegate downstream**; omitting the call short-circuits the pipeline:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
@@ -77,7 +77,7 @@ ctx.on('my-plugin/transform', async (_input, next) => {
|
||||
```
|
||||
|
||||
::: warning
|
||||
A waterfall listener **must call `next()`**. Omitting it vetoes the pipeline by design, enabling interception and gateway behavior.
|
||||
A waterfall listener **must call `next()`**. Omitting it short-circuits the pipeline by design, enabling interception and gateway behavior.
|
||||
:::
|
||||
|
||||
## Typed events
|
||||
|
||||
@@ -63,7 +63,7 @@ await ctx.serial('setup-phase', context)
|
||||
|
||||
### waterfall(瀑布式事件)— 流水线
|
||||
|
||||
每个监听器可以包装下游返回值,形成处理链。**必须调用 `next()` 传递给下游**,不调用即为否决:
|
||||
每个监听器可以包装下游返回值,形成处理链。**必须调用 `next()` 传递给下游**,不调用即会短路流水线:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
@@ -77,7 +77,7 @@ ctx.on('my-plugin/transform', async (_input, next) => {
|
||||
```
|
||||
|
||||
::: warning
|
||||
waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个流水线,这是故意为之的设计——用于实现拦截/网关逻辑。
|
||||
waterfall 监听器**必须调用 `next()`**。不调用 `next` 会短路整个流水线,这是故意为之的设计——用于实现拦截/网关逻辑。
|
||||
:::
|
||||
|
||||
## 类型安全的事件
|
||||
|
||||
@@ -60,7 +60,7 @@ interface ToolArgsMap {
|
||||
/** Exact service key or event name whose original JSDoc to include; valid only with what:"api" or what:"events". */
|
||||
name?: string;
|
||||
} & Record<string, JsonValue>;
|
||||
/** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` VETOES the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */
|
||||
/** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:"events"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */
|
||||
cordis_mount: {
|
||||
/** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */
|
||||
code: string;
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
},
|
||||
{
|
||||
"name": "cordis_mount",
|
||||
"description": "Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` VETOES the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.",
|
||||
"description": "Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{"type":"assistant/chunk","seq":23,"time":1785730459921,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":24,"time":1785730459921,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-tools-event","name":"cordis_inspect","arguments":"{\"what\":\"events\",\"name\":\"tools/pre-execute\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"283c82b3-1bda-481c-a716-c35f363c9752"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":25,"time":1785730459921,"data":{"turn":1,"step":2,"callId":"inspect-tools-event","name":"cordis_inspect","arguments":"{\"what\":\"events\",\"name\":\"tools/pre-execute\"}"}}
|
||||
{"type":"tool/result","seq":26,"time":1785730459929,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"inspect-tools-event"},"content":[{"type":"tool-result","toolCallId":"inspect-tools-event","content":[{"type":"text","text":"## events\n- tools/pre-execute [waterfall] — Allow, deny, or ask before dispatch.\n /**\n * Allow, deny, or ask before dispatch. `next()` delegates to allow; missing\n * approval support turns `ask` into denial. Async gates must observe\n * `exec.signal`; the registry rechecks cancellation after they settle but\n * never abandons their promise.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.\n * @param exec - the pending call (name, parsed arguments, caller agent).\n * @mode waterfall\n */\n 'tools/pre-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>\nwaterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() vetoes the chain."}],"isError":false}],"role":"user","id":"c187306a-d73c-4bcd-b76c-8607ddbc0974"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":26,"time":1785730459929,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"inspect-tools-event"},"content":[{"type":"tool-result","toolCallId":"inspect-tools-event","content":[{"type":"text","text":"## events\n- tools/pre-execute [waterfall] — Allow, deny, or ask before dispatch.\n /**\n * Allow, deny, or ask before dispatch. `next()` delegates to allow; missing\n * approval support turns `ask` into denial. Async gates must observe\n * `exec.signal`; the registry rechecks cancellation after they settle but\n * never abandons their promise.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.\n * @param exec - the pending call (name, parsed arguments, caller agent).\n * @mode waterfall\n */\n 'tools/pre-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>\nwaterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() short-circuits the chain."}],"isError":false}],"role":"user","id":"c187306a-d73c-4bcd-b76c-8607ddbc0974"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":27,"time":1785730459929,"data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","seq":28,"time":1785730459939,"data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1784449176735,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -338,7 +338,7 @@ describe('agent loop', () => {
|
||||
expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou run on mock.')
|
||||
})
|
||||
|
||||
it('omits the system field when a system-prompt/assemble veto empties the assembly', async () => {
|
||||
it('omits the system field when system-prompt/assemble short-circuits with an empty assembly', async () => {
|
||||
// The documented escape valve: a deployment that must drop the harness
|
||||
// openers short-circuits the assemble waterfall; the request then carries
|
||||
// NO system field at all (not an empty string).
|
||||
|
||||
@@ -3306,7 +3306,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
/** The inherited `ctx` surface (cordis core + loader/hmr/timer), in curated order. */
|
||||
export const INHERITED_CTX_API: readonly InheritedApiEntry[] = [
|
||||
{ name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).' },
|
||||
{ name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / veto-chain).' },
|
||||
{ name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / short-circuit chain).' },
|
||||
{ name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.' },
|
||||
{ name: 'ctx.effect', summary: 'Register a disposable side effect tied to the fiber.' },
|
||||
{ name: 'ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin', summary: 'Low-level service-store access and binding.' },
|
||||
|
||||
@@ -154,7 +154,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
+ 'Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). '
|
||||
+ 'Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a '
|
||||
+ 'trailing `next` callback which MUST be called — returning without `next()` '
|
||||
+ 'VETOES the call; prefer plain notification events unless you intend to '
|
||||
+ 'SHORT-CIRCUITS the call; prefer plain notification events unless you intend to '
|
||||
+ 'intercept. (2) Never await something that only resolves after the current '
|
||||
+ 'turn (your code runs INSIDE a tool call of that turn — it would deadlock). '
|
||||
+ '(3) Your `ctx` is a restricted façade: you can register tools, observe '
|
||||
|
||||
@@ -224,6 +224,6 @@ export function describeEvents(events: readonly EventApiEntry[] = EVENT_API, nam
|
||||
entry.push(` ${event.signature}`)
|
||||
return entry
|
||||
})
|
||||
lines.push('waterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() vetoes the chain.')
|
||||
lines.push('waterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() short-circuits the chain.')
|
||||
return lines
|
||||
}
|
||||
@@ -95,7 +95,7 @@ describe('cordis_inspect', () => {
|
||||
expect(report).toContain('- tools/change [emit]')
|
||||
expect(report).toContain('- tools/pre-execute [waterfall]')
|
||||
expect(report).toMatch(/'agent\/status'\(/)
|
||||
expect(report).toContain('returning without next() vetoes the chain')
|
||||
expect(report).toContain('returning without next() short-circuits the chain')
|
||||
expect(report).not.toContain('/**')
|
||||
expect(report).not.toContain('@mode waterfall')
|
||||
})
|
||||
@@ -171,7 +171,7 @@ describe('inspect renderers (direct)', () => {
|
||||
|
||||
it('describeEvents renders an empty catalog as just the waterfall caution', () => {
|
||||
expect(describeEvents([])).toEqual([
|
||||
'waterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() vetoes the chain.',
|
||||
'waterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() short-circuits the chain.',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -508,7 +508,7 @@ export const CORDIS_CATALOG_POLICY: CordisCatalogPolicy = {
|
||||
],
|
||||
inheritedServices: [
|
||||
{ name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).', source: 'vendor/cordis/src/events.ts:34' },
|
||||
{ name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / veto-chain).', source: 'vendor/cordis/src/events.ts:34' },
|
||||
{ name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / short-circuit chain).', source: 'vendor/cordis/src/events.ts:34' },
|
||||
{ name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.', source: 'vendor/cordis/src/registry.ts:164' },
|
||||
{ name: 'ctx.effect', summary: 'Register a disposable side effect tied to the fiber.', source: 'vendor/cordis/src/fiber.ts:9' },
|
||||
{ name: 'ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin', summary: 'Low-level service-store access and binding.', source: 'vendor/cordis/src/reflect.ts:7' },
|
||||
|
||||
Reference in New Issue
Block a user