feat(subagent): add Codex product provider
This commit is contained in:
42
examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
vendored
Normal file
42
examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# Test-only composition: one real Codex app-server delegation through the
|
||||
# Loader, fixed provider tool, common foreground settlement, and JSONL store.
|
||||
- id: fixture
|
||||
name: './fixture.ts'
|
||||
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
- id: subagent-codex
|
||||
name: '@deepseek-ai/dsh-subagent-codex'
|
||||
config:
|
||||
env:
|
||||
OPENAI_API_KEY: !!js process.env.DSH_TEST_OPENAI_API_KEY
|
||||
CODEX_HOME: !!js process.cwd() + '/codex-home'
|
||||
HOME: !!js process.cwd()
|
||||
XDG_CONFIG_HOME: !!js process.cwd() + '/xdg'
|
||||
PATH: !!js process.env.PATH
|
||||
HTTP_PROXY: ''
|
||||
HTTPS_PROXY: ''
|
||||
ALL_PROXY: ''
|
||||
NO_PROXY: '127.0.0.1,localhost'
|
||||
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: codex
|
||||
toolName: subagent_codex
|
||||
enableRunInBackground: false
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
config:
|
||||
provider: mock
|
||||
model: mock-delegate
|
||||
persona: 'Delegate the task through the fixed Codex tool.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext: false
|
||||
102
examples/acp-agent/tests/fixtures/subagent/subagent-codex/fixture.ts
vendored
Normal file
102
examples/acp-agent/tests/fixtures/subagent/subagent-codex/fixture.ts
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
/** Deterministic parent model and process-quiescence observer for the Codex Loader snapshot. */
|
||||
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import type { Context } from 'cordis'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type {
|
||||
SubprocessHandle,
|
||||
SubprocessSpawnSpec,
|
||||
} from '@deepseek-ai/dsh-subprocess'
|
||||
|
||||
const CODEX_TASK = 'Return the Loader snapshot sentinel exactly.'
|
||||
const QUIESCENCE_FILE = '.codex-quiescence.json'
|
||||
|
||||
function toolResultText(options: GenerateOptions): string {
|
||||
return options.messages.at(-1)?.content
|
||||
.filter(block => block.type === 'tool-result')
|
||||
.flatMap(block => block.content)
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join('') ?? ''
|
||||
}
|
||||
|
||||
class CodexDelegatingAdapter extends LlmAdapter {
|
||||
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
const result = toolResultText(options)
|
||||
if (result.length === 0) {
|
||||
const args = JSON.stringify({
|
||||
description: 'Codex Loader snapshot',
|
||||
prompt: CODEX_TASK,
|
||||
})
|
||||
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
|
||||
yield {
|
||||
type: 'tool-call-delta',
|
||||
index: 0,
|
||||
id: CallId('call-codex-loader'),
|
||||
name: 'subagent_codex',
|
||||
argumentsDelta: args,
|
||||
}
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: {
|
||||
type: 'tool-call',
|
||||
id: CallId('call-codex-loader'),
|
||||
name: 'subagent_codex',
|
||||
arguments: args,
|
||||
},
|
||||
}
|
||||
yield { type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } }
|
||||
yield { type: 'finish', reason: { kind: 'tool-calls' } }
|
||||
return
|
||||
}
|
||||
|
||||
const reply = `Codex child returned: ${result}`
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
yield { type: 'text-delta', index: 0, text: reply }
|
||||
yield { type: 'block-end', index: 0, block: { type: 'text', text: reply } }
|
||||
yield { type: 'usage', usage: { inputTokens: 10, outputTokens: reply.length } }
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
}
|
||||
}
|
||||
|
||||
interface ObservedProcess {
|
||||
readonly spec: SubprocessSpawnSpec
|
||||
readonly handle: SubprocessHandle
|
||||
}
|
||||
|
||||
export const name = 'codex-loader-snapshot-fixture'
|
||||
export const inject = ['llm', 'subprocess']
|
||||
|
||||
/**
|
||||
* Register the deterministic parent adapter and record whether every spawned
|
||||
* product tree was already quiet when the assembled application disposed.
|
||||
* @param ctx - Loader context supplying the LLM and subprocess seams.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.llm.registerAdapter(['mock'], new CodexDelegatingAdapter())
|
||||
ctx.effect(() => {
|
||||
const observed: ObservedProcess[] = []
|
||||
const originalSpawn = ctx.subprocess.spawn.bind(ctx.subprocess)
|
||||
ctx.subprocess.spawn = (spec: SubprocessSpawnSpec): SubprocessHandle => {
|
||||
const handle = originalSpawn(spec)
|
||||
observed.push({ spec, handle })
|
||||
return handle
|
||||
}
|
||||
return async () => {
|
||||
ctx.subprocess.spawn = originalSpawn
|
||||
const alreadyExited = AbortSignal.abort()
|
||||
const processes = await Promise.all(observed.map(async ({ spec, handle }) => ({
|
||||
argv: [...spec.argv],
|
||||
quiescent: await handle.waitForExit(alreadyExited),
|
||||
outcome: await handle.done,
|
||||
})))
|
||||
await writeFile(
|
||||
join(process.cwd(), QUIESCENCE_FILE),
|
||||
`${JSON.stringify({ processes })}\n`,
|
||||
)
|
||||
}
|
||||
}, 'codex Loader snapshot process observer')
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"stdout": {
|
||||
"type": "result",
|
||||
"success": true,
|
||||
"sessionId": "{{sessionId}}",
|
||||
"turn": 1,
|
||||
"result": "Codex child returned: REAL_CODEX_LOADER_SENTINEL_0_146_0",
|
||||
"reason": {
|
||||
"kind": "completed"
|
||||
},
|
||||
"usage": {
|
||||
"inputTokens": 20,
|
||||
"outputTokens": 61
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"path": "/v1/responses",
|
||||
"authorization": "Bearer dsh-fake-openai-loader-key",
|
||||
"taskObserved": true
|
||||
},
|
||||
"quiescence": {
|
||||
"processes": [
|
||||
{
|
||||
"argv": [
|
||||
"codex",
|
||||
"app-server",
|
||||
"--stdio"
|
||||
],
|
||||
"quiescent": true,
|
||||
"outcome": {
|
||||
"exitCode": 0,
|
||||
"signal": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Delegate through Codex once."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":0,"data":{"title":"Delegate through Codex once.","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"mock","model":"mock-delegate"},"system":"{{system}}","tools":[{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent_codex","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}}]},"reason":"initial"}}
|
||||
{"type":"request/context","seq":5,"time":0,"data":{"provider":"mock","model":"mock-delegate"}}
|
||||
{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call-codex-loader","name":"subagent_codex","argumentsDelta":"{\"description\":\"Codex Loader snapshot\",\"prompt\":\"Return the Loader snapshot sentinel exactly.\"}"}}}
|
||||
{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call-codex-loader","name":"subagent_codex","arguments":"{\"description\":\"Codex Loader snapshot\",\"prompt\":\"Return the Loader snapshot sentinel exactly.\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":11,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call-codex-loader","name":"subagent_codex","arguments":"{\"description\":\"Codex Loader snapshot\",\"prompt\":\"Return the Loader snapshot sentinel exactly.\"}"}],"source":{"kind":"model","provider":"mock","model":"mock-delegate"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[6,7,8,9,10],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":12,"time":0,"data":{"turn":1,"step":1,"callId":"call-codex-loader","name":"subagent_codex","arguments":"{\"description\":\"Codex Loader snapshot\",\"prompt\":\"Return the Loader snapshot sentinel exactly.\"}"}}
|
||||
{"type":"tool/result","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call-codex-loader"},"content":[{"type":"tool-result","toolCallId":"call-codex-loader","content":[{"type":"text","text":"REAL_CODEX_LOADER_SENTINEL_0_146_0"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[12],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":14,"time":0,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":15,"time":0,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"Codex child returned: REAL_CODEX_LOADER_SENTINEL_0_146_0"}}}
|
||||
{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Codex child returned: REAL_CODEX_LOADER_SENTINEL_0_146_0"}}}}
|
||||
{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":56}}}}
|
||||
{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"Codex child returned: REAL_CODEX_LOADER_SENTINEL_0_146_0"}],"source":{"kind":"model","provider":"mock","model":"mock-delegate"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":56}},"sourceEventSeqs":[16,17,18,19,20],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":23,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
167
examples/acp-agent/tests/subagent-product-providers.snapshot.ts
Normal file
167
examples/acp-agent/tests/subagent-product-providers.snapshot.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Real-product Loader snapshots for fixed subagent providers.
|
||||
*
|
||||
* PR1 owns the Codex scenario. PR2 extends this file with the sibling Claude
|
||||
* Code scenario and reruns both from its final stacked candidate.
|
||||
*/
|
||||
|
||||
import { dirname, delimiter, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
normalizeSessionLog,
|
||||
normalizeStdout,
|
||||
scrubSystemPrompts,
|
||||
type NormalizeContext,
|
||||
} from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import {
|
||||
LOADER_SMOKE_TEST_TIMEOUT_MS,
|
||||
runLoaderSmoke,
|
||||
} from '@deepseek-ai/dsh-loader-smoke'
|
||||
import { startResponsesFixture } from '../../../packages/subagent/subagent-codex/tests/responses-fixture.ts'
|
||||
|
||||
const testsDir = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
const fixtureDir = join(testsDir, 'fixtures/subagent/subagent-codex')
|
||||
const configPath = join(fixtureDir, 'cordis.yml')
|
||||
const snapshotDir = join(testsDir, 'snapshots/subagent-codex')
|
||||
const sessionExpected = join(snapshotDir, 'session.expected.jsonl')
|
||||
const evidenceExpected = join(snapshotDir, 'evidence.expected.json')
|
||||
const cliBin = join(repoRoot, 'packages/examples/cli-demo/src/bin.ts')
|
||||
const repoTsconfig = join(repoRoot, 'tsconfig.json')
|
||||
const codexBinDir = join(
|
||||
repoRoot,
|
||||
'packages/subagent/subagent-codex/node_modules/.bin',
|
||||
)
|
||||
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
||||
const CODEX_SENTINEL = 'REAL_CODEX_LOADER_SENTINEL_0_146_0'
|
||||
const FAKE_KEY = 'dsh-fake-openai-loader-key'
|
||||
|
||||
interface PersistedSession {
|
||||
readonly content: string
|
||||
readonly header: {
|
||||
readonly id: string
|
||||
readonly cwd: string
|
||||
}
|
||||
}
|
||||
|
||||
async function onlySession(root: string): Promise<PersistedSession> {
|
||||
const paths = (await readdir(root, { recursive: true }))
|
||||
.filter(path => path.endsWith('.jsonl'))
|
||||
expect(paths).toHaveLength(1)
|
||||
const path = paths[0]
|
||||
if (path === undefined) throw new Error('Codex Loader snapshot persisted no session')
|
||||
const content = await readFile(join(root, path), 'utf8')
|
||||
const header = JSON.parse(content.slice(0, content.indexOf('\n'))) as PersistedSession['header']
|
||||
return { content, header }
|
||||
}
|
||||
|
||||
function responseInputTexts(body: Record<string, unknown>): string[] {
|
||||
if (!Array.isArray(body.input)) return []
|
||||
return body.input.flatMap((item): string[] => {
|
||||
if (item === null || typeof item !== 'object') return []
|
||||
const content = (item as Record<string, unknown>).content
|
||||
if (!Array.isArray(content)) return []
|
||||
return content.flatMap((part): string[] => (
|
||||
part !== null
|
||||
&& typeof part === 'object'
|
||||
&& typeof (part as Record<string, unknown>).text === 'string'
|
||||
? [(part as Record<string, unknown>).text as string]
|
||||
: []
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
describe('real product subagent providers through the Loader', () => {
|
||||
it('pins the Codex tool, result, persisted Session, and process quiescence', async () => {
|
||||
const responses = await startResponsesFixture([
|
||||
{ kind: 'complete', text: CODEX_SENTINEL },
|
||||
])
|
||||
let session: PersistedSession | undefined
|
||||
let quiescence: unknown
|
||||
try {
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'Codex subagent Loader snapshot',
|
||||
tempDirPrefix: 'dsh-subagent-codex-loader-',
|
||||
binScript: cliBin,
|
||||
configPath,
|
||||
binArgs: [
|
||||
'--config',
|
||||
configPath,
|
||||
'--output-format',
|
||||
'json',
|
||||
'Delegate through Codex once.',
|
||||
],
|
||||
tsconfigPath: repoTsconfig,
|
||||
processTimeoutMs: 45_000,
|
||||
env: {
|
||||
DSH_TEST_OPENAI_API_KEY: FAKE_KEY,
|
||||
PATH: `${codexBinDir}${delimiter}${process.env.PATH ?? ''}`,
|
||||
},
|
||||
async prepare(cwd): Promise<void> {
|
||||
const codexHome = join(cwd, 'codex-home')
|
||||
await mkdir(codexHome)
|
||||
await writeFile(join(codexHome, 'config.toml'), [
|
||||
'model = "fixture-model"',
|
||||
'model_provider = "fixture"',
|
||||
'approval_policy = "on-request"',
|
||||
'sandbox_mode = "read-only"',
|
||||
'disable_response_storage = true',
|
||||
'check_for_update_on_startup = false',
|
||||
'',
|
||||
'[model_providers.fixture]',
|
||||
'name = "Fixture Responses"',
|
||||
`base_url = "${responses.baseUrl}"`,
|
||||
'env_key = "OPENAI_API_KEY"',
|
||||
'wire_api = "responses"',
|
||||
'requires_openai_auth = false',
|
||||
'',
|
||||
'[analytics]',
|
||||
'enabled = false',
|
||||
'',
|
||||
].join('\n'))
|
||||
},
|
||||
async inspect(cwd): Promise<void> {
|
||||
session = await onlySession(join(cwd, '.sessions'))
|
||||
quiescence = JSON.parse(await readFile(join(cwd, '.codex-quiescence.json'), 'utf8'))
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.stderr).toBe('')
|
||||
expect(session).toBeDefined()
|
||||
if (session === undefined) throw new Error('Codex Loader snapshot session was not inspected')
|
||||
const context: NormalizeContext = {
|
||||
sessionIds: [session.header.id],
|
||||
cwd: session.header.cwd,
|
||||
}
|
||||
const normalizedSession = scrubSystemPrompts(normalizeSessionLog(session.content, context))
|
||||
const request = responses.requests[0]
|
||||
expect(request).toBeDefined()
|
||||
if (request === undefined) throw new Error('Codex Loader snapshot made no Responses request')
|
||||
const evidence = `${JSON.stringify({
|
||||
stdout: JSON.parse(normalizeStdout(result.stdout, context)) as unknown,
|
||||
request: {
|
||||
method: request.method,
|
||||
path: request.path,
|
||||
authorization: request.headers.authorization,
|
||||
taskObserved: responseInputTexts(request.body)
|
||||
.includes('Return the Loader snapshot sentinel exactly.'),
|
||||
},
|
||||
quiescence,
|
||||
}, null, 2)}\n`
|
||||
|
||||
if (refreshing) {
|
||||
await mkdir(snapshotDir, { recursive: true })
|
||||
await Promise.all([
|
||||
writeFile(sessionExpected, normalizedSession),
|
||||
writeFile(evidenceExpected, evidence),
|
||||
])
|
||||
}
|
||||
expect(normalizedSession).toBe(await readFile(sessionExpected, 'utf8'))
|
||||
expect(evidence).toBe(await readFile(evidenceExpected, 'utf8'))
|
||||
} finally {
|
||||
await responses.close()
|
||||
}
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS + 30_000)
|
||||
})
|
||||
@@ -63,9 +63,11 @@
|
||||
"@deepseek-ai/dsh-spill-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-acp": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-codex": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-dsh-sdk": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-fork": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-spawn": "workspace:*",
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:*",
|
||||
"@deepseek-ai/dsh-subprocess-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:*",
|
||||
"@deepseek-ai/dsh-tasks-local": "workspace:*",
|
||||
|
||||
Reference in New Issue
Block a user