dsh-plugin-dev-kb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/cordis.patch.yml +12 -0
- package/kb/INDEX.md +210 -0
- package/kb/README.md +69 -0
- package/kb/extra/AGENTS.md +75 -0
- package/kb/extra/api-gateway.md +164 -0
- package/kb/extra/api-gateway.zh.md +164 -0
- package/kb/extra/cookbook/adding-a-vendored-package.md +59 -0
- package/kb/extra/cookbook/adding-a-vendored-package.zh.md +59 -0
- package/kb/extra/cookbook/maintaining-dsh-code-review.md +64 -0
- package/kb/extra/cookbook/maintaining-dsh-code-review.zh.md +64 -0
- package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.md +32 -0
- package/kb/extra/cookbook/responding-to-pr-review-on-a-stack.zh.md +32 -0
- package/kb/extra/defensive-patterns.md +33 -0
- package/kb/extra/defensive-patterns.zh.md +33 -0
- package/kb/extra/development.md +171 -0
- package/kb/extra/development.zh.md +171 -0
- package/kb/extra/event-producer-consumer.md +76 -0
- package/kb/extra/event-producer-consumer.zh.md +78 -0
- package/kb/extra/glossary.md +45 -0
- package/kb/extra/glossary.zh.md +45 -0
- package/kb/extra/graph-atlas.md +24 -0
- package/kb/extra/graph-atlas.zh.md +26 -0
- package/kb/extra/i18n/README.md +60 -0
- package/kb/extra/i18n/README.zh.md +60 -0
- package/kb/extra/i18n/style-samples.md +87 -0
- package/kb/extra/i18n/terminology.md +214 -0
- package/kb/extra/i18n/translation-prompt.md +263 -0
- package/kb/extra/i18n/translation-rules.md +69 -0
- package/kb/extra/i18n/translation-rules.zh.md +69 -0
- package/kb/extra/module-graph.md +1641 -0
- package/kb/extra/module-graph.zh.md +1643 -0
- package/kb/extra/postmortem/0001-acp-default-export-drops-inject.md +113 -0
- package/kb/extra/postmortem/0001-acp-default-export-drops-inject.zh.md +113 -0
- package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.md +47 -0
- package/kb/extra/postmortem/0002-js-expression-disabled-filesystem-tools.zh.md +47 -0
- package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.md +53 -0
- package/kb/extra/postmortem/0003-web-agent-gui-feedback-loop.zh.md +53 -0
- package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.md +55 -0
- package/kb/extra/postmortem/0004-landlock-partial-notice-misclassified-child-failures.zh.md +55 -0
- package/kb/extra/postmortem/README.md +18 -0
- package/kb/extra/postmortem/README.zh.md +18 -0
- package/kb/extra/rescope.md +53 -0
- package/kb/extra/rescope.zh.md +53 -0
- package/kb/extra/subsystems/attachment.md +125 -0
- package/kb/extra/subsystems/attachment.zh.md +125 -0
- package/kb/extra/subsystems/extensions.md +364 -0
- package/kb/extra/subsystems/extensions.zh.md +364 -0
- package/kb/extra/subsystems/feedback.md +266 -0
- package/kb/extra/subsystems/feedback.zh.md +266 -0
- package/kb/extra/testing.md +49 -0
- package/kb/extra/testing.zh.md +49 -0
- package/kb/extra/web-styling.md +25 -0
- package/kb/extra/web-styling.zh.md +25 -0
- package/kb/meta/search-index.json +1328 -0
- package/kb/meta/site-pages.txt +168 -0
- package/kb/meta/source.json +13 -0
- package/kb/meta/topics.md +75 -0
- package/kb/site/develop/basic/config.md +108 -0
- package/kb/site/develop/basic/index.md +146 -0
- package/kb/site/develop/basic/publish.md +185 -0
- package/kb/site/develop/basic/tool.md +54 -0
- package/kb/site/develop/cordis-tutorial/01-first-plugin.md +95 -0
- package/kb/site/develop/cordis-tutorial/02-lifecycle-and-effects.md +98 -0
- package/kb/site/develop/cordis-tutorial/03-services.md +98 -0
- package/kb/site/develop/cordis-tutorial/04-events.md +144 -0
- package/kb/site/develop/cordis-tutorial/05-config.md +84 -0
- package/kb/site/develop/cordis-tutorial/06-composition-and-hmr.md +113 -0
- package/kb/site/develop/cordis-tutorial/07-into-the-harness.md +107 -0
- package/kb/site/develop/cordis-tutorial/index.md +62 -0
- package/kb/site/develop/framework/events.md +145 -0
- package/kb/site/develop/framework/index.md +139 -0
- package/kb/site/develop/framework/service.md +152 -0
- package/kb/site/develop/practice/index.md +157 -0
- package/kb/site/develop/practice/llm-adapter.md +190 -0
- package/kb/site/en/develop/basic/config.md +108 -0
- package/kb/site/en/develop/basic/index.md +146 -0
- package/kb/site/en/develop/basic/publish.md +185 -0
- package/kb/site/en/develop/basic/tool.md +54 -0
- package/kb/site/en/develop/cordis-tutorial/01-first-plugin.md +95 -0
- package/kb/site/en/develop/cordis-tutorial/02-lifecycle-and-effects.md +98 -0
- package/kb/site/en/develop/cordis-tutorial/03-services.md +98 -0
- package/kb/site/en/develop/cordis-tutorial/04-events.md +144 -0
- package/kb/site/en/develop/cordis-tutorial/05-config.md +84 -0
- package/kb/site/en/develop/cordis-tutorial/06-composition-and-hmr.md +113 -0
- package/kb/site/en/develop/cordis-tutorial/07-into-the-harness.md +107 -0
- package/kb/site/en/develop/cordis-tutorial/index.md +60 -0
- package/kb/site/en/develop/framework/events.md +145 -0
- package/kb/site/en/develop/framework/index.md +139 -0
- package/kb/site/en/develop/framework/service.md +150 -0
- package/kb/site/en/develop/practice/index.md +157 -0
- package/kb/site/en/develop/practice/llm-adapter.md +190 -0
- package/kb/site/en/guide/providers-custom-form.png +0 -0
- package/kb/site/en/guide/providers-models-page.png +0 -0
- package/kb/site/en/guide/providers.md +100 -0
- package/kb/site/en/guide/python-sdk.md +106 -0
- package/kb/site/en/guide/quickstart.md +32 -0
- package/kb/site/en/index.md +8 -0
- package/kb/site/en/reference/agent-lifecycle.md +86 -0
- package/kb/site/en/reference/capability-seams.md +475 -0
- package/kb/site/en/reference/config-catalog.md +3155 -0
- package/kb/site/en/reference/cookbook/adding-a-conversation-node.md +235 -0
- package/kb/site/en/reference/cookbook/adding-a-package.md +120 -0
- package/kb/site/en/reference/cookbook/adding-a-settings-card.md +102 -0
- package/kb/site/en/reference/cookbook/adding-a-tool.md +96 -0
- package/kb/site/en/reference/cookbook/adding-an-llm-adapter.md +45 -0
- package/kb/site/en/reference/cookbook/extension-cookbook.md +131 -0
- package/kb/site/en/reference/cordis-api/context.md +368 -0
- package/kb/site/en/reference/cordis-api/events.md +211 -0
- package/kb/site/en/reference/cordis-api/fiber.md +379 -0
- package/kb/site/en/reference/cordis-api/inherited.md +43 -0
- package/kb/site/en/reference/cordis-api/registry.md +156 -0
- package/kb/site/en/reference/cordis-api/service.md +106 -0
- package/kb/site/en/reference/cordis-primer.md +46 -0
- package/kb/site/en/reference/index.md +131 -0
- package/kb/site/en/reference/persistence-catalog.md +949 -0
- package/kb/site/en/reference/subsystems/approval.md +173 -0
- package/kb/site/en/reference/subsystems/client-modules.md +121 -0
- package/kb/site/en/reference/subsystems/code-runtime.md +194 -0
- package/kb/site/en/reference/subsystems/commands.md +190 -0
- package/kb/site/en/reference/subsystems/compaction.md +241 -0
- package/kb/site/en/reference/subsystems/core.md +1073 -0
- package/kb/site/en/reference/subsystems/credentials.md +136 -0
- package/kb/site/en/reference/subsystems/filesystem.md +498 -0
- package/kb/site/en/reference/subsystems/goal.md +280 -0
- package/kb/site/en/reference/subsystems/index.md +58 -0
- package/kb/site/en/reference/subsystems/invariants.md +91 -0
- package/kb/site/en/reference/subsystems/jobs.md +293 -0
- package/kb/site/en/reference/subsystems/llm-streaming.md +920 -0
- package/kb/site/en/reference/subsystems/lsp.md +205 -0
- package/kb/site/en/reference/subsystems/permission-presets.md +134 -0
- package/kb/site/en/reference/subsystems/persistence.md +388 -0
- package/kb/site/en/reference/subsystems/plan.md +90 -0
- package/kb/site/en/reference/subsystems/sandbox.md +221 -0
- package/kb/site/en/reference/subsystems/schedule.md +189 -0
- package/kb/site/en/reference/subsystems/scope.md +62 -0
- package/kb/site/en/reference/subsystems/session-projection.md +265 -0
- package/kb/site/en/reference/subsystems/session-query.md +498 -0
- package/kb/site/en/reference/subsystems/session-reference.md +111 -0
- package/kb/site/en/reference/subsystems/session-telemetry.md +197 -0
- package/kb/site/en/reference/subsystems/session-title.md +207 -0
- package/kb/site/en/reference/subsystems/session.md +852 -0
- package/kb/site/en/reference/subsystems/settings.md +313 -0
- package/kb/site/en/reference/subsystems/shell.md +306 -0
- package/kb/site/en/reference/subsystems/skills.md +334 -0
- package/kb/site/en/reference/subsystems/spill.md +120 -0
- package/kb/site/en/reference/subsystems/storage.md +232 -0
- package/kb/site/en/reference/subsystems/subagent.md +737 -0
- package/kb/site/en/reference/subsystems/subprocess.md +327 -0
- package/kb/site/en/reference/subsystems/system-prompt.md +210 -0
- package/kb/site/en/reference/subsystems/terminal.md +187 -0
- package/kb/site/en/reference/subsystems/token-meter.md +93 -0
- package/kb/site/en/reference/subsystems/tools.md +723 -0
- package/kb/site/en/reference/subsystems/typert.md +339 -0
- package/kb/site/en/reference/subsystems/user-questions.md +181 -0
- package/kb/site/en/reference/subsystems/web-server.md +111 -0
- package/kb/site/en/reference/subsystems/web.md +202 -0
- package/kb/site/en/reference/subsystems/workflow.md +281 -0
- package/kb/site/en/reference/subsystems/workspace.md +231 -0
- package/kb/site/en/reference/tool-catalog.md +1877 -0
- package/kb/site/en/reference/tool-execution-pipeline.md +66 -0
- package/kb/site/guide/providers-custom-form.zh.png +0 -0
- package/kb/site/guide/providers-models-page.zh.png +0 -0
- package/kb/site/guide/providers.md +100 -0
- package/kb/site/guide/python-sdk.md +106 -0
- package/kb/site/guide/quickstart.md +32 -0
- package/kb/site/index.md +8 -0
- package/kb/site/reference/agent-lifecycle.md +86 -0
- package/kb/site/reference/capability-seams.md +475 -0
- package/kb/site/reference/config-catalog.md +3154 -0
- package/kb/site/reference/cookbook/adding-a-conversation-node.md +235 -0
- package/kb/site/reference/cookbook/adding-a-package.md +120 -0
- package/kb/site/reference/cookbook/adding-a-settings-card.md +102 -0
- package/kb/site/reference/cookbook/adding-a-tool.md +98 -0
- package/kb/site/reference/cookbook/adding-an-llm-adapter.md +45 -0
- package/kb/site/reference/cookbook/extension-cookbook.md +133 -0
- package/kb/site/reference/cordis-api/context.md +368 -0
- package/kb/site/reference/cordis-api/events.md +211 -0
- package/kb/site/reference/cordis-api/fiber.md +379 -0
- package/kb/site/reference/cordis-api/inherited.md +43 -0
- package/kb/site/reference/cordis-api/registry.md +156 -0
- package/kb/site/reference/cordis-api/service.md +106 -0
- package/kb/site/reference/cordis-primer.md +52 -0
- package/kb/site/reference/index.md +135 -0
- package/kb/site/reference/persistence-catalog.md +949 -0
- package/kb/site/reference/subsystems/approval.md +173 -0
- package/kb/site/reference/subsystems/client-modules.md +121 -0
- package/kb/site/reference/subsystems/code-runtime.md +194 -0
- package/kb/site/reference/subsystems/commands.md +190 -0
- package/kb/site/reference/subsystems/compaction.md +241 -0
- package/kb/site/reference/subsystems/core.md +1081 -0
- package/kb/site/reference/subsystems/credentials.md +136 -0
- package/kb/site/reference/subsystems/filesystem.md +498 -0
- package/kb/site/reference/subsystems/goal.md +280 -0
- package/kb/site/reference/subsystems/index.md +58 -0
- package/kb/site/reference/subsystems/invariants.md +91 -0
- package/kb/site/reference/subsystems/jobs.md +293 -0
- package/kb/site/reference/subsystems/llm-streaming.md +926 -0
- package/kb/site/reference/subsystems/lsp.md +205 -0
- package/kb/site/reference/subsystems/permission-presets.md +134 -0
- package/kb/site/reference/subsystems/persistence.md +388 -0
- package/kb/site/reference/subsystems/plan.md +90 -0
- package/kb/site/reference/subsystems/sandbox.md +221 -0
- package/kb/site/reference/subsystems/schedule.md +189 -0
- package/kb/site/reference/subsystems/scope.md +62 -0
- package/kb/site/reference/subsystems/session-projection.md +265 -0
- package/kb/site/reference/subsystems/session-query.md +498 -0
- package/kb/site/reference/subsystems/session-reference.md +111 -0
- package/kb/site/reference/subsystems/session-telemetry.md +197 -0
- package/kb/site/reference/subsystems/session-title.md +207 -0
- package/kb/site/reference/subsystems/session.md +854 -0
- package/kb/site/reference/subsystems/settings.md +313 -0
- package/kb/site/reference/subsystems/shell.md +306 -0
- package/kb/site/reference/subsystems/skills.md +334 -0
- package/kb/site/reference/subsystems/spill.md +120 -0
- package/kb/site/reference/subsystems/storage.md +232 -0
- package/kb/site/reference/subsystems/subagent.md +739 -0
- package/kb/site/reference/subsystems/subprocess.md +327 -0
- package/kb/site/reference/subsystems/system-prompt.md +210 -0
- package/kb/site/reference/subsystems/terminal.md +187 -0
- package/kb/site/reference/subsystems/token-meter.md +93 -0
- package/kb/site/reference/subsystems/tools.md +723 -0
- package/kb/site/reference/subsystems/typert.md +339 -0
- package/kb/site/reference/subsystems/user-questions.md +181 -0
- package/kb/site/reference/subsystems/web-server.md +111 -0
- package/kb/site/reference/subsystems/web.md +202 -0
- package/kb/site/reference/subsystems/workflow.md +281 -0
- package/kb/site/reference/subsystems/workspace.md +231 -0
- package/kb/site/reference/tool-catalog.md +1880 -0
- package/kb/site/reference/tool-execution-pipeline.md +66 -0
- package/package.json +40 -0
- package/scripts/rebuild-index.mjs +88 -0
- package/skills/dsh-plugin-dev-kb.md +66 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/cordis-tutorial/04-events.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 4. Events
|
|
6
|
+
|
|
7
|
+
Services support direct calls; **events** let a plugin announce something without knowing which plugins listen. The harness uses events for interactions such as tool results, model requests, and approval decisions.
|
|
8
|
+
|
|
9
|
+
## Declare, emit, listen
|
|
10
|
+
|
|
11
|
+
Create `stats.ts` in `tmp/cordis-tutorial` — a service that counts things and announces each change:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
15
|
+
|
|
16
|
+
declare module '@deepseek-ai/cordis' {
|
|
17
|
+
interface Context {
|
|
18
|
+
stats: StatsService
|
|
19
|
+
}
|
|
20
|
+
interface Events {
|
|
21
|
+
'stats/report'(name: string, count: number): void
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class StatsService extends Service {
|
|
26
|
+
private counts = new Map<string, number>()
|
|
27
|
+
|
|
28
|
+
constructor(ctx: Context) {
|
|
29
|
+
super(ctx, 'stats')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
bump(name: string) {
|
|
33
|
+
const next = (this.counts.get(name) ?? 0) + 1
|
|
34
|
+
this.counts.set(name, next)
|
|
35
|
+
this.ctx.emit('stats/report', name, next)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const name = 'stats'
|
|
40
|
+
|
|
41
|
+
export function apply(ctx: Context) {
|
|
42
|
+
ctx.plugin(StatsService)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The `interface Events` merge is the event-system twin of the `interface Context` merge from chapter 3: it declares the event name and its listener signature, so `ctx.emit` and `ctx.on` are fully typed. The `namespace/action` naming convention keeps the flat event namespace readable.
|
|
47
|
+
|
|
48
|
+
Create `reporter.ts`:
|
|
49
|
+
|
|
50
|
+
```ts ignore-check
|
|
51
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
52
|
+
import type {} from './stats.ts'
|
|
53
|
+
|
|
54
|
+
export const name = 'reporter'
|
|
55
|
+
export const inject = ['stats']
|
|
56
|
+
|
|
57
|
+
export function apply(ctx: Context) {
|
|
58
|
+
ctx.on('stats/report', (name, count) => {
|
|
59
|
+
console.log(`[stats] ${name} -> ${count}`)
|
|
60
|
+
})
|
|
61
|
+
ctx.stats.bump('tool_call')
|
|
62
|
+
ctx.stats.bump('tool_call')
|
|
63
|
+
ctx.stats.bump('prompt')
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The `import type {} from './stats.ts'` line imports nothing at runtime; it exists so TypeScript sees the declaration merges. Compose and run:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
- name: './stats.ts'
|
|
71
|
+
- name: './reporter.ts'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
[stats] tool_call -> 1
|
|
76
|
+
[stats] tool_call -> 2
|
|
77
|
+
[stats] prompt -> 1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Because `ctx.on()` is an effect, the listener disappears with the plugin — no manual `removeListener` bookkeeping, ever.
|
|
81
|
+
|
|
82
|
+
## Dispatch modes
|
|
83
|
+
|
|
84
|
+
`emit` is one of five dispatch modes. Which one an event uses is part of its contract — it decides whether listeners can return values, run concurrently, or short-circuit each other:
|
|
85
|
+
|
|
86
|
+
| Mode | Call | Semantics |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| emit | `ctx.emit(name, ...args)` | Synchronous broadcast; returned promises and values are not awaited or collected. |
|
|
89
|
+
| parallel | `await ctx.parallel(name, ...args)` | All listeners run concurrently; awaited together. |
|
|
90
|
+
| serial | `await ctx.serial(name, ...args)` | Listeners run in order, awaited; the first non-`null`/`false`/`undefined` return wins and stops the rest. |
|
|
91
|
+
| bail | `ctx.bail(name, ...args)` | Synchronous version of serial. |
|
|
92
|
+
| waterfall | `ctx.waterfall(name, ...args, next)` | Around-middleware; see below. |
|
|
93
|
+
|
|
94
|
+
Every harness event documents its mode in the generated reference on its owning [subsystem page](../../reference/subsystems/core.md).
|
|
95
|
+
|
|
96
|
+
## Waterfall: transform or short-circuit
|
|
97
|
+
|
|
98
|
+
Waterfall is the mode that powers interception. Each listener receives the arguments plus a `next()` continuation; it can transform what `next()` returns, or return without calling `next()` and short-circuit the rest of the chain — what the Cordis docs call the veto. Create `waterfall-demo.ts`:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
102
|
+
|
|
103
|
+
declare module '@deepseek-ai/cordis' {
|
|
104
|
+
interface Events {
|
|
105
|
+
'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const name = 'waterfall-demo'
|
|
110
|
+
|
|
111
|
+
export function apply(ctx: Context) {
|
|
112
|
+
// Listener 1: wrap the downstream result.
|
|
113
|
+
ctx.on('demo/transform', async (input, next) => {
|
|
114
|
+
const downstream = await next()
|
|
115
|
+
return downstream.toUpperCase()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Listener 2: short-circuit when it owns the decision.
|
|
119
|
+
ctx.on('demo/transform', async (input, next) => {
|
|
120
|
+
if (input.includes('blocked')) return '** blocked **'
|
|
121
|
+
return next()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
void (async () => {
|
|
125
|
+
console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello'))
|
|
126
|
+
console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'blocked words'))
|
|
127
|
+
})()
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Point `cordis.yml` at just this file and run:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
HELLO
|
|
135
|
+
** BLOCKED **
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Walk through the second line: listener 1 runs first, calls `next()`, which invokes listener 2; listener 2 sees `blocked` and returns without calling `next()` — the innermost default (the function passed to `ctx.waterfall`) never runs — and listener 1 uppercases the replacement message on the way out.
|
|
139
|
+
|
|
140
|
+
The discipline that follows: **a waterfall listener that only observes or annotates must call `next()`**; returning without it is a deliberate short-circuit. Forgetting `next()` in a logging listener silently swallows the default behavior for everyone downstream. It is a standing rule of this repository ([waterfall semantics](../../reference/cordis-primer.md#cordis-waterfall-semantics)).
|
|
141
|
+
|
|
142
|
+
The harness uses waterfalls for decisions that cooperating plugins may wrap or answer: [`agent/request`](../../reference/subsystems/core.md#agentrequest--waterfall) lets a plugin replace the model-call config, and [`approval/request`](../../reference/subsystems/approval.md#approvalrequest--waterfall) lets a policy answer instead of the user.
|
|
143
|
+
|
|
144
|
+
Next: [Configuration](./05-config.md) — plugin options from `cordis.yml`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/cordis-tutorial/05-config.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 5. Configuration
|
|
6
|
+
|
|
7
|
+
Each `cordis.yml` entry can carry a `config` block, and the plugin declares a schema that validates it before `apply` runs. Bad config fails the load with a precise error — the plugin never starts half-configured.
|
|
8
|
+
|
|
9
|
+
## A configurable plugin
|
|
10
|
+
|
|
11
|
+
Create `config-demo.ts` in `tmp/cordis-tutorial`:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
15
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
16
|
+
|
|
17
|
+
export const name = 'config-demo'
|
|
18
|
+
|
|
19
|
+
export interface Config {
|
|
20
|
+
greeting: string
|
|
21
|
+
targets: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Config: Schema<Config> = Schema.object({
|
|
25
|
+
greeting: Schema.string().default('Hello'),
|
|
26
|
+
targets: Schema.array(String).default(['world']),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export function apply(ctx: Context, config: Config) {
|
|
30
|
+
for (const target of config.targets) {
|
|
31
|
+
console.log(`${config.greeting}, ${target}!`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The exported `Config` is both a TypeScript interface and a runtime schema with the same name — consumers get the type, Cordis gets the validator. This repo uses [Schemastery](https://github.com/shigma/schemastery) for schemas; Cordis itself accepts any [Standard Schema](https://standardschema.dev/) validator, so a plain object exported as `Config` will not work.
|
|
37
|
+
|
|
38
|
+
Configure it:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
- name: './config-demo.ts'
|
|
42
|
+
config:
|
|
43
|
+
targets: ['alpha', 'beta']
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Hello, alpha!
|
|
50
|
+
Hello, beta!
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`greeting` was omitted, so the schema default filled it in — `apply` always receives complete, validated config.
|
|
54
|
+
|
|
55
|
+
## Fail loud
|
|
56
|
+
|
|
57
|
+
Now feed it something invalid:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
- name: './config-demo.ts'
|
|
61
|
+
config:
|
|
62
|
+
targets: 'not-an-array'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
ValidationError: invalid config:
|
|
67
|
+
- $.targets expected array but got not-an-array (at targets)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The plugin's fiber goes to FAILED, and this tutorial's launcher exits with status 1 after printing the error. A plugin should also reject schema-valid config that names an unavailable resource or provider as soon as it can resolve that reference.
|
|
71
|
+
|
|
72
|
+
## Computed config values
|
|
73
|
+
|
|
74
|
+
The loader used in this repo supports a `!!js` tag for config values that must be computed at load time:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
- name: './config-demo.ts'
|
|
78
|
+
config:
|
|
79
|
+
greeting: !!js process.env.DEMO_GREETING ?? 'Hello'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`!!js` works only inside `config` and in an entry's `disabled` field. `disabled: !!js ...` evaluates against the loader context at every mount decision (this repo's extension), so a row can gate itself on platform or environment; the other metadata (`name`, `id`, `inject`, ...) stays static, where an expression is ordinary truthy data. See [loader configuration](../../reference/cordis-primer.md#loader-configuration).
|
|
83
|
+
|
|
84
|
+
Next: [Composition and HMR](./06-composition-and-hmr.md) — treating `cordis.yml` as the application.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/cordis-tutorial/06-composition-and-hmr.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 6. Composition and HMR
|
|
6
|
+
|
|
7
|
+
Every capability built so far is a plugin, and `cordis.yml` selects the application's plugin tree. This chapter changes that composition, hot-reloads a plugin, and diagnoses a plugin that never loads.
|
|
8
|
+
|
|
9
|
+
## Entries are more than a name
|
|
10
|
+
|
|
11
|
+
A config entry accepts metadata beyond `name` and `config`:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
- id: greeter # stable identity for this entry
|
|
15
|
+
name: './greeter.ts'
|
|
16
|
+
- id: consumer
|
|
17
|
+
name: './consumer.ts'
|
|
18
|
+
disabled: true # keep the entry, skip mounting it
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`id` gives the entry a stable identity so the loader can tell an edit to an existing entry apart from a removal plus an addition. `disabled: true` unmounts a plugin without deleting its entry — flip it back and the plugin (and everything PENDING on its services) loads again.
|
|
22
|
+
|
|
23
|
+
Groups nest a sub-list of entries that load and unload as one unit, and `isolate` gives a group its own instance of a service name — two groups can each see a differently configured `shell` provider without affecting each other. The [Cordis primer](../../reference/cordis-primer.md) and the [service isolation example](../framework/service.md#service-isolation) cover the details.
|
|
24
|
+
|
|
25
|
+
## Hot module replacement
|
|
26
|
+
|
|
27
|
+
Because unloading releases effects ([chapter 2](./02-lifecycle-and-effects.md)) and loading follows dependencies ([chapter 3](./03-services.md)), HMR can replace a running plugin by unloading and loading it. The `@deepseek-ai/cordis-plugin-hmr` plugin watches your files and does exactly that on save.
|
|
28
|
+
|
|
29
|
+
In `tmp/cordis-tutorial`, write `cordis.yml`:
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
- id: logger
|
|
33
|
+
name: '@deepseek-ai/cordis-plugin-logger-console'
|
|
34
|
+
- id: timer
|
|
35
|
+
name: '@deepseek-ai/cordis-plugin-timer'
|
|
36
|
+
- id: hmr
|
|
37
|
+
name: '@deepseek-ai/cordis-plugin-hmr'
|
|
38
|
+
config:
|
|
39
|
+
root: ['.']
|
|
40
|
+
- id: hello
|
|
41
|
+
name: './hello.ts'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Two support plugins joined the list: HMR logs through the Cordis logger service, so without a console exporter you would not see its messages, and it `inject`s the `timer` service for debouncing — without `@deepseek-ai/cordis-plugin-timer` it sits in PENDING forever, silently. That silence is the subject of the next section.
|
|
45
|
+
|
|
46
|
+
HMR reads Node's loader internals through the Loader's native helper. Run Cordis under tsx:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
node --import tsx ../../vendor/cordis/bin.js
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Now edit `hello.ts` — change the log message — and save:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
hello from my first plugin
|
|
56
|
+
2026-07-22 15:44:36 [I] hmr watching [ '.' ]
|
|
57
|
+
2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts
|
|
58
|
+
hello from my EDITED plugin
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The old instance unloaded (all its effects unwound), the new code loaded, `apply` ran again. Stop the process with Ctrl-C. Editing `cordis.yml` itself is also picked up: the loader diffs entries by `id` and mounts, unmounts, or reconfigures only what changed. This is why the entries above carry explicit `id`s — an entry without one gets a generated id on every read, so after any config-file edit it counts as removed-plus-added and remounts even if its own lines did not change.
|
|
62
|
+
|
|
63
|
+
## Diagnosing a plugin that never loads
|
|
64
|
+
|
|
65
|
+
The flip side of dependency-driven loading: a plugin whose `inject` names a service nobody provides waits forever, printing nothing. No error — PENDING is a legitimate state, since the provider may be mounted later.
|
|
66
|
+
|
|
67
|
+
You can see the states directly. Every context can enumerate the plugin registry; create `diagnose.ts`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { FiberState, type Context } from '@deepseek-ai/cordis'
|
|
71
|
+
|
|
72
|
+
export const name = 'diagnose'
|
|
73
|
+
|
|
74
|
+
export function apply(ctx: Context) {
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
for (const runtime of ctx.registry.values()) {
|
|
77
|
+
for (const fiber of runtime.fibers) {
|
|
78
|
+
if (fiber.state === FiberState.PENDING) {
|
|
79
|
+
console.log(`${fiber.name} is PENDING — a required service is missing`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, 500)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
And a plugin with an unsatisfiable dependency, `needs-timer.ts`:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
91
|
+
|
|
92
|
+
export const name = 'needs-timer'
|
|
93
|
+
export const inject = ['timer']
|
|
94
|
+
|
|
95
|
+
export function apply(ctx: Context) {
|
|
96
|
+
console.log('needs-timer loaded')
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
- name: './needs-timer.ts'
|
|
102
|
+
- name: './diagnose.ts'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Run it (plain `node --import tsx ../../vendor/cordis/bin.js`; stop with Ctrl-C):
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
needs-timer is PENDING — a required service is missing
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`inject: ['timer']` has no provider. Add `- name: '@deepseek-ai/cordis-plugin-timer'` to the list and the plugin loads. When a plugin does nothing and reports nothing, inspect its fiber state. Iterating without the PENDING filter also shows the loader's own plugins (Loader, Include) as ACTIVE fibers because plugins mount the config file itself.
|
|
112
|
+
|
|
113
|
+
Next: [Into the harness](./07-into-the-harness.md) — the same patterns against real harness services.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/cordis-tutorial/07-into-the-harness.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 7. Into the harness
|
|
6
|
+
|
|
7
|
+
This chapter registers a model-callable tool with the harness's `tools` service, executes it through the harness tool pipeline, and observes the result event. It remains keyless and does not call a model.
|
|
8
|
+
|
|
9
|
+
## A tool plugin
|
|
10
|
+
|
|
11
|
+
Create `greet-tool.ts` in `tmp/cordis-tutorial`:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
15
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
16
|
+
import { CallId } from '@deepseek-ai/dsh-llm'
|
|
17
|
+
|
|
18
|
+
export const name = 'greet-tool'
|
|
19
|
+
export const inject = ['tools']
|
|
20
|
+
|
|
21
|
+
export function apply(ctx: Context) {
|
|
22
|
+
ctx.tools.register(defineTool({
|
|
23
|
+
name: 'greet',
|
|
24
|
+
description: 'Greet the named person.',
|
|
25
|
+
parameters: {
|
|
26
|
+
name: { type: 'string', required: true, description: 'Who to greet' },
|
|
27
|
+
},
|
|
28
|
+
output: {
|
|
29
|
+
schema: { type: 'string' },
|
|
30
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
31
|
+
},
|
|
32
|
+
async execute(args) {
|
|
33
|
+
return `Hello, ${args.name}!`
|
|
34
|
+
},
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
// Drive one call through the real execution pipeline, standing in for
|
|
38
|
+
// the model. CallId brands the correlation id a provider would issue.
|
|
39
|
+
void (async () => {
|
|
40
|
+
const result = await ctx.tools.execute({
|
|
41
|
+
callId: CallId('demo-1'),
|
|
42
|
+
name: 'greet',
|
|
43
|
+
arguments: { name: 'Cordis' },
|
|
44
|
+
signal: new AbortController().signal,
|
|
45
|
+
})
|
|
46
|
+
console.log('tool replied:', JSON.stringify(result.content))
|
|
47
|
+
})()
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Every pattern here is from the earlier chapters: `inject: ['tools']` ([chapter 3](./03-services.md)) holds the plugin until the tool registry exists; `ctx.tools.register(...)` attaches the registration disposer to the plugin ([chapter 2](./02-lifecycle-and-effects.md)), so unloading unregisters the tool. `defineTool` converts the `parameters` spec to the JSON Schema shown to the model, infers the type of `args`, and validates model-supplied arguments before `execute` runs. The tool returns the canonical value declared by `output.schema`; `output.render` separately produces the Native and durable result content.
|
|
52
|
+
|
|
53
|
+
## An observer plugin
|
|
54
|
+
|
|
55
|
+
Create `tool-logger.ts` — a separate plugin that watches every tool call in the app through the harness's `tools/result` event:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
59
|
+
import type {} from '@deepseek-ai/dsh-tools'
|
|
60
|
+
|
|
61
|
+
export const name = 'tool-logger'
|
|
62
|
+
export const inject = ['tools']
|
|
63
|
+
|
|
64
|
+
export function apply(ctx: Context) {
|
|
65
|
+
ctx.on('tools/result', (exec, result) => {
|
|
66
|
+
const text = result.content
|
|
67
|
+
.map(block => (block.type === 'text' ? block.text : ''))
|
|
68
|
+
.join('')
|
|
69
|
+
console.log(`[tool-logger] ${exec.name} -> ${text}`)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The `import type {} from '@deepseek-ai/dsh-tools'` line pulls in the package's declaration merges so `'tools/result'` and its payload are typed — the same move as chapter 4's `stats.ts` import, at package scale.
|
|
75
|
+
|
|
76
|
+
## Compose and run
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
- name: '@deepseek-ai/dsh-system-prompt'
|
|
80
|
+
- name: '@deepseek-ai/dsh-tools'
|
|
81
|
+
- name: './tool-logger.ts'
|
|
82
|
+
- name: './greet-tool.ts'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`@deepseek-ai/dsh-tools` injects the `systemPrompt` service because tools contribute schemas to the system prompt, so the composition lists its provider too. Without it, the tools plugin remains PENDING as described in [chapter 6](./06-composition-and-hmr.md).
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
node --import tsx ../../vendor/cordis/bin.js
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
[tool-logger] greet -> Hello, Cordis!
|
|
93
|
+
tool replied: [{"type":"text","text":"Hello, Cordis!"}]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The logger fired first: `tools/result` is emitted as part of result materialization, before `execute`'s promise resolves to the caller. Neither of your plugins knows the other exists — the registry service and the event connect them.
|
|
97
|
+
|
|
98
|
+
## From here to a full agent
|
|
99
|
+
|
|
100
|
+
A real agent is this composition plus more plugins: an LLM adapter, the agent loop, persistence, an entry point. Compare [examples/headless-agent/cordis.yml](https://github.com/deepseek-ai/deepseek-harness/blob/master/examples/headless-agent/cordis.yml) — you can read every entry in it now. Add your `greet-tool.ts` to a copy of that file.
|
|
101
|
+
|
|
102
|
+
Where to go next:
|
|
103
|
+
|
|
104
|
+
- [Build a tool](../basic/tool.md) — more of `defineTool`, including presentation and richer schemas.
|
|
105
|
+
- [Three-layer capability design](../practice/index.md) — how the harness structures replaceable capabilities.
|
|
106
|
+
- The generated `cordis-surface` regions on the [subsystem pages](../../reference/subsystems/core.md) — everything you can inject and listen to, each on its owning page.
|
|
107
|
+
- [Architecture](../../reference/index.md) — the system map these plugins live in.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/cordis-tutorial/index.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Cordis tutorial
|
|
6
|
+
|
|
7
|
+
Cordis is the plugin framework underneath DeepSeek Harness: a small runtime where every capability — tools, LLM adapters, file access, the agent loop itself — is a plugin mounted into a shared context. This tutorial teaches Cordis hands-on: each chapter is a runnable example you build in a scratch directory inside this repository, ending with a plugin wired into real harness services.
|
|
8
|
+
|
|
9
|
+
The audience is agent developers. You do not need deep TypeScript experience; the [TypeScript notes](#typescript-notes) below explain the syntax that may be unfamiliar, and every chapter shows the exact commands and expected output.
|
|
10
|
+
|
|
11
|
+
If you want the condensed concept reference instead of a walkthrough, read the [Cordis primer](../../reference/cordis-primer.md). The exhaustive API reference lives in the generated `cordis-surface` regions on the [subsystem pages](../../reference/subsystems/core.md) and the [Cordis core API](../../reference/cordis-api/context.md) pages.
|
|
12
|
+
|
|
13
|
+
To write plugins for the harness itself — loaded from a `cordis.yml` and driven from the Web UI rather than the launcher below — start from [your first Harness plugin](../basic/index.md).
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
You need a clone of this repository with dependencies installed; the [development guide](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/development.md#setup-tutorial) lists the prerequisites. No API key is needed for this tutorial; every example runs keylessly.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
git clone https://github.com/deepseek-ai/deepseek-harness.git
|
|
21
|
+
cd deepseek-harness
|
|
22
|
+
pnpm install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Create the scratch directory the chapters work in. `tmp/` is gitignored, so nothing you write there touches version control:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
mkdir -p tmp/cordis-tutorial
|
|
29
|
+
cd tmp/cordis-tutorial
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Every chapter runs the same command from this directory:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
node --import tsx ../../vendor/cordis/bin.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That one-file launcher (see [vendor/cordis/bin.js](https://github.com/deepseek-ai/deepseek-harness/blob/master/vendor/cordis/bin.js)) creates a root `Context`, mounts the Loader plugin, and tells it to load `./cordis.yml` from the current directory. Everything else — which plugins exist, how they are configured — comes from that YAML file, which you will write in a moment. The `--import tsx` flag lets Node run the TypeScript files the config points at without a build step.
|
|
39
|
+
|
|
40
|
+
## Chapters
|
|
41
|
+
|
|
42
|
+
1. [Your first plugin](./01-first-plugin.md) — a plugin is a function; the loader mounts it.
|
|
43
|
+
2. [Lifecycle and effects](./02-lifecycle-and-effects.md) — Cordis-managed registrations are undone when their plugin unloads.
|
|
44
|
+
3. [Services](./03-services.md) — expose a capability on `ctx` and depend on it with `inject`.
|
|
45
|
+
4. [Events](./04-events.md) — typed events, broadcast dispatch, and the waterfall short-circuit.
|
|
46
|
+
5. [Configuration](./05-config.md) — validated config from `cordis.yml`, failing loud on bad input.
|
|
47
|
+
6. [Composition and HMR](./06-composition-and-hmr.md) — the config file as a plugin tree, hot reload, and diagnosing a plugin that never loads.
|
|
48
|
+
7. [Into the harness](./07-into-the-harness.md) — register a model-callable tool against real harness services.
|
|
49
|
+
|
|
50
|
+
<a id="typescript-notes"></a>
|
|
51
|
+
|
|
52
|
+
## TypeScript notes
|
|
53
|
+
|
|
54
|
+
The examples use three TypeScript features beyond ordinary modern JavaScript:
|
|
55
|
+
|
|
56
|
+
- **Type annotations** describe values without changing runtime behavior: `ctx: Context` says that `ctx` has the Cordis context API, `who: string` accepts text, and `string[]` means an array of strings.
|
|
57
|
+
- **`import type { Context } from '@deepseek-ai/cordis'`** imports only type information. It vanishes at runtime, so a plugin file that needs `Context` solely for annotations adds no runtime dependency.
|
|
58
|
+
- **Declaration merging** (`declare module '@deepseek-ai/cordis' { ... }`) adds your entries to interfaces that Cordis already declares — for example the type of a new `ctx.greeter` property or event name. It generates no runtime wiring; the plugin separately provides the service or emits the event. Chapter 3 shows the pattern in full.
|
|
59
|
+
|
|
60
|
+
Chapter 5 also uses an `interface` to describe a configuration object's fields and a generic type such as `Schema<Config>` to say which object fields a schema validates. You can copy those declarations as shown; the surrounding text explains what each one connects.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/user/develop/framework/events.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Event system
|
|
6
|
+
|
|
7
|
+
Events are the core communication mechanism between Cordis plugins. Harness uses them extensively for loosely coupled extension points.
|
|
8
|
+
|
|
9
|
+
## Basic use
|
|
10
|
+
|
|
11
|
+
### Listen for an event
|
|
12
|
+
|
|
13
|
+
```ts ignore-check
|
|
14
|
+
ctx.on('event-name', (payload) => {
|
|
15
|
+
// Handle the event.
|
|
16
|
+
})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Emit an event
|
|
20
|
+
|
|
21
|
+
```ts ignore-check
|
|
22
|
+
ctx.emit('event-name', payload)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Event modes
|
|
26
|
+
|
|
27
|
+
Cordis provides several event modes for different interaction contracts.
|
|
28
|
+
|
|
29
|
+
### emit — broadcast
|
|
30
|
+
|
|
31
|
+
Every listener runs synchronously and return values are ignored:
|
|
32
|
+
|
|
33
|
+
```ts ignore-check
|
|
34
|
+
// Emit
|
|
35
|
+
ctx.emit('my-plugin/ready', { id: 'worker-1' })
|
|
36
|
+
|
|
37
|
+
// Listen
|
|
38
|
+
ctx.on('my-plugin/ready', ({ id }) => {
|
|
39
|
+
console.log(`${id} is ready`)
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### bail — short circuit
|
|
44
|
+
|
|
45
|
+
Listeners run in order; the first result other than `null`, `false`, or `undefined` becomes the final result:
|
|
46
|
+
|
|
47
|
+
```ts ignore-check
|
|
48
|
+
// Dispatch
|
|
49
|
+
const result = ctx.bail('some-check', input)
|
|
50
|
+
|
|
51
|
+
// Listen: a returned value stops later listeners.
|
|
52
|
+
ctx.on('some-check', (input) => {
|
|
53
|
+
if (shouldBlock(input)) return 'blocked'
|
|
54
|
+
// Return null, false, or undefined to continue to the next listener.
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### serial — ordered execution
|
|
59
|
+
|
|
60
|
+
Listeners run in registration order and asynchronous results are awaited. The first result other than `null`, `false`, or `undefined` stops further execution:
|
|
61
|
+
|
|
62
|
+
```ts ignore-check
|
|
63
|
+
await ctx.serial('setup-phase', context)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### waterfall — pipeline
|
|
67
|
+
|
|
68
|
+
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:
|
|
69
|
+
|
|
70
|
+
```ts ignore-check
|
|
71
|
+
// Dispatch
|
|
72
|
+
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
|
|
73
|
+
|
|
74
|
+
// Listen: next() is mandatory.
|
|
75
|
+
ctx.on('my-plugin/transform', async (_input, next) => {
|
|
76
|
+
const downstream = await next()
|
|
77
|
+
return downstream.trim()
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
::: warning
|
|
82
|
+
A waterfall listener **must call `next()`**. Omitting it short-circuits the pipeline by design, enabling interception and gateway behavior.
|
|
83
|
+
:::
|
|
84
|
+
|
|
85
|
+
## Typed events
|
|
86
|
+
|
|
87
|
+
Harness uses TypeScript declaration merging for type-safe events:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import '@deepseek-ai/cordis'
|
|
91
|
+
|
|
92
|
+
declare module '@deepseek-ai/cordis' {
|
|
93
|
+
interface Events {
|
|
94
|
+
'my-plugin/ready': (payload: { id: string }) => void
|
|
95
|
+
'my-plugin/check': (input: string) => boolean | undefined
|
|
96
|
+
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
|
|
101
|
+
// are now inferred correctly.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Cordis events and session records
|
|
105
|
+
|
|
106
|
+
Harness Cordis events use `namespace/action` names, including `agent/step`, `agent/request`, `agent/request-error`, `tools/result`, and `session/event`. The generated `cordis-surface` regions on the [subsystem pages](../../reference/subsystems/core.md) record complete signatures and modes.
|
|
107
|
+
|
|
108
|
+
`turn/*`, `step/*`, `tool/call`, `tool/result`, and `compaction/*` are durable session-event types, not same-named Cordis events. To observe them, listen to `session/event` and inspect `event.type`.
|
|
109
|
+
|
|
110
|
+
## Event listeners are effects
|
|
111
|
+
|
|
112
|
+
A listener registered with `ctx.on()` is removed automatically when its plugin unloads:
|
|
113
|
+
|
|
114
|
+
```ts ignore-check
|
|
115
|
+
export function apply(ctx: Context) {
|
|
116
|
+
// This listener is removed when the plugin disposes.
|
|
117
|
+
ctx.on('tools/result', handler)
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Example: logging plugin
|
|
122
|
+
|
|
123
|
+
This plugin logs tool calls and results:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
127
|
+
import '@deepseek-ai/dsh-tools'
|
|
128
|
+
|
|
129
|
+
export const name = 'tool-logger'
|
|
130
|
+
|
|
131
|
+
export function apply(ctx: Context) {
|
|
132
|
+
ctx.on('tools/result', (exec, result) => {
|
|
133
|
+
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
|
|
134
|
+
const text = result.content
|
|
135
|
+
.map(block => block.type === 'text' ? block.text : '')
|
|
136
|
+
.join('')
|
|
137
|
+
console.log(`[tool result] ${text.slice(0, 100)}`)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Next steps
|
|
143
|
+
|
|
144
|
+
- [Capability layering](../practice/index.md) — understand events within capability interfaces
|
|
145
|
+
- [LLM adapters](../practice/llm-adapter.md) — implement a complete LLM backend
|