dsh-cc-agents 0.1.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/README.md +79 -0
- package/cordis.patch.yml +7 -0
- package/package.json +66 -0
- package/src/index.js +444 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# dsh-cc-agents
|
|
2
|
+
|
|
3
|
+
Load Claude Code **"first kind" agents** (`.claude/agents/*.md`) into DSH as
|
|
4
|
+
delegatable subagents with identity anchoring.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
CC agents are identity-anchored subagents: a frontmatter block
|
|
9
|
+
(`name` / `description` / `tools` / `disallowedTools` / `model` / `skills` /
|
|
10
|
+
`background` / `initialPrompt` …) plus a Markdown body that is the agent's
|
|
11
|
+
system prompt. DSH's subagent seam has no pre-registered agent directory, so
|
|
12
|
+
this adapter provides both halves:
|
|
13
|
+
|
|
14
|
+
1. **Catalog injection** — at every session start, a user-role message lists
|
|
15
|
+
the available agents (`name: description`), reproducing CC's @-mention
|
|
16
|
+
semantics: the model *sees* which agents exist and when to delegate.
|
|
17
|
+
2. **`cc_agent` delegation tool** — the model calls
|
|
18
|
+
`cc_agent(agent, description, prompt)`; the adapter:
|
|
19
|
+
- looks the agent up in the loaded IR catalog,
|
|
20
|
+
- starts a subagent via `ctx.subagents.start(provider, …)` with
|
|
21
|
+
`persona` = the agent body (DSH's native identity channel),
|
|
22
|
+
- scopes the child's tools with `toolFilter` from `disallowedTools`
|
|
23
|
+
(applied first) then `tools` (CC semantics; buckets like `Read`/`Bash`
|
|
24
|
+
expand to DSH tool names, `mcp__…` names pass through),
|
|
25
|
+
- preloads `skills` (frontmatter skill names → full skill bodies),
|
|
26
|
+
- maps `model` → `agentOptions.model`, `background` → background job,
|
|
27
|
+
- maps `initialPrompt` → prompt prefix.
|
|
28
|
+
|
|
29
|
+
## Config
|
|
30
|
+
|
|
31
|
+
| key | default | meaning |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `provider` | `spawn` | `ctx.subagents` provider to delegate on (`spawn` / `fork`) |
|
|
34
|
+
| `toolName` | `cc_agent` | model-facing tool name |
|
|
35
|
+
| `injectCatalog` | `true` | inject agent catalog into session start |
|
|
36
|
+
| `enableRunInBackground` | `true` | expose `run_in_background` |
|
|
37
|
+
| `maxDepth` | `3` | absolute delegation-depth cap |
|
|
38
|
+
| `enableGlobal` | `true` | include global `~/.claude/agents` |
|
|
39
|
+
| `globalClaudeDir` | `~/.claude` | override global dir |
|
|
40
|
+
|
|
41
|
+
## Scope precedence
|
|
42
|
+
|
|
43
|
+
Project `.claude/agents/` beats global `~/.claude/agents/` on name clashes
|
|
44
|
+
(fail loud — a warning is logged). Plugin agents arrive with the plugin source
|
|
45
|
+
(M4).
|
|
46
|
+
|
|
47
|
+
## Classification (from dsh-cc-loader)
|
|
48
|
+
|
|
49
|
+
- `DIRECT` — delegatable.
|
|
50
|
+
- `ADAPTED` — delegatable with degraded fields (`memory`, unknown fields).
|
|
51
|
+
- `UNSUPPORTED` — fields DSH cannot honor (`hooks` / `mcpServers` /
|
|
52
|
+
`permissionMode` — CC itself forbids these on plugin agents); the agent
|
|
53
|
+
still delegates without them.
|
|
54
|
+
- `BLOCKED` — `isolation: worktree`; never offered for delegation.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
dsh plugin --profile <p> add dsh-cc-agents
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Dev checkout (hot user layer, `file:///` required on Windows):
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- insert:
|
|
66
|
+
- id: cc-agents
|
|
67
|
+
name: 'file:///C:/Users/.../packages/cc-agents/src/index.js'
|
|
68
|
+
config: { provider: spawn }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
- `@deepseek-ai/dsh-subagent` with an in-process provider (`spawn`) registered.
|
|
74
|
+
- `@deepseek-ai/dsh-jobs` + `@deepseek-ai/dsh-tool-jobs` only for background
|
|
75
|
+
runs.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT. Shared parse layer is `dsh-cc-loader` (MIT, part of this repo).
|
package/cordis.patch.yml
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-cc-agents",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Load Claude Code .claude/agents (identity-anchored subagents: frontmatter + system-prompt body) into DSH delegation via the persona channel.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dsh": {
|
|
11
|
+
"bundle": {
|
|
12
|
+
"patch": "./cordis.patch.yml"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"cordis.patch.yml"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"dsh",
|
|
22
|
+
"deepseek-harness",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"claude",
|
|
25
|
+
"agents",
|
|
26
|
+
"subagent",
|
|
27
|
+
"persona"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"dsh-cc-loader": "^0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@deepseek-ai/cordis": "*",
|
|
35
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
36
|
+
"@deepseek-ai/dsh-subagent": "*",
|
|
37
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
38
|
+
"@deepseek-ai/schemastery": "*"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"@deepseek-ai/cordis": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"@deepseek-ai/dsh-llm": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@deepseek-ai/dsh-subagent": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@deepseek-ai/dsh-tools": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@deepseek-ai/schemastery": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
59
|
+
"@deepseek-ai/schemastery": "3.18.1"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20"
|
|
63
|
+
},
|
|
64
|
+
"repository": "git+https://github.com/Bcy2020/dsh-cc-ecosystem.git",
|
|
65
|
+
"homepage": "https://github.com/Bcy2020/dsh-cc-ecosystem"
|
|
66
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// dsh-cc-agents — adapt Claude Code "first kind" agents (.claude/agents/*.md)
|
|
2
|
+
// into DSH delegation.
|
|
3
|
+
//
|
|
4
|
+
// CC agents are identity-anchored subagents: frontmatter (name/description/
|
|
5
|
+
// tools/disallowedTools/model/skills/…) + a system-prompt body. DSH has no
|
|
6
|
+
// pre-registered agent directory, so this adapter provides both halves:
|
|
7
|
+
// 1. catalog injection (agent/session-start → user message): the model sees
|
|
8
|
+
// the available agents' name + description (CC @-mention semantics).
|
|
9
|
+
// 2. a `cc_agent` delegation tool: looks the agent up, then starts a
|
|
10
|
+
// subagent via ctx.subagents with
|
|
11
|
+
// persona = agent body (DSH native identity channel)
|
|
12
|
+
// toolFilter = tools/disallowedTools expanded to DSH tool names
|
|
13
|
+
// agentOptions.model = frontmatter.model
|
|
14
|
+
// plus skill preloading (frontmatter.skills → skill bodies) and
|
|
15
|
+
// initialPrompt/background support.
|
|
16
|
+
//
|
|
17
|
+
// Classification lives in dsh-cc-loader; BLOCKED agents (isolation:worktree)
|
|
18
|
+
// are never offered for delegation.
|
|
19
|
+
|
|
20
|
+
import { readFile } from 'node:fs/promises'
|
|
21
|
+
import { homedir } from 'node:os'
|
|
22
|
+
import z from '@deepseek-ai/schemastery'
|
|
23
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
24
|
+
import { loadClaude, parseFrontmatter, expandCcToolToDsh, pluginComponentName } from 'dsh-cc-loader'
|
|
25
|
+
|
|
26
|
+
export const name = 'cc-agents'
|
|
27
|
+
export const inject = ['tools', 'subagents']
|
|
28
|
+
|
|
29
|
+
export const Config = z.object({
|
|
30
|
+
/** ctx.subagents provider the delegation runs on (spawn/fork). */
|
|
31
|
+
provider: z.string().default('spawn'),
|
|
32
|
+
/** Model-facing tool name. */
|
|
33
|
+
toolName: z.string().default('cc_agent'),
|
|
34
|
+
/** Inject the agent catalog into every session start (CC @-mention semantics). */
|
|
35
|
+
injectCatalog: z.boolean().default(true),
|
|
36
|
+
/** Expose run_in_background. */
|
|
37
|
+
enableRunInBackground: z.boolean().default(true),
|
|
38
|
+
/** Absolute delegation-depth cap (default 3). */
|
|
39
|
+
maxDepth: z.natural().max(Number.MAX_SAFE_INTEGER).default(3),
|
|
40
|
+
projectRootMarkers: z.array(z.string()).default(['.git']),
|
|
41
|
+
enableGlobal: z.boolean().default(true),
|
|
42
|
+
globalClaudeDir: z.string(),
|
|
43
|
+
homeDir: z.string(),
|
|
44
|
+
projectAgentRank: z.number().default(150),
|
|
45
|
+
globalAgentRank: z.number().default(160),
|
|
46
|
+
// ── plugin roots (M4c) ────────────────────────────────────────────────────
|
|
47
|
+
// Plugin agents are exposed as `plugin-<plugin>-<name>` (DSH skill-name
|
|
48
|
+
// grammar is strict kebab-case; CC's `<plugin>:<name>` colon does not fit).
|
|
49
|
+
pluginRoots: z.array(z.string()).default([]),
|
|
50
|
+
pluginAgentRank: z.number().default(170),
|
|
51
|
+
/**
|
|
52
|
+
* CC frontmatter model names → DSH model names. CC agents name Claude
|
|
53
|
+
* models (sonnet/opus/…), which this deployment cannot resolve; without an
|
|
54
|
+
* alias the declared model is ignored (default model) with a warning.
|
|
55
|
+
*/
|
|
56
|
+
modelAliases: z.dict(z.string()).default({}),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
/** The `{kind:'plugin'}` source stamped on every context this adapter injects. */
|
|
60
|
+
const PLUGIN_SOURCE = { kind: 'plugin', plugin: 'cc-agents' }
|
|
61
|
+
|
|
62
|
+
export function apply(ctx, config = {}) {
|
|
63
|
+
const homeDir = config.homeDir ?? homedir()
|
|
64
|
+
const loaderOpts = () => ({
|
|
65
|
+
cwd: process.cwd(), // replaced per call
|
|
66
|
+
homeDir,
|
|
67
|
+
projectRootMarkers: config.projectRootMarkers,
|
|
68
|
+
enableGlobal: config.enableGlobal,
|
|
69
|
+
globalClaudeDir: config.globalClaudeDir,
|
|
70
|
+
projectSkillRank: config.projectAgentRank,
|
|
71
|
+
globalSkillRank: config.globalAgentRank,
|
|
72
|
+
pluginRoots: config.pluginRoots,
|
|
73
|
+
pluginSkillRank: config.pluginAgentRank,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
ctx.logger.info(`cc-agents: registered (delegation via ${config.provider}, catalog injection ${config.injectCatalog !== false ? 'on' : 'off'})`)
|
|
77
|
+
|
|
78
|
+
// Catalog state: cached per session id so session-start and tool calls
|
|
79
|
+
// share one discovery per session. Invalidation = new session id.
|
|
80
|
+
const sessionCache = new Map() // sessionId → { agents, skills }
|
|
81
|
+
|
|
82
|
+
async function catalogFor(agent) {
|
|
83
|
+
const sessionId = agent?.session?.header?.id
|
|
84
|
+
if (sessionId !== undefined) {
|
|
85
|
+
const hit = sessionCache.get(sessionId)
|
|
86
|
+
if (hit !== undefined) return hit
|
|
87
|
+
}
|
|
88
|
+
const cwd = agent?.session?.header?.cwd ?? process.cwd()
|
|
89
|
+
const ir = await loadClaude({ ...loaderOpts(), cwd })
|
|
90
|
+
for (const w of ir.warnings) ctx.logger.warn(`cc-agents: ${w}`)
|
|
91
|
+
const entry = { agents: mergePluginAgents(ir), skills: ir.components.skills, cwd }
|
|
92
|
+
if (sessionId !== undefined) sessionCache.set(sessionId, entry)
|
|
93
|
+
return entry
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── catalog injection (agent/session-start) ──────────────────────────────
|
|
97
|
+
// Top-level sessions only: subagents (delegated children carry a
|
|
98
|
+
// parentSession header) get their own system prompt — the CC agent body as
|
|
99
|
+
// persona — and must NOT receive the parent's catalog reminder. Injecting it
|
|
100
|
+
// would append a user message after the delegation prompt, which the child
|
|
101
|
+
// then mistakes for its actual task.
|
|
102
|
+
if (config.injectCatalog !== false) {
|
|
103
|
+
ctx.on('agent/session-start', async ({ agent }) => {
|
|
104
|
+
try {
|
|
105
|
+
if (agent?.session?.header?.parentSession !== undefined) return
|
|
106
|
+
const { agents } = await catalogFor(agent)
|
|
107
|
+
const delegatable = agents.filter((a) => a.status !== 'BLOCKED')
|
|
108
|
+
if (delegatable.length === 0) return
|
|
109
|
+
const lines = delegatable.map((a) => {
|
|
110
|
+
const tools = a.tools.length > 0 ? ` (tools: ${a.tools.join(', ')})` : ''
|
|
111
|
+
return `- ${a.name}: ${a.description}${tools}`
|
|
112
|
+
})
|
|
113
|
+
const text = `<system-reminder>
|
|
114
|
+
Claude Code agents available for delegation via the ${config.toolName} tool:
|
|
115
|
+
${lines.join('\n')}
|
|
116
|
+
</system-reminder>`
|
|
117
|
+
agent.inject(createUserMessage({ content: [{ type: 'text', text }], source: PLUGIN_SOURCE }))
|
|
118
|
+
} catch (error) {
|
|
119
|
+
ctx.logger.warn(`cc-agents: catalog injection failed: ${String(error)}`)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── delegation tool ──────────────────────────────────────────────────────
|
|
125
|
+
const toolName = config.toolName ?? 'cc_agent'
|
|
126
|
+
ctx.tools.register(defineCcAgentTool(ctx, config, catalogFor, toolName))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── tool definition ────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function defineCcAgentTool(ctx, config, catalogFor, toolName) {
|
|
132
|
+
const backgroundEnabled = config.enableRunInBackground !== false
|
|
133
|
+
const providerName = config.provider
|
|
134
|
+
|
|
135
|
+
const description = `Delegate a task to a Claude Code agent (identity-anchored subagent): the agent's system prompt is its persona, its frontmatter tools are scoped to the child, and its skills are preloaded. The agent runs as a fresh child that does not see this conversation — give it a complete, standalone prompt. It returns its result, not its intermediate steps.`
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
name: toolName,
|
|
139
|
+
description: description + (backgroundEnabled
|
|
140
|
+
? ' This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`.'
|
|
141
|
+
: ''),
|
|
142
|
+
parameters: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
agent: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
description: 'The Claude Code agent to delegate to (its name from the injected catalog).',
|
|
148
|
+
},
|
|
149
|
+
description: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
description: 'A short (3-5 word) description of the delegated task, for display.',
|
|
152
|
+
},
|
|
153
|
+
prompt: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
description: 'The complete, self-contained task for the agent. It does not share this conversation\'s context.',
|
|
156
|
+
},
|
|
157
|
+
...backgroundEnabled ? {
|
|
158
|
+
run_in_background: {
|
|
159
|
+
type: 'boolean',
|
|
160
|
+
description: 'Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill.',
|
|
161
|
+
},
|
|
162
|
+
} : {},
|
|
163
|
+
},
|
|
164
|
+
required: ['agent', 'description', 'prompt'],
|
|
165
|
+
},
|
|
166
|
+
output: {
|
|
167
|
+
schema: {
|
|
168
|
+
oneOf: [
|
|
169
|
+
{
|
|
170
|
+
type: 'object',
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
required: ['kind', 'jobId'],
|
|
173
|
+
properties: {
|
|
174
|
+
kind: { type: 'string', const: 'background' },
|
|
175
|
+
jobId: { type: 'string' },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'object',
|
|
180
|
+
additionalProperties: false,
|
|
181
|
+
required: ['kind', 'runId', 'output'],
|
|
182
|
+
properties: {
|
|
183
|
+
kind: { type: 'string', const: 'foreground' },
|
|
184
|
+
runId: { type: 'string' },
|
|
185
|
+
output: { type: 'array', items: {} },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
render: (_args, value) => [{
|
|
191
|
+
type: 'text',
|
|
192
|
+
text: value.kind === 'background'
|
|
193
|
+
? `started background CC agent task ${value.jobId}`
|
|
194
|
+
: outputValueText(value.output),
|
|
195
|
+
}],
|
|
196
|
+
},
|
|
197
|
+
isConcurrencySafe: () => true,
|
|
198
|
+
async execute(args, exec) {
|
|
199
|
+
const parent = exec.agent
|
|
200
|
+
if (!parent) throw new Error(`${toolName} requires a calling agent (exec.agent was undefined)`)
|
|
201
|
+
// Raw ctx.tools.register() skips defineTool's arg validation, so enforce
|
|
202
|
+
// the required string parameters here (a missing prompt must never turn
|
|
203
|
+
// into the literal text "undefined" inside the child's prompt).
|
|
204
|
+
if (typeof args?.agent !== 'string' || args.agent.length === 0) {
|
|
205
|
+
throw new Error(`${toolName}: "agent" (the Claude Code agent name) is required`)
|
|
206
|
+
}
|
|
207
|
+
if (typeof args?.description !== 'string' || args.description.length === 0) {
|
|
208
|
+
throw new Error(`${toolName}: "description" (a short task description) is required`)
|
|
209
|
+
}
|
|
210
|
+
if (typeof args?.prompt !== 'string' || args.prompt.length === 0) {
|
|
211
|
+
throw new Error(`${toolName}: "prompt" (the standalone task for the agent) is required`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const { agents, skills } = await catalogFor(parent)
|
|
215
|
+
const agent = agents.find((a) => a.name === args.agent)
|
|
216
|
+
if (agent === undefined) {
|
|
217
|
+
const available = agents.filter((a) => a.status !== 'BLOCKED').map((a) => a.name)
|
|
218
|
+
throw new Error(`cc-agents: unknown agent "${args.agent}" (available: ${available.join(', ') || 'none'})`) }
|
|
219
|
+
if (agent.status === 'BLOCKED') {
|
|
220
|
+
throw new Error(`cc-agents: agent "${args.agent}" is not delegatable (${agent.notes?.join('; ') ?? 'blocked'})`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── tool filter: disallowedTools first, then tools (CC semantics). ────
|
|
224
|
+
const deny = expandAll(agent.disallowedTools)
|
|
225
|
+
const allow = expandAll(agent.tools)
|
|
226
|
+
const knownDeny = filterKnownToolNames(parent.ctx, deny)
|
|
227
|
+
const knownAllow = allow.length > 0 ? filterKnownToolNames(parent.ctx, allow) : []
|
|
228
|
+
const toolFilter = knownDeny.length > 0 || knownAllow.length > 0
|
|
229
|
+
? {
|
|
230
|
+
...knownDeny.length > 0 ? { deny: knownDeny } : {},
|
|
231
|
+
...knownAllow.length > 0 ? { allow: knownAllow } : {},
|
|
232
|
+
}
|
|
233
|
+
: undefined
|
|
234
|
+
if (knownDeny.length < deny.length || knownAllow.length < allow.length) {
|
|
235
|
+
ctx.logger.warn(`cc-agents: agent "${agent.name}" — dropped unknown tool names (deny ${deny.length - knownDeny.length}, allow ${allow.length - knownAllow.length})`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── prompt: preloaded skills + initialPrompt + user task. ─────────────
|
|
239
|
+
const preload = []
|
|
240
|
+
for (const skillName of agent.skills) {
|
|
241
|
+
const skill = skills.find((s) => s.name === skillName)
|
|
242
|
+
if (skill === undefined) {
|
|
243
|
+
ctx.logger.warn(`cc-agents: agent "${agent.name}" preload skill "${skillName}" not found`)
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
let body
|
|
247
|
+
try {
|
|
248
|
+
const raw = await readFile(skill.locator.path, { encoding: 'utf8' })
|
|
249
|
+
const parsed = parseFrontmatter(raw)
|
|
250
|
+
body = parsed === undefined ? raw.trim() : parsed.body.trim()
|
|
251
|
+
} catch { continue }
|
|
252
|
+
if (body.length > 0) preload.push(`<skill name="${skillName}">\n${body}\n</skill>`)
|
|
253
|
+
}
|
|
254
|
+
const parts = []
|
|
255
|
+
if (preload.length > 0) parts.push(`Preloaded skills:\n${preload.join('\n\n')}`)
|
|
256
|
+
if (agent.initialPrompt !== undefined) parts.push(agent.initialPrompt)
|
|
257
|
+
parts.push(args.prompt)
|
|
258
|
+
const prompt = [{ type: 'text', text: parts.join('\n\n') }]
|
|
259
|
+
|
|
260
|
+
const model = resolveModel(ctx, config, agent)
|
|
261
|
+
const request = {
|
|
262
|
+
label: `cc:${agent.scope}:${agent.name}`,
|
|
263
|
+
prompt,
|
|
264
|
+
parent,
|
|
265
|
+
persona: personaFor(agent),
|
|
266
|
+
...toolFilter !== undefined ? { toolFilter } : {},
|
|
267
|
+
...model !== undefined ? { agentOptions: { model } } : {},
|
|
268
|
+
maxDepth: config.maxDepth,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const runInBackground = args.run_in_background === true || agent.background === true
|
|
272
|
+
if (runInBackground) {
|
|
273
|
+
const jobs = ctx.get('jobs')
|
|
274
|
+
if (jobs === undefined) {
|
|
275
|
+
throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs')
|
|
276
|
+
}
|
|
277
|
+
const id = jobs.start({
|
|
278
|
+
kind: 'subagent',
|
|
279
|
+
label: request.label,
|
|
280
|
+
owner: parent,
|
|
281
|
+
run: () => {
|
|
282
|
+
const controller = new AbortController()
|
|
283
|
+
const start = ctx.subagents.start(providerName, { ...request, signal: controller.signal })
|
|
284
|
+
return {
|
|
285
|
+
cancel: (reason) => controller.abort(reason ?? 'background CC agent task killed'),
|
|
286
|
+
done: settleStart(start, controller.signal),
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
return { kind: 'background', jobId: id }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const run = await ctx.subagents.start(providerName, { ...request, signal: exec.signal })
|
|
294
|
+
try {
|
|
295
|
+
return await settleForegroundRun(run)
|
|
296
|
+
} catch (error) {
|
|
297
|
+
ctx.logger.warn(`cc-agents: agent "${agent.name}" run failed: ${String(error)}`)
|
|
298
|
+
throw error
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Top-level agents + plugin agents namespaced `plugin-<plugin>-<name>`
|
|
308
|
+
* (M4c). Pure: takes the loader IR, returns the delegation catalog. Plugin
|
|
309
|
+
* agents keep every IR field except the name, which becomes the DSH-facing
|
|
310
|
+
* namespaced identifier the model sees in the injected catalog and passes to
|
|
311
|
+
* the cc_agent tool.
|
|
312
|
+
* @param {object} ir - loadClaude IR.
|
|
313
|
+
* @returns {object[]} agent catalog entries.
|
|
314
|
+
*/
|
|
315
|
+
export function mergePluginAgents(ir) {
|
|
316
|
+
const pluginAgents = []
|
|
317
|
+
for (const plugin of ir.components.plugins ?? []) {
|
|
318
|
+
for (const a of plugin.components.agents ?? []) {
|
|
319
|
+
pluginAgents.push({ ...a, name: pluginComponentName(plugin.name, a.name), scope: 'plugin' })
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return [...(ir.components.agents ?? []), ...pluginAgents]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The delegation persona: the agent body, plus any frontmatter `context`
|
|
327
|
+
* material appended verbatim (community convention; CC has no such field in
|
|
328
|
+
* the official 16, but some agents carry extra system-prompt prose there).
|
|
329
|
+
*/
|
|
330
|
+
export function personaFor(agent) {
|
|
331
|
+
const context = agent.context
|
|
332
|
+
if (context === undefined || context.length === 0) return agent.systemPrompt
|
|
333
|
+
return `${agent.systemPrompt}\n\n${context.join('\n\n')}`
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Resolve a CC frontmatter `model` name to a DSH model name via the
|
|
338
|
+
* `modelAliases` config. CC agents name Claude models (sonnet/opus/…), which
|
|
339
|
+
* a DSH deployment usually cannot resolve; without an alias the declared
|
|
340
|
+
* model is dropped (default model) with a warning. Never returns a raw CC
|
|
341
|
+
* model name — passing it to agentOptions would fail the child.
|
|
342
|
+
*/
|
|
343
|
+
function resolveModel(ctx, config, agent) {
|
|
344
|
+
const ccModel = agent.model
|
|
345
|
+
if (ccModel === undefined) return undefined
|
|
346
|
+
const alias = config.modelAliases?.[ccModel]
|
|
347
|
+
if (alias !== undefined && typeof alias === 'string' && alias.length > 0) return alias
|
|
348
|
+
ctx.logger.warn(`cc-agents: agent "${agent.name}" declares model "${ccModel}" — no modelAliases mapping, using the default model`)
|
|
349
|
+
return undefined
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Expand CC tool names to candidate DSH names, collecting drop notes. */
|
|
353
|
+
function expandAll(ccNames) {
|
|
354
|
+
const notes = []
|
|
355
|
+
const out = []
|
|
356
|
+
for (const name of ccNames) out.push(...expandCcToolToDsh(name, notes))
|
|
357
|
+
return out
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Keep only tool names the current scope's registry accepts, probing each
|
|
362
|
+
* candidate with a scoped restrict() + immediate dispose. The parent's
|
|
363
|
+
* registry approximates the child's (spawn children inherit the deployment
|
|
364
|
+
* composition), so a name valid here is valid in the child's creation window.
|
|
365
|
+
*/
|
|
366
|
+
function filterKnownToolNames(agentCtx, names) {
|
|
367
|
+
const known = []
|
|
368
|
+
for (const name of names) {
|
|
369
|
+
try {
|
|
370
|
+
const dispose = agentCtx.tools.restrict({ deny: [name] })
|
|
371
|
+
dispose()
|
|
372
|
+
known.push(name)
|
|
373
|
+
} catch {
|
|
374
|
+
// Unknown global tool (or scoped-only registration) — skip.
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return known
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Render text blocks from the canonical JSON block array. */
|
|
381
|
+
function outputValueText(values) {
|
|
382
|
+
return values
|
|
383
|
+
.filter((value) => typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
384
|
+
&& value.type === 'text' && typeof value.text === 'string')
|
|
385
|
+
.map((value) => value.text)
|
|
386
|
+
.join('')
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Settle pending startup without rejecting the task producer contract. */
|
|
390
|
+
async function settleStart(start, signal) {
|
|
391
|
+
try {
|
|
392
|
+
return await settleForegroundRun(await start)
|
|
393
|
+
} catch (error) {
|
|
394
|
+
return signal.aborted
|
|
395
|
+
? { status: 'killed' }
|
|
396
|
+
: { status: 'failed', detail: String(error) }
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Collect and release one foreground run. */
|
|
401
|
+
async function settleForegroundRun(run) {
|
|
402
|
+
const [execution] = await Promise.allSettled([
|
|
403
|
+
run.result.then((result) => {
|
|
404
|
+
const error = stopReasonError(result)
|
|
405
|
+
if (error !== undefined) throw new Error(withPartialText(error, result.output))
|
|
406
|
+
return {
|
|
407
|
+
kind: 'foreground',
|
|
408
|
+
runId: run.id,
|
|
409
|
+
output: result.output,
|
|
410
|
+
}
|
|
411
|
+
}),
|
|
412
|
+
])
|
|
413
|
+
const [disposal] = await Promise.allSettled([Promise.resolve().then(() => run.dispose())])
|
|
414
|
+
if (execution.status === 'rejected') {
|
|
415
|
+
if (disposal.status === 'rejected') {
|
|
416
|
+
throw new AggregateError([execution.reason, disposal.reason],
|
|
417
|
+
`CC agent run failed: ${String(execution.reason)}; dispose failed: ${String(disposal.reason)}`)
|
|
418
|
+
}
|
|
419
|
+
throw execution.reason
|
|
420
|
+
}
|
|
421
|
+
if (disposal.status === 'rejected') throw disposal.reason
|
|
422
|
+
return execution.value
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** A non-`completed` stop reason means the child did not finish cleanly. */
|
|
426
|
+
function stopReasonError(result) {
|
|
427
|
+
switch (result.stopReason) {
|
|
428
|
+
case 'completed': return undefined
|
|
429
|
+
case 'aborted': return 'CC agent run was cancelled'
|
|
430
|
+
case 'error': return 'CC agent run failed'
|
|
431
|
+
case 'max-tokens': return 'CC agent run hit its token limit before finishing'
|
|
432
|
+
case 'refusal': return 'CC agent declined the task'
|
|
433
|
+
default: return `CC agent run ended abnormally (${String(result.stopReason)})`
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Append partial output to a stop-reason error so real text still reaches the model. */
|
|
438
|
+
function withPartialText(error, output) {
|
|
439
|
+
const text = output
|
|
440
|
+
.filter((block) => block.type === 'text')
|
|
441
|
+
.map((block) => block.text)
|
|
442
|
+
.join('')
|
|
443
|
+
return text.length === 0 ? error : `${error}\nPartial output before the run ended:\n${text}`
|
|
444
|
+
}
|