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,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/user/develop/framework/index.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Plugins and lifecycle
|
|
6
|
+
|
|
7
|
+
This page describes the Cordis plugin model and lifecycle state machine.
|
|
8
|
+
|
|
9
|
+
## Fiber state machine
|
|
10
|
+
|
|
11
|
+
Every loaded plugin owns a **Fiber** scope with the following states:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
PENDING → LOADING → ACTIVE
|
|
15
|
+
↘ FAILED
|
|
16
|
+
ACTIVE → UNLOADING → DISPOSED
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| State | Meaning |
|
|
20
|
+
|------|------|
|
|
21
|
+
| PENDING | Declared, but required dependencies are not ready |
|
|
22
|
+
| LOADING | Dependencies are ready and `apply` is running |
|
|
23
|
+
| ACTIVE | The plugin is running |
|
|
24
|
+
| FAILED | `apply` threw an error |
|
|
25
|
+
| UNLOADING | The plugin is unloading and disposing resources |
|
|
26
|
+
| DISPOSED | The plugin is fully unloaded |
|
|
27
|
+
|
|
28
|
+
## Dependency-driven loading
|
|
29
|
+
|
|
30
|
+
A plugin with `inject` waits for every required service before loading:
|
|
31
|
+
|
|
32
|
+
```ts ignore-check
|
|
33
|
+
export const inject = ['tools', 'llm']
|
|
34
|
+
|
|
35
|
+
export function apply(ctx: Context) {
|
|
36
|
+
// ctx.tools and ctx.llm are ready here.
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If a required service disappears, for example during provider replacement, the plugin unloads automatically (ACTIVE → DISPOSED) and loads again when the service returns.
|
|
41
|
+
|
|
42
|
+
## Automatic cleanup
|
|
43
|
+
|
|
44
|
+
Every registration made through `ctx` is undone when the plugin unloads:
|
|
45
|
+
|
|
46
|
+
```ts ignore-check
|
|
47
|
+
export function apply(ctx: Context) {
|
|
48
|
+
// Event listener: removed automatically on unload.
|
|
49
|
+
ctx.on('some-event', handler)
|
|
50
|
+
|
|
51
|
+
// Custom resource: the returned disposer runs on unload.
|
|
52
|
+
ctx.effect(() => {
|
|
53
|
+
const connection = createConnection()
|
|
54
|
+
return () => connection.close()
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The framework tracks and disposes all of these operations:
|
|
60
|
+
- `ctx.on(event, handler)` — event listener
|
|
61
|
+
- `ctx.tools.register(tool)` — tool registration
|
|
62
|
+
- `ctx.llm.registerAdapter(names, adapter)` — LLM adapter registration
|
|
63
|
+
- `ctx.effect(() => cleanup)` — custom resource
|
|
64
|
+
|
|
65
|
+
During unload, disposer invocation starts in reverse registration order, but multiple async disposers run concurrently and have no serial completion guarantee. Put order-dependent cleanup in one disposer returned from a single `ctx.effect()` and await its steps serially there.
|
|
66
|
+
|
|
67
|
+
## Nested contexts
|
|
68
|
+
|
|
69
|
+
`ctx.plugin()` creates a child Fiber that inherits the parent context but has an independent lifecycle:
|
|
70
|
+
|
|
71
|
+
```ts ignore-check
|
|
72
|
+
export function apply(ctx: Context) {
|
|
73
|
+
// Register a child plugin.
|
|
74
|
+
ctx.plugin(childPlugin)
|
|
75
|
+
|
|
76
|
+
// The child has its own Fiber and unloads with its parent.
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Dispose semantics
|
|
81
|
+
|
|
82
|
+
To stop a plugin instance early:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
86
|
+
|
|
87
|
+
declare const ctx: Context
|
|
88
|
+
declare function myPlugin(ctx: Context): void
|
|
89
|
+
|
|
90
|
+
const fiber = ctx.plugin(myPlugin)
|
|
91
|
+
|
|
92
|
+
// Dispose it manually later.
|
|
93
|
+
await fiber.dispose()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`dispose` guarantees:
|
|
97
|
+
1. All registrations owned by the plugin are removed.
|
|
98
|
+
2. Child plugins are recursively unloaded.
|
|
99
|
+
3. The returned promise resolves after all asynchronous cleanup finishes.
|
|
100
|
+
|
|
101
|
+
## Hot replacement (HMR)
|
|
102
|
+
|
|
103
|
+
With `@deepseek-ai/cordis-plugin-hmr` loaded from `cordis.yml`, editing a plugin source file triggers:
|
|
104
|
+
|
|
105
|
+
1. Unload the old plugin and clean up its registrations.
|
|
106
|
+
2. Load the new code.
|
|
107
|
+
3. Run the new `apply`.
|
|
108
|
+
|
|
109
|
+
Because plugin registrations clean themselves up, hot replacement does not retain registrations from the old instance.
|
|
110
|
+
|
|
111
|
+
## Example lifecycle
|
|
112
|
+
|
|
113
|
+
```ts ignore-check
|
|
114
|
+
export function apply(ctx: Context) {
|
|
115
|
+
console.log('plugin loading')
|
|
116
|
+
|
|
117
|
+
ctx.effect(() => {
|
|
118
|
+
console.log('effect registered')
|
|
119
|
+
return () => console.log('effect cleaned up')
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Loading prints:
|
|
125
|
+
```
|
|
126
|
+
plugin loading
|
|
127
|
+
effect registered
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Unloading prints:
|
|
131
|
+
```
|
|
132
|
+
effect cleaned up
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Next steps
|
|
136
|
+
|
|
137
|
+
- [Services and dependencies](./service.md) — expose a capability to other plugins
|
|
138
|
+
- [Event system](./events.md) — communicate between plugins
|
|
139
|
+
- [Cordis tutorial](../cordis-tutorial/index.md) — the same lifecycle, services, and events built step by step against the Cordis runtime
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/user/develop/framework/service.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Services and dependencies
|
|
6
|
+
|
|
7
|
+
A service is a capability one plugin exposes to other plugins. `inject` declares the services a plugin requires.
|
|
8
|
+
|
|
9
|
+
## What is a service?
|
|
10
|
+
|
|
11
|
+
In Harness, `tools`, `llm`, and `agents` are services. Each is a named capability mounted on `ctx`:
|
|
12
|
+
|
|
13
|
+
```ts ignore-check
|
|
14
|
+
ctx.tools // ToolRuntime service
|
|
15
|
+
ctx.llm // LLM service
|
|
16
|
+
ctx.agents // Agent service
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Any plugin can provide a service for other plugins to consume.
|
|
20
|
+
|
|
21
|
+
## Consume a service
|
|
22
|
+
|
|
23
|
+
Declare `inject` to use an existing service:
|
|
24
|
+
|
|
25
|
+
```ts ignore-check
|
|
26
|
+
export const inject = ['tools']
|
|
27
|
+
|
|
28
|
+
export function apply(ctx: Context) {
|
|
29
|
+
// ctx.tools exists and is ready here.
|
|
30
|
+
ctx.tools.register(/* ... */)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
When `apply` runs, every service declared by `inject` is ready. If a service is not ready, the plugin waits instead of running.
|
|
35
|
+
|
|
36
|
+
## Provide a service
|
|
37
|
+
|
|
38
|
+
### Extend Service
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
42
|
+
|
|
43
|
+
export default class MetricsService extends Service {
|
|
44
|
+
static inject = ['llm'] // A service may depend on other services.
|
|
45
|
+
|
|
46
|
+
constructor(ctx: Context) {
|
|
47
|
+
super(ctx, 'metrics') // 'metrics' is the service name.
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Public service method.
|
|
51
|
+
record(event: string, value: number) {
|
|
52
|
+
// ...
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
After loading this plugin, consumers access the service as `ctx.metrics`:
|
|
58
|
+
|
|
59
|
+
```ts ignore-check
|
|
60
|
+
export const inject = ['metrics']
|
|
61
|
+
|
|
62
|
+
export function apply(ctx: Context) {
|
|
63
|
+
ctx.metrics.record('tool_call', 1)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Declare its type
|
|
68
|
+
|
|
69
|
+
Use TypeScript declaration merging to type `ctx.metrics`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
73
|
+
|
|
74
|
+
declare module '@deepseek-ai/cordis' {
|
|
75
|
+
interface Context {
|
|
76
|
+
metrics: MetricsService
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default class MetricsService extends Service {
|
|
81
|
+
constructor(ctx: Context) {
|
|
82
|
+
super(ctx, 'metrics')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
record(event: string, value: number) { /* ... */ }
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Dependency behavior
|
|
90
|
+
|
|
91
|
+
### Required and optional dependencies
|
|
92
|
+
|
|
93
|
+
```ts ignore-check
|
|
94
|
+
// Required: the plugin does not load while the service is absent.
|
|
95
|
+
export const inject = ['tools']
|
|
96
|
+
|
|
97
|
+
// Optional: omit inject and query with ctx.get() at the use site.
|
|
98
|
+
export function apply(ctx: Context) {
|
|
99
|
+
const metrics = ctx.get('metrics')
|
|
100
|
+
metrics?.record('plugin_loaded', 1)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### When a service disappears
|
|
105
|
+
|
|
106
|
+
If a required service disappears while the application is running, for example because its provider unloads:
|
|
107
|
+
|
|
108
|
+
1. Dependent plugins dispose automatically.
|
|
109
|
+
2. They load again when the service returns.
|
|
110
|
+
|
|
111
|
+
This prevents a plugin from calling a service that no longer exists.
|
|
112
|
+
|
|
113
|
+
## Service isolation
|
|
114
|
+
|
|
115
|
+
`cordis.yml` can isolate services so separate plugin groups see separate instances of the same service:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
- id: group-a
|
|
119
|
+
name: '@deepseek-ai/cordis-plugin-group'
|
|
120
|
+
group: true
|
|
121
|
+
isolate:
|
|
122
|
+
shell: true
|
|
123
|
+
config:
|
|
124
|
+
- name: '@deepseek-ai/dsh-bash-local'
|
|
125
|
+
config:
|
|
126
|
+
timeoutMs: 5000
|
|
127
|
+
- name: './src/plugin-a.ts'
|
|
128
|
+
|
|
129
|
+
- id: group-b
|
|
130
|
+
name: '@deepseek-ai/cordis-plugin-group'
|
|
131
|
+
group: true
|
|
132
|
+
isolate:
|
|
133
|
+
shell: true
|
|
134
|
+
config:
|
|
135
|
+
- name: '@deepseek-ai/dsh-bash-local'
|
|
136
|
+
config:
|
|
137
|
+
timeoutMs: 60000
|
|
138
|
+
- name: './src/plugin-b.ts'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`plugin-a` and `plugin-b` each see the Bash instance in their own group, with no cross-group effect.
|
|
142
|
+
|
|
143
|
+
## Built-in Harness services
|
|
144
|
+
|
|
145
|
+
The repository generates the service names, public methods, and source locations into each service's [subsystem page](../../reference/subsystems/core.md). Use those generated regions and the service's TypeScript interface while developing a plugin; do not maintain a second static list.
|
|
146
|
+
|
|
147
|
+
## Next steps
|
|
148
|
+
|
|
149
|
+
- [Event system](./events.md) — communicate between plugins without tight coupling
|
|
150
|
+
- [Capability layering](../practice/index.md) — use services as capability interfaces
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/user/develop/practice/index.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Three-role capability design
|
|
6
|
+
|
|
7
|
+
This page has two parts: a concept reference for the three-role capability pattern, followed by an advanced tutorial that builds one capability. Complete the [basic plugin path](../basic/index.md) and [services tutorial](../framework/service.md) first.
|
|
8
|
+
|
|
9
|
+
## Concept reference
|
|
10
|
+
|
|
11
|
+
When a capability is general enough to need replaceable providers, such as Bash execution, Harness separates three roles: a **Service Definition**, a **Service Provider**, and a **Consumer**. Put the roles in separate packages when they need to evolve or be replaced independently; a package may otherwise own more than one role. The complete capability is its seam. No individual role is a seam.
|
|
12
|
+
|
|
13
|
+
## Bash example
|
|
14
|
+
|
|
15
|
+
The Bash execution capability consists of:
|
|
16
|
+
|
|
17
|
+
- **Service Definition** (`dsh-shell`) — defines the Cordis service and Bash request and result types
|
|
18
|
+
- **Service Provider** (`dsh-bash-local`) — executes commands on the local machine
|
|
19
|
+
- **Consumer** (`dsh-tool-bash`) — exposes the capability as a model-callable tool
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
|
23
|
+
│ dsh-shell │────▶│ dsh-bash-local │ │ dsh-tool-bash│
|
|
24
|
+
│(definition) │ │ (provider) │ │(consumer/tool)│
|
|
25
|
+
└─────────────┘ └──────────────────┘ └──────────────┘
|
|
26
|
+
▲ │
|
|
27
|
+
└────────────────────────────────────────────┘
|
|
28
|
+
inject: ['shell']
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Benefits of the split
|
|
32
|
+
|
|
33
|
+
### Replace providers
|
|
34
|
+
|
|
35
|
+
One Service Definition can have multiple providers selected through `cordis.yml`:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
# Local execution
|
|
39
|
+
- name: '@deepseek-ai/dsh-bash-local'
|
|
40
|
+
|
|
41
|
+
# Replace this row with another package that provides the same service.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The Service Definition and tool remain unchanged while the provider changes.
|
|
45
|
+
|
|
46
|
+
### Evolve independently
|
|
47
|
+
|
|
48
|
+
- The Service Definition changes rarely after callers depend on its contract.
|
|
49
|
+
- Service Providers can improve performance and security independently.
|
|
50
|
+
- Consumers can change how they present the capability to the model.
|
|
51
|
+
|
|
52
|
+
### Decouple dependencies
|
|
53
|
+
|
|
54
|
+
- The Service Provider depends on the Service Definition.
|
|
55
|
+
- The Consumer depends on the Service Definition.
|
|
56
|
+
- The Service Provider and Consumer **do not depend on each other**.
|
|
57
|
+
|
|
58
|
+
The [capability-seam reference](../../reference/capability-seams.md) owns the current built-in families and package links.
|
|
59
|
+
|
|
60
|
+
## Tutorial: develop a three-role capability
|
|
61
|
+
|
|
62
|
+
### Step 1: write the Service Definition
|
|
63
|
+
|
|
64
|
+
```ts ignore-check
|
|
65
|
+
// packages/my-cap/my-cap/src/index.ts
|
|
66
|
+
import { Service, type Context } from '@deepseek-ai/cordis'
|
|
67
|
+
|
|
68
|
+
declare module '@deepseek-ai/cordis' {
|
|
69
|
+
interface Context {
|
|
70
|
+
myCap: MyCapService
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export abstract class MyCapService extends Service {
|
|
75
|
+
constructor(ctx: Context) {
|
|
76
|
+
super(ctx, 'myCap')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Execute the capability. */
|
|
80
|
+
abstract execute(request: MyCapRequest): Promise<MyCapResult>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface MyCapRequest {
|
|
84
|
+
input: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface MyCapResult {
|
|
88
|
+
output: string
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Step 2: write a Service Provider
|
|
93
|
+
|
|
94
|
+
```ts ignore-check
|
|
95
|
+
// packages/my-cap/my-cap-local/src/index.ts
|
|
96
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
97
|
+
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
|
|
98
|
+
|
|
99
|
+
class MyCapLocal extends MyCapService {
|
|
100
|
+
async execute(request: MyCapRequest): Promise<MyCapResult> {
|
|
101
|
+
// Local provider behavior.
|
|
102
|
+
return { output: request.input.toUpperCase() }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const name = 'my-cap-local'
|
|
107
|
+
|
|
108
|
+
export function apply(ctx: Context) {
|
|
109
|
+
ctx.plugin(MyCapLocal)
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Step 3: write a consumer
|
|
114
|
+
|
|
115
|
+
```ts ignore-check
|
|
116
|
+
// packages/my-cap/tool-my-cap/src/index.ts
|
|
117
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
118
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
119
|
+
|
|
120
|
+
export const name = 'tool-my-cap'
|
|
121
|
+
export const inject = ['tools', 'myCap']
|
|
122
|
+
|
|
123
|
+
export function apply(ctx: Context) {
|
|
124
|
+
ctx.tools.register(defineTool({
|
|
125
|
+
name: 'my_cap',
|
|
126
|
+
description: 'Execute my capability.',
|
|
127
|
+
parameters: {
|
|
128
|
+
input: { type: 'string', required: true },
|
|
129
|
+
},
|
|
130
|
+
output: {
|
|
131
|
+
schema: { type: 'string' },
|
|
132
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
133
|
+
},
|
|
134
|
+
async execute(args) {
|
|
135
|
+
const result = await ctx.myCap.execute({ input: args.input })
|
|
136
|
+
return result.output
|
|
137
|
+
},
|
|
138
|
+
}))
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Compose them in cordis.yml
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
- name: '@deepseek-ai/dsh-my-cap-local'
|
|
146
|
+
- name: '@deepseek-ai/dsh-tool-my-cap'
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Design points
|
|
150
|
+
|
|
151
|
+
- **Do not split preemptively** — use separate packages only when the roles need to evolve independently. A simple tool plugin does not.
|
|
152
|
+
- **The Service Definition owns Request/Result types** — Service Providers and Consumers depend only on the Service Definition package.
|
|
153
|
+
- **Explicit > implicit** — resolve defaults in an explicit `resolve(request): Spec` step rather than hiding `?? default` expressions inside `run()`.
|
|
154
|
+
|
|
155
|
+
## Next steps
|
|
156
|
+
|
|
157
|
+
- [LLM adapter](./llm-adapter.md) — implement an LLM provider
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
editSource: "docs/user/develop/practice/llm-adapter.md"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# LLM adapters
|
|
6
|
+
|
|
7
|
+
This guide connects a new LLM provider to Harness.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
An LLM adapter extends `LlmAdapter` and implements `stream()`, translating Harness's provider-neutral request into a provider API call and translating the response back into Harness chunks.
|
|
12
|
+
|
|
13
|
+
## Minimal implementation
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
18
|
+
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
|
19
|
+
|
|
20
|
+
class MyAdapter extends LlmAdapter {
|
|
21
|
+
private apiKey: string
|
|
22
|
+
|
|
23
|
+
constructor(apiKey: string) {
|
|
24
|
+
super()
|
|
25
|
+
this.apiKey = apiKey
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
29
|
+
// 1. Convert options.messages to the provider format.
|
|
30
|
+
// 2. Call the streaming API.
|
|
31
|
+
// 3. Convert the response into StreamChunk values.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Config {
|
|
36
|
+
apiKey: string
|
|
37
|
+
providers: string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Config: Schema<Config> = Schema.object({
|
|
41
|
+
apiKey: Schema.string().required(),
|
|
42
|
+
providers: Schema.array(Schema.string()).required(),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const name = 'my-llm-adapter'
|
|
46
|
+
export const inject = ['llm']
|
|
47
|
+
|
|
48
|
+
export function apply(ctx: Context, config: Config) {
|
|
49
|
+
const adapter = new MyAdapter(config.apiKey)
|
|
50
|
+
ctx.llm.registerAdapter(config.providers, adapter)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## StreamChunk protocol
|
|
55
|
+
|
|
56
|
+
`stream()` yields chunks using this protocol:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
|
60
|
+
|
|
61
|
+
async function* exampleChunks(): AsyncIterable<StreamChunk> {
|
|
62
|
+
// 1. Start each content block with block-start.
|
|
63
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
64
|
+
|
|
65
|
+
// 2. Stream text through text-delta.
|
|
66
|
+
yield { type: 'text-delta', index: 0, text: 'Hello' }
|
|
67
|
+
yield { type: 'text-delta', index: 0, text: ' world' }
|
|
68
|
+
|
|
69
|
+
// 3. End each content block with block-end and the complete block.
|
|
70
|
+
yield {
|
|
71
|
+
type: 'block-end',
|
|
72
|
+
index: 0,
|
|
73
|
+
block: { type: 'text', text: 'Hello world' },
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Tool-call block.
|
|
77
|
+
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
|
|
78
|
+
yield {
|
|
79
|
+
type: 'tool-call-delta',
|
|
80
|
+
index: 1,
|
|
81
|
+
id: CallId('call-123'),
|
|
82
|
+
name: 'bash',
|
|
83
|
+
argumentsDelta: '{"command":"ls"}',
|
|
84
|
+
}
|
|
85
|
+
yield {
|
|
86
|
+
type: 'block-end',
|
|
87
|
+
index: 1,
|
|
88
|
+
block: {
|
|
89
|
+
type: 'tool-call',
|
|
90
|
+
id: CallId('call-123'),
|
|
91
|
+
name: 'bash',
|
|
92
|
+
arguments: '{"command":"ls"}',
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 5. Token usage.
|
|
97
|
+
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
|
|
98
|
+
|
|
99
|
+
// 6. Finish reason.
|
|
100
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
101
|
+
// Alternatively, { kind: 'tool-calls' } requests tool execution.
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Key rules
|
|
106
|
+
|
|
107
|
+
- Every `block-start` has a matching `block-end`.
|
|
108
|
+
- `index` increases from 0 and identifies content-block order.
|
|
109
|
+
- A `tool-call-delta` carries raw JSON text in `argumentsDelta`, either all at once or over multiple chunks.
|
|
110
|
+
- `finish` is the final chunk.
|
|
111
|
+
- Emit `usage` before `finish`.
|
|
112
|
+
|
|
113
|
+
## GenerateOptions
|
|
114
|
+
|
|
115
|
+
`stream()` receives the exported `GenerateOptions` type. It includes the model, adapter-owned reasoning-effort id, conversation history, system prompt, tool schemas, generation parameters, stop sequences, and abort signal; treat the TypeScript type exported by `@deepseek-ai/dsh-llm` as authoritative. Map supported fields to the provider API. If the provider cannot honor a field, throw `LlmError` with a stable code instead of silently dropping it.
|
|
116
|
+
|
|
117
|
+
Override `resolveModel(provider, model, signal?)` to return exact provider/model identity plus optional `context` and `reasoning` metadata in one lookup. Reasoning metadata contains ordered opaque ids and display names plus an optional configured default; preserve the adapter's authoritative selectable list, including `off` when its upstream capability API returns it, instead of promoting those values into a core enum. Honor the optional signal for asynchronous lookup so cancellation and disposal reach quiescence. The service validates the aggregate and rejects unsupported explicit efforts before `stream()`; omitting `reasoning` means that model has no selectable reasoning-effort capability.
|
|
118
|
+
|
|
119
|
+
## Register an adapter
|
|
120
|
+
|
|
121
|
+
```ts ignore-check
|
|
122
|
+
ctx.llm.registerAdapter(['my-provider'], adapter)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The first argument lists provider routes handled by the adapter. `GenerateOptions.provider` selects the registered adapter, while `GenerateOptions.model` passes an adapter-owned model id without lifecycle registration. Override `listModels()` when the adapter can advertise model choices to selectors.
|
|
126
|
+
|
|
127
|
+
## Use it from cordis.yml
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
- id: my-llm
|
|
131
|
+
name: './src/my-llm-adapter.ts'
|
|
132
|
+
config:
|
|
133
|
+
apiKey: !!js process.env.MY_API_KEY
|
|
134
|
+
providers:
|
|
135
|
+
- my-provider
|
|
136
|
+
|
|
137
|
+
- id: agent-loop
|
|
138
|
+
name: '@deepseek-ai/dsh-agent-loop'
|
|
139
|
+
config:
|
|
140
|
+
agents:
|
|
141
|
+
- id: main
|
|
142
|
+
provider: my-provider
|
|
143
|
+
model: my-model-v1
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Reference implementations
|
|
147
|
+
|
|
148
|
+
The repository contains complete implementations:
|
|
149
|
+
|
|
150
|
+
- `packages/llm/llm-deepseek/` — DeepSeek API adapter using the OpenAI-compatible format
|
|
151
|
+
- `packages/llm/llm-pi-ai/` — Pi AI adapter using a different API format
|
|
152
|
+
|
|
153
|
+
Compare the two shipped adapters to see the same harness contract implemented over different provider SDKs.
|
|
154
|
+
|
|
155
|
+
## Error handling
|
|
156
|
+
|
|
157
|
+
Adapters throw transport and protocol failures as `LlmError` values with stable codes. The agent loop preserves the error and code for diagnostics and policy; it does not convert an ordinary `Error` automatically. Every provider HTTP request must also merge `attributionHeaders()` and forward `options.signal`.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import {
|
|
161
|
+
attributionHeaders,
|
|
162
|
+
LlmAdapter,
|
|
163
|
+
LlmError,
|
|
164
|
+
type GenerateOptions,
|
|
165
|
+
type StreamChunk,
|
|
166
|
+
} from '@deepseek-ai/dsh-llm'
|
|
167
|
+
|
|
168
|
+
class HttpAdapter extends LlmAdapter {
|
|
169
|
+
constructor(private readonly endpoint: string) {
|
|
170
|
+
super()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
174
|
+
const response = await fetch(this.endpoint, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: {
|
|
177
|
+
'content-type': 'application/json',
|
|
178
|
+
...attributionHeaders(),
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify({ model: options.model, messages: options.messages }),
|
|
181
|
+
...options.signal ? { signal: options.signal } : {},
|
|
182
|
+
})
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR')
|
|
185
|
+
}
|
|
186
|
+
// A real adapter parses the response and emits the complete chunk sequence.
|
|
187
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
Binary file
|
|
Binary file
|