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,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/user/develop/basic/index.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Your first plugin
|
|
6
|
+
|
|
7
|
+
This tutorial creates a minimal Harness plugin and loads it into the Web UI. Start from a repository checkout that has completed the [run-from-source path](https://github.com/deepseek-ai/deepseek-harness/blob/master/README.md#run-from-source).
|
|
8
|
+
|
|
9
|
+
## Create a local project
|
|
10
|
+
|
|
11
|
+
From the repository root, create a scratch project for the tutorial:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
mkdir -p scratch-plugin/src
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What is a plugin?
|
|
18
|
+
|
|
19
|
+
In Harness, a plugin is a TypeScript module that exports an `apply` function. The framework calls `apply` when loading the plugin and passes a `ctx` context object through which the plugin registers capabilities:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
23
|
+
|
|
24
|
+
export const name = 'my-plugin'
|
|
25
|
+
|
|
26
|
+
export function apply(ctx: Context) {
|
|
27
|
+
// Register capabilities here.
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That is the complete configuration.
|
|
32
|
+
|
|
33
|
+
## Create the plugin file
|
|
34
|
+
|
|
35
|
+
Create `scratch-plugin/src/my-plugin.ts`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
39
|
+
|
|
40
|
+
export const name = 'hello-plugin'
|
|
41
|
+
|
|
42
|
+
export function apply(ctx: Context) {
|
|
43
|
+
// Required dependencies are ready before apply runs.
|
|
44
|
+
console.log('[hello-plugin] plugin loaded!')
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Register it in cordis.yml
|
|
49
|
+
|
|
50
|
+
Run `pwd` from the repository root, then create `scratch-plugin/cordis.yml` as a Web overlay that inserts the local plugin. Replace `/absolute/path/to/deepseek-harness` below with the printed path:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
- insert:
|
|
54
|
+
- id: hello
|
|
55
|
+
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The plugin path must be absolute. A patch file contributes configuration but does not change the profile directory from which the loader resolves module paths.
|
|
59
|
+
|
|
60
|
+
Start the Web UI with that overlay:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
pnpm dsh web --patch ./scratch-plugin/cordis.yml
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Open `http://127.0.0.1:3080`. The terminal prints `[hello-plugin] plugin loaded!` during startup.
|
|
67
|
+
|
|
68
|
+
## Automatic cleanup
|
|
69
|
+
|
|
70
|
+
Anything registered through `ctx`—event listeners, tools, or timers—is cleaned up when the plugin unloads. You do not need to call removeListener or clearInterval manually.
|
|
71
|
+
|
|
72
|
+
For a resource that needs explicit cleanup, such as a network connection, use `ctx.effect()` to provide its disposer:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
76
|
+
|
|
77
|
+
export function apply(ctx: Context) {
|
|
78
|
+
ctx.effect(() => {
|
|
79
|
+
const timer = setInterval(() => {
|
|
80
|
+
console.log('heartbeat')
|
|
81
|
+
}, 5000)
|
|
82
|
+
|
|
83
|
+
// The returned function runs when the plugin unloads.
|
|
84
|
+
return () => clearInterval(timer)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Declare dependencies
|
|
90
|
+
|
|
91
|
+
If the plugin consumes another service such as `tools` or `llm`, declare it in `inject`:
|
|
92
|
+
|
|
93
|
+
```ts ignore-check
|
|
94
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
95
|
+
|
|
96
|
+
export const name = 'my-tool-plugin'
|
|
97
|
+
export const inject = ['tools']
|
|
98
|
+
|
|
99
|
+
export function apply(ctx: Context) {
|
|
100
|
+
// ctx.tools is ready here.
|
|
101
|
+
ctx.tools.register(/* ... */)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The framework waits for every required service before loading the plugin.
|
|
106
|
+
|
|
107
|
+
## Three plugin forms
|
|
108
|
+
|
|
109
|
+
In addition to a function module, a plugin can use object or class form.
|
|
110
|
+
|
|
111
|
+
### Object form
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
115
|
+
|
|
116
|
+
export default {
|
|
117
|
+
name: 'my-plugin',
|
|
118
|
+
inject: ['tools'],
|
|
119
|
+
apply(ctx: Context) {
|
|
120
|
+
// ...
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Class form
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
129
|
+
|
|
130
|
+
export default class MyService extends Service {
|
|
131
|
+
static inject = ['tools']
|
|
132
|
+
|
|
133
|
+
constructor(ctx: Context) {
|
|
134
|
+
super(ctx, 'myService')
|
|
135
|
+
// Perform synchronous initialization in the constructor.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Function form is sufficient in most cases. Use class form when the plugin provides a service to other plugins; see [services and dependencies](../framework/service.md).
|
|
141
|
+
|
|
142
|
+
## Next steps
|
|
143
|
+
|
|
144
|
+
- [Build a tool](./tool.md) — learn the tool definition DSL
|
|
145
|
+
- [Plugin configuration](./config.md) — accept user configuration
|
|
146
|
+
- [Cordis tutorial](../cordis-tutorial/index.md) — the plugin framework underneath, built from a scratch directory with no API key
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/user/develop/basic/publish.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Package and install a plugin
|
|
6
|
+
|
|
7
|
+
The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `dsh plugin add`, and explains the layer order that determines the composed configuration. It assumes the `dsh` CLI is installed. Complete [plugin configuration](./config.md) first.
|
|
8
|
+
|
|
9
|
+
To use a fresh source checkout instead, complete the [run-from-source section](https://github.com/deepseek-ai/deepseek-harness/blob/master/README.md#run-from-source), keep this tutorial's `hello-plugin` directory at the repository root, and run the remaining `dsh ...` commands from there as `pnpm dsh ...`. See [source execution](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.md#source-execution) for build and launcher behavior.
|
|
10
|
+
|
|
11
|
+
## Two concepts, two manifests
|
|
12
|
+
|
|
13
|
+
Installation is built on two concepts. Both are described by a `package.json`, but they carry different kinds of manifest under the `dsh` key, and they answer different questions:
|
|
14
|
+
|
|
15
|
+
- A **bundle** is an npm package that ships a configuration layer. Its manifest declares `dsh.bundle`, answering "what does this package contribute?": a patch file that inserts or overrides plugin rows.
|
|
16
|
+
- A **profile** is a directory under `$DSH_HOME/profiles/<name>` describing one runnable composition. Its manifest declares `dsh.profile`, answering "which bundles compose this setup, in what order?".
|
|
17
|
+
|
|
18
|
+
A bundle is what you author and distribute; a profile is what a user boots with `dsh --profile <name>`. Nothing is both.
|
|
19
|
+
|
|
20
|
+
### The bundle manifest
|
|
21
|
+
|
|
22
|
+
Create the package directory:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
mkdir -p hello-plugin
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
hello-plugin/
|
|
30
|
+
├── package.json # declares dsh.bundle
|
|
31
|
+
├── cordis.patch.yml # the layer applied when a profile lists this bundle
|
|
32
|
+
└── index.js # plugin modules the patch rows reference
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Create `hello-plugin/package.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"name": "dsh-hello-plugin",
|
|
40
|
+
"version": "0.1.0",
|
|
41
|
+
"type": "module",
|
|
42
|
+
"main": "index.js",
|
|
43
|
+
"files": ["index.js", "cordis.patch.yml"],
|
|
44
|
+
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Create `hello-plugin/index.js` with the plugin entry point:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
export const name = 'hello-plugin'
|
|
52
|
+
|
|
53
|
+
export function apply() {
|
|
54
|
+
console.log('[hello-plugin] plugin loaded!')
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Create `hello-plugin/cordis.patch.yml`. The patch is a YAML array like the `--patch` overlays you have been writing, except plugin rows reference the package by name instead of a relative source path so Node resolution finds the installed code:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
- insert:
|
|
62
|
+
- id: hello
|
|
63
|
+
name: dsh-hello-plugin
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A package without the `dsh.bundle` declaration still installs, but only as a plain dependency: `dsh plugin` prints a warning and activates no layer. Use that package format for a library that plugin packages import rather than a plugin users enable.
|
|
67
|
+
|
|
68
|
+
### The profile manifest
|
|
69
|
+
|
|
70
|
+
A profile directory holds two files:
|
|
71
|
+
|
|
72
|
+
- `package.json` — the profile's out-of-tree plugin dependencies (managed by pnpm) plus the `dsh.profile` manifest with its ordered `bundles` list.
|
|
73
|
+
- `cordis.patch.yml` — the user's own patch layer, applied after every bundle layer.
|
|
74
|
+
|
|
75
|
+
You never write a profile manifest by hand: `dsh plugin` creates and maintains it. The next section shows the result.
|
|
76
|
+
|
|
77
|
+
## Install into a profile
|
|
78
|
+
|
|
79
|
+
`dsh plugin --profile <name> <args...>` forwards to pnpm in the profile directory, so every pnpm verb works. From the directory that contains `hello-plugin`, install the package checkout:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
dsh plugin --profile demo add ./hello-plugin
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The first use initializes the profile (with `@deepseek-ai/dsh-base` as its first bundle), pnpm links the checkout, and `dsh` appends the bundle to `dsh.profile.bundles` because the package declares `dsh.bundle`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"name": "dsh-profile-demo",
|
|
90
|
+
"private": true,
|
|
91
|
+
"dependencies": {
|
|
92
|
+
"dsh-hello-plugin": "link:/path/to/hello-plugin"
|
|
93
|
+
},
|
|
94
|
+
"dsh": {
|
|
95
|
+
"profile": {
|
|
96
|
+
"bundles": [
|
|
97
|
+
"@deepseek-ai/dsh-base",
|
|
98
|
+
"dsh-hello-plugin"
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Verify the layer without booting, then boot:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer
|
|
109
|
+
dsh --profile demo
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`dsh plugin --profile demo remove dsh-hello-plugin` removes both the dependency and the layer.
|
|
113
|
+
|
|
114
|
+
## The loading order
|
|
115
|
+
|
|
116
|
+
The effective configuration composes over an empty root by applying, in order:
|
|
117
|
+
|
|
118
|
+
1. Each bundle patch named in the profile's `dsh.profile.bundles` list, in list order — `@deepseek-ai/dsh-base` first, then each installed bundle in the order it was added.
|
|
119
|
+
2. The profile's own `cordis.patch.yml`.
|
|
120
|
+
3. The home-level `$DSH_HOME/cordis.patch.yml` — machine-local preferences shared by every profile.
|
|
121
|
+
4. Each `--patch <path>` overlay, in argv order.
|
|
122
|
+
|
|
123
|
+
App arguments are not another patch layer. A surface bundle can resolve them through an ordinary app-owned service, described below.
|
|
124
|
+
|
|
125
|
+
Later layers win per row, and a patch replaces a row's entire `config` value rather than deep-merging keys. Two consequences for bundle authors:
|
|
126
|
+
|
|
127
|
+
- Your patch can override rows from earlier layers by `id` — the same way [the `dsh-web-app` bundle](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/bundle/web-app/cordis.patch.yml) overrides `dsh-base` rows — but must restate every key the row needs, not just the changed one.
|
|
128
|
+
- Users can override your rows in their profile's `cordis.patch.yml` without touching your package, so prefer configuration defaults users are likely to keep and let the schema carry the rest.
|
|
129
|
+
|
|
130
|
+
In-box bundle names always resolve from the dsh installation itself; pnpm manages only out-of-tree packages, so your bundle can rely on `@deepseek-ai/dsh-base` being present and current.
|
|
131
|
+
|
|
132
|
+
## Give a surface bundle its own command line
|
|
133
|
+
|
|
134
|
+
A bundle that defines a runnable app mounts an ordinary provider plugin:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
- id: hello-startup
|
|
138
|
+
name: 'dsh-hello-plugin/startup'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The plugin exports `inject = ['cmdlineArgs']`, calls `parseCmdline` from [`@deepseek-ai/dsh-cmdline`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/boot/cmdline/README.md) with its own commander program, and provides its app-owned service from the program's action. The launcher hands every plugin the same immutable arguments after launcher flags, so app-specific flags need no launcher change and multiple plugins may parse the snapshot. The Loader row needs no launcher marker or special kind.
|
|
142
|
+
|
|
143
|
+
Rows configured by those arguments inject the provider's service and read it from their own `!!js` options, with the deployment value beside it as the fallback:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
- id: my-app
|
|
147
|
+
name: '@example/my-app'
|
|
148
|
+
inject: [myAppStartup]
|
|
149
|
+
config:
|
|
150
|
+
port: !!js ctx.myAppStartup.port ?? 8080
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
On `--help`, the provider publishes no service, so those rows never activate. Loader mounts the composition once, waits for each row's ordinary injections, and only then evaluates that row's `!!js` config against its injected context.
|
|
154
|
+
|
|
155
|
+
## Installing from GitHub: the build-script catch
|
|
156
|
+
|
|
157
|
+
Publishing to a registry is not required — users can install straight from a git host:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
dsh plugin --profile demo add github:you/hello-plugin
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
But a git install fetches **sources, not built artifacts**: nothing runs your `build` script, so a TypeScript package arrives without its `lib/` output and fails to load. Two things must happen, one on each side:
|
|
164
|
+
|
|
165
|
+
- **The author** ships a `prepare` script — pnpm runs it after a git install — that builds the published entry points from source, self-contained: it must not assume dev-only context such as a sibling monorepo checkout. [turtle-ui](https://github.com/deepseek-harness/turtle-ui) is a working example: its `prepare` runs a dedicated tsdown config that transpiles `src/` without project references or type checking.
|
|
166
|
+
- **The user** allowlists the build. pnpm ≥10 refuses to run a git dependency's `prepare` script until it is explicitly allowed, so the first `add` fails; `dsh` points at the fix — copy the exact package key pnpm printed into the profile's `pnpm-workspace.yaml`:
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
allowBuilds:
|
|
170
|
+
dsh-hello-plugin: true
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
and re-run the `add`.
|
|
174
|
+
|
|
175
|
+
Treat that allowance as what it is: **permission to execute the package's code on your machine at install time**, outside any sandbox the agent runs under. Only allow packages whose source you trust, and pin a commit (`github:you/hello-plugin#<sha>`) so a later push cannot silently change what runs.
|
|
176
|
+
|
|
177
|
+
If you would rather not ask users for the allowance, distribute built artifacts instead — neither form needs any build permission:
|
|
178
|
+
|
|
179
|
+
- **Publish to npm** with `lib/` built at `pnpm publish` time; `dsh plugin add your-package` then installs prebuilt code.
|
|
180
|
+
- **Ship a tarball** from `pnpm pack`; users run `dsh plugin add ./hello-plugin-0.1.0.tgz`.
|
|
181
|
+
|
|
182
|
+
## Next steps
|
|
183
|
+
|
|
184
|
+
- [Plugins and lifecycle](../framework/index.md) — the full plugin lifecycle
|
|
185
|
+
- [CLI behavior reference](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.md) — exact layer precedence, flags, and profile mechanics
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/user/develop/basic/tool.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Build a tool
|
|
6
|
+
|
|
7
|
+
This tutorial adds a `greet` tool to the Web UI. Complete [Your first plugin](./index.md) first and keep its `scratch-plugin` directory.
|
|
8
|
+
|
|
9
|
+
## Create the tool plugin
|
|
10
|
+
|
|
11
|
+
Replace `scratch-plugin/src/my-plugin.ts` with:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
15
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
16
|
+
|
|
17
|
+
export const name = 'greet-tool'
|
|
18
|
+
export const inject = ['tools']
|
|
19
|
+
|
|
20
|
+
export function apply(ctx: Context) {
|
|
21
|
+
ctx.tools.register(defineTool({
|
|
22
|
+
name: 'greet',
|
|
23
|
+
description: 'Greet someone by name.',
|
|
24
|
+
parameters: {
|
|
25
|
+
name: { type: 'string', required: true, description: 'The name to greet' },
|
|
26
|
+
},
|
|
27
|
+
output: {
|
|
28
|
+
schema: { type: 'string' },
|
|
29
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
30
|
+
},
|
|
31
|
+
async execute(args) {
|
|
32
|
+
return `Hello, ${args.name}!`
|
|
33
|
+
},
|
|
34
|
+
}))
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`inject` makes Cordis wait for the tool registry. `defineTool` infers and validates `args` from `parameters`; `execute` returns the canonical value declared by `output.schema`, and `output.render` converts that value to model-facing content.
|
|
39
|
+
|
|
40
|
+
## Run and call the tool
|
|
41
|
+
|
|
42
|
+
Restart the development command if it is not running:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pnpm dsh web --patch ./scratch-plugin/cordis.yml
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Open `http://127.0.0.1:3080` and ask: `Use the greet tool to greet Ada.` The model can call `greet` and receives `Hello, Ada!` as the tool result.
|
|
49
|
+
|
|
50
|
+
## Next steps
|
|
51
|
+
|
|
52
|
+
- [Plugin configuration](./config.md) — make the greeting configurable.
|
|
53
|
+
- [Tool authoring reference](../../reference/cookbook/adding-a-tool.md) — look up nested schemas, canonical values, background work, policy hooks, Code Mode, and UI cards.
|
|
54
|
+
- [Capability layering](../practice/index.md) — split a replaceable capability into Service Definition, Service Provider, and Consumer packages.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/cordis-tutorial/01-first-plugin.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 1. Your first plugin
|
|
6
|
+
|
|
7
|
+
In the loader configuration used here, a Cordis plugin module named-exports an `apply` function. When Cordis loads it, it calls `apply` with a **context** — the `ctx` object through which the plugin registers everything it contributes.
|
|
8
|
+
|
|
9
|
+
## Write the plugin
|
|
10
|
+
|
|
11
|
+
In your `tmp/cordis-tutorial` directory (see [setup](./index.md#setup)), create `hello.ts`:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
15
|
+
|
|
16
|
+
export const name = 'hello'
|
|
17
|
+
|
|
18
|
+
export function apply(ctx: Context) {
|
|
19
|
+
console.log('hello from my first plugin')
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The `name` export is optional display metadata; it labels the plugin in diagnostics.
|
|
24
|
+
|
|
25
|
+
## Compose the app
|
|
26
|
+
|
|
27
|
+
This tutorial's launcher assembles the application from configuration. Create `cordis.yml`:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
- name: './hello.ts'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The file is a list of plugin entries. `name` is a module specifier — a relative path or an npm package name — and the loader mounts every entry. Entries start concurrently, so list position guarantees nothing about which plugin loads first; ordering comes from service dependencies (`inject`, [chapter 3](./03-services.md)), not from position in the file.
|
|
34
|
+
|
|
35
|
+
## Run it
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
node --import tsx ../../vendor/cordis/bin.js
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Expected output:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
hello from my first plugin
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The process exits on its own once nothing is left running. What happened:
|
|
48
|
+
|
|
49
|
+
1. The launcher created a root `Context` and mounted the **Loader** plugin.
|
|
50
|
+
2. The Loader read `cordis.yml`, resolved `./hello.ts`, and mounted it as a child plugin.
|
|
51
|
+
3. Cordis called your `apply(ctx)`.
|
|
52
|
+
|
|
53
|
+
There is no framework bootstrap code in your file: a plugin describes what it contributes, and `cordis.yml` composes the application. The [`dsh` base](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/bundle/base/cordis.patch.yml), for example, is a longer plugin composition that deployment overlays patch.
|
|
54
|
+
|
|
55
|
+
## The two other plugin shapes
|
|
56
|
+
|
|
57
|
+
A function is the most common form, but Cordis accepts three:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
61
|
+
|
|
62
|
+
// 1. Function plugin (what you just wrote).
|
|
63
|
+
export function apply(ctx: Context) {}
|
|
64
|
+
|
|
65
|
+
// 2. Object plugin: an object with an `apply` method.
|
|
66
|
+
export const objectPlugin = {
|
|
67
|
+
name: 'object-plugin',
|
|
68
|
+
apply(ctx: Context) {},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Class plugin: a Service subclass (covered in chapter 3).
|
|
72
|
+
export class MyService extends Service {
|
|
73
|
+
constructor(ctx: Context) {
|
|
74
|
+
super(ctx, 'myTutorialService')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use the function form until you need to expose a service; [chapter 3](./03-services.md) covers when the class form earns its place.
|
|
80
|
+
|
|
81
|
+
## Try breaking it
|
|
82
|
+
|
|
83
|
+
Make `apply` throw:
|
|
84
|
+
|
|
85
|
+
```ts ignore-check
|
|
86
|
+
export function apply(ctx: Context) {
|
|
87
|
+
throw new Error('apply exploded')
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Run again: the process dies with your error. A plugin that fails to load is a loud failure, not a skipped entry.
|
|
92
|
+
|
|
93
|
+
One caveat worth knowing early: a config entry whose module cannot be **resolved** — a typo'd path or package name — is reported through the Cordis logger service instead of crashing the process, and at boot that report can be lost before a console exporter is watching. If a freshly added entry seems to do nothing, check the spelling first.
|
|
94
|
+
|
|
95
|
+
Next: [Lifecycle and effects](./02-lifecycle-and-effects.md) — what happens when a plugin unloads.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/cordis-tutorial/02-lifecycle-and-effects.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 2. Lifecycle and effects
|
|
6
|
+
|
|
7
|
+
A Cordis plugin can be unloaded by a config edit, hot reload, explicit disposal, or loss of a required service. Registrations made through Cordis APIs are effects and are undone when their owning plugin unloads; resources managed outside those APIs must be wrapped in `ctx.effect()`.
|
|
8
|
+
|
|
9
|
+
## Effects
|
|
10
|
+
|
|
11
|
+
For a resource Cordis does not already manage — a timer, a connection, a watcher — wrap it in `ctx.effect()` and return a disposer:
|
|
12
|
+
|
|
13
|
+
Create `lifecycle.ts` in `tmp/cordis-tutorial`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
+
|
|
18
|
+
export const name = 'lifecycle-demo'
|
|
19
|
+
|
|
20
|
+
function heartbeat(ctx: Context) {
|
|
21
|
+
console.log('heartbeat plugin loading')
|
|
22
|
+
ctx.effect(() => {
|
|
23
|
+
const timer = setInterval(() => console.log('tick'), 200)
|
|
24
|
+
return () => {
|
|
25
|
+
clearInterval(timer)
|
|
26
|
+
console.log('heartbeat cleaned up')
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function apply(ctx: Context) {
|
|
32
|
+
// Mount a child plugin and keep its fiber to dispose it later.
|
|
33
|
+
const fiber = ctx.plugin(heartbeat)
|
|
34
|
+
// The demo timer is itself an effect: if THIS plugin is unloaded first,
|
|
35
|
+
// the pending callback is cancelled instead of firing on a dead app.
|
|
36
|
+
ctx.effect(() => {
|
|
37
|
+
const timer = setTimeout(async () => {
|
|
38
|
+
await fiber.dispose()
|
|
39
|
+
console.log('disposed')
|
|
40
|
+
process.exit(0)
|
|
41
|
+
}, 700)
|
|
42
|
+
return () => clearTimeout(timer)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Point `cordis.yml` at it:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
- name: './lifecycle.ts'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run (`node --import tsx ../../vendor/cordis/bin.js`) and you get:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
heartbeat plugin loading
|
|
57
|
+
tick
|
|
58
|
+
tick
|
|
59
|
+
tick
|
|
60
|
+
heartbeat cleaned up
|
|
61
|
+
disposed
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Three things to notice:
|
|
65
|
+
|
|
66
|
+
- `ctx.plugin(heartbeat)` mounts a function **from code** as a plugin — the same operation the YAML loader performs for each config entry. A function plugin needs no `apply` method: Cordis calls the function directly and uses its name only for diagnostics. An `apply` method is required only for the object form, `ctx.plugin({ apply(ctx) { /* ... */ } })`. The call returns a **fiber**, the runtime handle for one loaded plugin instance.
|
|
67
|
+
- The effect body runs during load; the disposer it returns runs during unload. You never call the disposer yourself for a plugin-lifetime resource.
|
|
68
|
+
- `fiber.dispose()` resolves after all of the plugin's cleanup — including async disposers — has finished, and recursively unloads any child plugins it mounted.
|
|
69
|
+
|
|
70
|
+
## The fiber state machine
|
|
71
|
+
|
|
72
|
+
Every loaded plugin instance owns a fiber that moves through these states:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
|
|
76
|
+
↘ FAILED
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **PENDING** — declared, but a required service (chapter 3) is not available yet.
|
|
80
|
+
- **LOADING / ACTIVE** — `apply` is running / has completed.
|
|
81
|
+
- **FAILED** — `apply` or config validation threw.
|
|
82
|
+
- **UNLOADING / DISPOSED** — disposers are running / everything is torn down.
|
|
83
|
+
|
|
84
|
+
You will meet PENDING again in [chapter 6](./06-composition-and-hmr.md), where it is the usual answer to "why does my plugin print nothing?".
|
|
85
|
+
|
|
86
|
+
## What is already an effect
|
|
87
|
+
|
|
88
|
+
You rarely write `ctx.effect()` yourself, because the built-in registration APIs are effects already:
|
|
89
|
+
|
|
90
|
+
- `ctx.on(event, listener)` — the listener is removed on unload ([chapter 4](./04-events.md)).
|
|
91
|
+
- `ctx.plugin(child)` — the child is disposed with its parent.
|
|
92
|
+
- Service registrations are effects. Harness registries such as `ctx.tools.register(...)` also attach their returned disposers to the calling plugin, so they unwind automatically ([chapter 7](./07-into-the-harness.md)).
|
|
93
|
+
|
|
94
|
+
For a resource Cordis does not manage, acquire it inside `ctx.effect()` and return a disposer that releases it. Cordis then invokes that release during unloading, including hot reload.
|
|
95
|
+
|
|
96
|
+
One ordering caveat: disposers start in reverse registration order, but multiple **async** disposers run concurrently. If teardown steps must run in sequence, keep them in one disposer and await them there.
|
|
97
|
+
|
|
98
|
+
Next: [Services](./03-services.md) — how plugins share capabilities.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/cordis-tutorial/03-services.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 3. Services
|
|
6
|
+
|
|
7
|
+
A **service** is a named capability one plugin provides and other plugins consume through `ctx`. In the harness, `ctx.tools`, `ctx.llm`, and `ctx.agents` are services. A consumer names the capability, such as `'tools'`, rather than importing its provider, so configuration can select a provider without changing the consumer.
|
|
8
|
+
|
|
9
|
+
## Provide a service
|
|
10
|
+
|
|
11
|
+
Create `greeter.ts` in `tmp/cordis-tutorial`:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
15
|
+
|
|
16
|
+
declare module '@deepseek-ai/cordis' {
|
|
17
|
+
interface Context {
|
|
18
|
+
greeter: GreeterService
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class GreeterService extends Service {
|
|
23
|
+
constructor(ctx: Context) {
|
|
24
|
+
super(ctx, 'greeter')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
greet(who: string) {
|
|
28
|
+
return `Hello, ${who}!`
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const name = 'greeter'
|
|
33
|
+
|
|
34
|
+
export function apply(ctx: Context) {
|
|
35
|
+
ctx.plugin(GreeterService)
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Two pieces work together:
|
|
40
|
+
|
|
41
|
+
- **Runtime**: `super(ctx, 'greeter')` registers the instance under the name `greeter`. From then on, any plugin can reach it as `ctx.greeter`. The registration is an effect — unloading the provider removes the service.
|
|
42
|
+
- **Compile time**: the `declare module '@deepseek-ai/cordis'` block is TypeScript declaration merging. It adds `greeter` to the `Context` interface so `ctx.greeter` typechecks everywhere. It generates no code; without it the service still works at runtime, but consumers lose type safety.
|
|
43
|
+
|
|
44
|
+
A `Service` subclass is itself a plugin (the class form from chapter 1), so `ctx.plugin(GreeterService)` mounts it like any other.
|
|
45
|
+
|
|
46
|
+
## Consume a service with `inject`
|
|
47
|
+
|
|
48
|
+
Create `consumer.ts`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
52
|
+
|
|
53
|
+
export const name = 'consumer'
|
|
54
|
+
export const inject = ['greeter']
|
|
55
|
+
|
|
56
|
+
export function apply(ctx: Context) {
|
|
57
|
+
console.log(ctx.greeter.greet('world'))
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`inject` lists the services this plugin requires. Cordis holds the plugin in PENDING until every listed service exists, so inside `apply`, `ctx.greeter` is guaranteed ready. Load order in `cordis.yml` does not matter — dependencies, not file order, decide when plugins start.
|
|
62
|
+
|
|
63
|
+
Compose and run:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
- name: './greeter.ts'
|
|
67
|
+
- name: './consumer.ts'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
Hello, world!
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Swap the two lines in `cordis.yml` and rerun: same output. Try removing `./greeter.ts` entirely: the consumer stays PENDING and prints nothing — no crash, no partial run. A PENDING fiber does not keep Node's event loop alive either, so a composition with nothing else running exits 0 silently. [Chapter 6](./06-composition-and-hmr.md) shows how to diagnose that state.
|
|
75
|
+
|
|
76
|
+
## Dependencies are tracked after load
|
|
77
|
+
|
|
78
|
+
`inject` is not a one-shot boot check. If a required service disappears while the app runs — its provider was unloaded or hot-replaced — every dependent plugin is unloaded too, and loads again when the service returns. Combined with effects ([chapter 2](./02-lifecycle-and-effects.md)), this prevents a running consumer from retaining a reference to an unavailable service: its own registrations are unwound when the dependency disappears.
|
|
79
|
+
|
|
80
|
+
This is also why service replacement works in config: unload the `dsh-bash-local` entry, mount a different `shell` provider, and every plugin injecting `'shell'` cleanly restarts against the new implementation.
|
|
81
|
+
|
|
82
|
+
## Optional dependencies
|
|
83
|
+
|
|
84
|
+
`inject` is for hard requirements. For a capability the plugin can live without, skip `inject` and probe at the use site:
|
|
85
|
+
|
|
86
|
+
```ts ignore-check
|
|
87
|
+
export function apply(ctx: Context) {
|
|
88
|
+
// undefined when no provider is loaded; the plugin still runs.
|
|
89
|
+
const greeter = ctx.get('greeter')
|
|
90
|
+
console.log(greeter?.greet('maybe') ?? 'no greeter available')
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Naming
|
|
95
|
+
|
|
96
|
+
Service names live in one flat namespace per application. Prefix or namespace your own services distinctively (the harness claims plain names like `tools` and `llm`); the generated `cordis-surface` regions on the [subsystem pages](../../reference/subsystems/core.md) list every name the harness registers.
|
|
97
|
+
|
|
98
|
+
Next: [Events](./04-events.md) — communication without a shared service.
|