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 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).
@@ -0,0 +1,7 @@
1
+ - insert:
2
+ - id: cc-agents
3
+ name: 'dsh-cc-agents'
4
+ config:
5
+ provider: spawn
6
+ toolName: cc_agent
7
+ injectCatalog: true
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
+ }