refactor: align instruction and job helper symbols

This commit is contained in:
Tianyi Cui committed 2026-09-12 20:20:02 +08:00
1 parent 739285c813
commit 6c3c3064c3
22 files changed
+337 -337

No files matched your search

@@ -23,9 +23,9 @@ import Sandbox from '@deepseek-ai/dsh-sandbox-local'
import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as WorkspaceContext from '@deepseek-ai/dsh-agent-instructions'
import * as AgentInstructions from '@deepseek-ai/dsh-agent-instructions'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
@@ -80,7 +80,7 @@ async function workspacePtcModeHarness(): Promise<Context> {
await harness.plugin(AgentRegistry)
await harness.plugin(LocalFileSystem, { cwd: '/' })
await harness.plugin(ToolFs)
await harness.plugin(WorkspaceContext, { maxBytes: 65536 })
await harness.plugin(AgentInstructions, { maxBytes: 65536 })
await harness.plugin(AgentLoop, { agents: [] })
await harness.plugin(LlmDeepSeek, { models: [{ id: 'deepseek-v4-flash' }] })
await mountRuntime(harness)
@@ -140,7 +140,7 @@ async function typedPtcModeHarness(): Promise<Context> {
async function backgroundPtcModeHarness(cwd: string): Promise<Context> {
const harness = await typedPtcModeHarness()
await harness.plugin(LocalJobRegistry)
await harness.plugin(ToolTasks, {})
await harness.plugin(ToolJobs, {})
if (harness.get('subprocess') === undefined) await harness.plugin(LocalSubprocessRuntime)
await harness.plugin(BashEnvPlugin)
await harness.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 })
@@ -25,7 +25,7 @@ import {
} from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import { logPath } from '../../../../../../packages/session/session-persistence-jsonl/src/format.ts'
import { renderWorkspaceContext } from '@deepseek-ai/dsh-agent-instructions'
import { renderAgentInstructions } from '@deepseek-ai/dsh-agent-instructions'
import { resolveConfig, workspaceBaselineIdentity } from '@deepseek-ai/dsh-agent-instructions/src/config.ts'
import { describe, expect, it } from 'vitest'
@@ -73,7 +73,7 @@ async function seedVisibleBaseline(
delegationDepth: 0,
}
const files = options.files ?? [{ name: 'AGENTS.md', content: oldInstruction }]
const baseline = renderWorkspaceContext(files.map(file => ({
const baseline = renderAgentInstructions(files.map(file => ({
absolutePath: join(cwd, file.name),
displayPath: file.name,
content: file.content,
@@ -14,8 +14,8 @@ import { resolveConfig, resolveDiscoveryConfig, type ResolvedConfig } from './co
import { trimmedInstructionDigest } from './digest.ts'
import {
decodeScopeKey,
renderWorkspaceInstructionSet,
type RenderedWorkspaceContext,
renderAgentInstructionSet,
type RenderedAgentInstructions,
USER_GLOBAL_DIRECTORY,
USER_GLOBAL_FILE,
} from './render.ts'
@@ -64,7 +64,7 @@ interface LoadOptions extends DiscoverOptions {
/** Rendered baseline plus the successfully read and byte-budget-retained files. */
export interface RenderedInstructionSet {
rendered: RenderedWorkspaceContext
rendered: RenderedAgentInstructions
/** Successfully read candidates before content deduplication and byte budgeting. */
observed: LoadedInstructionFile[]
/** Candidates retained by content deduplication and byte budgeting. */
@@ -401,7 +401,7 @@ export function dedupInstructionFilesByDirectory(files: LoadedInstructionFile[])
export async function loadBaselineInstructions(
options: LoadOptions,
fileSystem?: FileSystem,
): Promise<RenderedWorkspaceContext | undefined> {
): Promise<RenderedAgentInstructions | undefined> {
return (await loadBaselineInstructionSet(options, fileSystem))?.rendered
}
@@ -434,7 +434,7 @@ export async function loadBaselineInstructionSet(
const deduped = dedupInstructionFilesByDirectory(loaded)
if (deduped.length === 0) {
if (options.replacePreviousBaseline !== true) return undefined
const { rendered, included } = renderWorkspaceInstructionSet([], {
const { rendered, included } = renderAgentInstructionSet([], {
maxBytes: config.maxBytes,
replacePreviousBaseline: true,
})
@@ -444,7 +444,7 @@ export async function loadBaselineInstructionSet(
included,
}
}
const { rendered, included } = renderWorkspaceInstructionSet(deduped, {
const { rendered, included } = renderAgentInstructionSet(deduped, {
maxBytes: config.maxBytes,
...options.replacePreviousBaseline === undefined
? {}
@@ -23,7 +23,7 @@ import {
baselineInstructionState,
name,
reconcileInstructionContext,
workspaceContextMessage,
agentInstructionsMessage,
type InstructionVersionCache,
type AgentInstructionSource,
} from './state.ts'
@@ -40,8 +40,8 @@ export type {
InstructionFile,
LoadedInstructionFile,
} from './files.ts'
export { renderWorkspaceContext } from './render.ts'
export type { RenderedWorkspaceContext, TruncatedInstruction } from './render.ts'
export { renderAgentInstructions } from './render.ts'
export type { RenderedAgentInstructions, TruncatedInstruction } from './render.ts'
function visibleBaselineSource(
agent: Agent,
@@ -62,7 +62,7 @@ function visibleBaselineSource(
return undefined
}
function isWorkspaceContext(message: UserMessage): boolean {
function isAgentInstructionsMessage(message: UserMessage): boolean {
return message.source.kind === 'agent-instructions'
}
@@ -162,7 +162,7 @@ export function apply(ctx: Context, config: Config): void {
}
for (const [scope, state] of baseline.versions) versionStates?.set(scope, state)
if (!keepVisibleBaseline && instructions !== undefined && instructions.rendered.text.length > 0) {
const baselineContent = workspaceContextMessage(instructions.rendered.text).content
const baselineContent = agentInstructionsMessage(instructions.rendered.text).content
content.push(...baselineContent)
const replacementScopes = new Set(baseline.changes.keys())
const replacementRemovals = replacePreviousBaseline
@@ -225,7 +225,7 @@ export function apply(ctx: Context, config: Config): void {
}
const syncInbox = (agent: Agent, claimed: readonly UserMessage[], desired: UserMessage | undefined): void => {
const pending = agent.inbox.nextStep.filter(isWorkspaceContext)
const pending = agent.inbox.nextStep.filter(isAgentInstructionsMessage)
const alreadySupplied = desired !== undefined && (
claimed.some(message => sameContextPayload(message, desired))
|| agent.session.surface.nodes.some((seq) => {
@@ -257,7 +257,7 @@ export function apply(ctx: Context, config: Config): void {
claimed: readonly UserMessage[],
touchedPaths: readonly string[] = [],
): Promise<void> => {
const pending = agent.inbox.nextStep.filter(isWorkspaceContext)
const pending = agent.inbox.nextStep.filter(isAgentInstructionsMessage)
const desired = await compose(agent, signal, claimed, pending, touchedPaths)
signal.throwIfAborted()
syncInbox(agent, claimed, desired)
@@ -318,7 +318,7 @@ export function apply(ctx: Context, config: Config): void {
): Promise<PreStepDecision> => {
const decision = await next()
await waitForProjections(agent)
const pending = agent.inbox.nextStep.filter(isWorkspaceContext)
const pending = agent.inbox.nextStep.filter(isAgentInstructionsMessage)
const desired = await compose(agent, signal, messages, pending)
signal.throwIfAborted()
// An empty first entry owns a no-step turn; keep context pending instead
@@ -9,14 +9,14 @@ import type { InstructionFile, LoadedInstructionFile } from './files.ts'
const SYSTEM_REMINDER_OPEN = '<system-reminder>'
const SYSTEM_REMINDER_CLOSE = '</system-reminder>'
const WORKSPACE_CONTEXT_INTRO = 'The following workspace instructions may be relevant to your work. '
const AGENT_INSTRUCTIONS_INTRO = 'The following workspace instructions may be relevant to your work. '
+ 'Use them as guidance when applicable. More specific instructions take precedence over broader ones. '
+ 'They do not override system, developer, or direct user instructions.'
const REPLACEMENT_WORKSPACE_CONTEXT_INTRO = 'This complete workspace instruction baseline replaces all earlier workspace instruction baselines. '
+ WORKSPACE_CONTEXT_INTRO
const EMPTY_REPLACEMENT_WORKSPACE_CONTEXT_INTRO = 'This complete workspace instruction baseline replaces all earlier workspace instruction baselines. '
const REPLACEMENT_AGENT_INSTRUCTIONS_INTRO = 'This complete workspace instruction baseline replaces all earlier workspace instruction baselines. '
+ AGENT_INSTRUCTIONS_INTRO
const EMPTY_REPLACEMENT_AGENT_INSTRUCTIONS_INTRO = 'This complete workspace instruction baseline replaces all earlier workspace instruction baselines. '
+ 'No workspace instructions are currently active.'
const COMPACT_WORKSPACE_CONTEXT_INTRO = 'Workspace instructions were omitted or truncated to fit the configured byte budget.'
const COMPACT_AGENT_INSTRUCTIONS_INTRO = 'Workspace instructions were omitted or truncated to fit the configured byte budget.'
/** Byte-accounting record for one truncated instruction file. */
export interface TruncatedInstruction {
@@ -26,13 +26,13 @@ export interface TruncatedInstruction {
}
/** Model-facing text plus omitted and truncated source records. */
export interface RenderedWorkspaceContext {
export interface RenderedAgentInstructions {
text: string
omitted: InstructionFile[]
truncated: TruncatedInstruction[]
}
interface RenderedInstructionContext extends RenderedWorkspaceContext {
interface RenderedInstructionContext extends RenderedAgentInstructions {
/**
* Original files semantically represented by rendered section text. This is
* not the complement of `omitted`: a truncated file may be represented here
@@ -156,15 +156,15 @@ function additionalSectionText(file: LoadedInstructionFile): string {
].join('\n')
}
const BASELINE_RENDER_STYLE: RenderStyle = { intro: WORKSPACE_CONTEXT_INTRO, section: sectionText }
const BASELINE_RENDER_STYLE: RenderStyle = { intro: AGENT_INSTRUCTIONS_INTRO, section: sectionText }
function baselineRenderStyle(files: LoadedInstructionFile[], replacePreviousBaseline: boolean | undefined): RenderStyle {
if (replacePreviousBaseline !== true) return BASELINE_RENDER_STYLE
return {
...BASELINE_RENDER_STYLE,
intro: files.length === 0
? EMPTY_REPLACEMENT_WORKSPACE_CONTEXT_INTRO
: REPLACEMENT_WORKSPACE_CONTEXT_INTRO,
? EMPTY_REPLACEMENT_AGENT_INSTRUCTIONS_INTRO
: REPLACEMENT_AGENT_INSTRUCTIONS_INTRO,
}
}
@@ -299,7 +299,7 @@ function renderInstructionContext(
const omitted = files.slice(0, -1).map(file => ({ absolutePath: file.absolutePath, displayPath: file.displayPath }))
const originalBytes = byteLength(mostSpecific.content)
for (const candidateStyle of [style, { ...style, intro: COMPACT_WORKSPACE_CONTEXT_INTRO }]) {
for (const candidateStyle of [style, { ...style, intro: COMPACT_AGENT_INSTRUCTIONS_INTRO }]) {
const truncatedFile = truncateToFit(mostSpecific, [], maxBytes, omitted, candidateStyle)
const includedBytes = byteLength(truncatedFile.content)
const truncated = [{
@@ -338,10 +338,10 @@ function renderInstructionContext(
* @returns bounded public rendering plus files with surviving content, including genuinely empty files.
* @internal
*/
export function renderWorkspaceInstructionSet(
export function renderAgentInstructionSet(
files: LoadedInstructionFile[],
options: { maxBytes: number; replacePreviousBaseline?: boolean },
): { rendered: RenderedWorkspaceContext; included: LoadedInstructionFile[] } {
): { rendered: RenderedAgentInstructions; included: LoadedInstructionFile[] } {
const style = baselineRenderStyle(files, options.replacePreviousBaseline)
const { represented, ...rendered } = renderInstructionContext(files, options.maxBytes, style)
return { rendered, included: represented }
@@ -353,9 +353,9 @@ export function renderWorkspaceInstructionSet(
* @param options - rendering byte budget and whether this baseline supersedes a visible predecessor.
* @returns bounded baseline prompt text and budget diagnostics.
*/
export function renderWorkspaceContext(
export function renderAgentInstructions(
files: LoadedInstructionFile[],
options: { maxBytes: number; replacePreviousBaseline?: boolean },
): RenderedWorkspaceContext {
return renderWorkspaceInstructionSet(files, options).rendered
): RenderedAgentInstructions {
return renderAgentInstructionSet(files, options).rendered
}
@@ -78,7 +78,7 @@ export interface ReconciledInstructionContext {
versionUpdates: InstructionVersionUpdate[]
}
function workspaceContextHook(text: string, changes: AgentInstructionChange[]): UserMessage {
function agentInstructionsHook(text: string, changes: AgentInstructionChange[]): UserMessage {
return createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'agent-instructions', form: 'instructions', changes },
@@ -90,14 +90,14 @@ function workspaceContextHook(text: string, changes: AgentInstructionChange[]):
* @param text - complete plugin-owned system-reminder text.
* @returns a user-role prefix message.
*/
export function workspaceContextMessage(text: string): Message {
export function agentInstructionsMessage(text: string): Message {
return createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: name },
})
}
function isWorkspaceContextSource(
function isAgentInstructionsSource(
source: unknown,
): source is { kind: 'agent-instructions'; changes: unknown[] } {
return typeof source === 'object' && source !== null
@@ -141,14 +141,14 @@ function visibleInstructionChanges(
for (const seq of agent.session.surface.nodes) {
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
const event = agent.session.eventAt(seq)
if (event?.type !== 'user/message' || !isWorkspaceContextSource(event.data.source)) continue
if (event?.type !== 'user/message' || !isAgentInstructionsSource(event.data.source)) continue
const changes = workspaceInstructionChanges(event.data.source)
for (const change of changes) {
visible.set(change.scope, change)
}
}
for (const message of authorityMessages) {
if (!isWorkspaceContextSource(message.source)) continue
if (!isAgentInstructionsSource(message.source)) continue
for (const change of workspaceInstructionChanges(message.source)) {
visible.set(change.scope, change)
}
@@ -283,7 +283,7 @@ export async function reconcileInstructionContext(
}
for (const message of options.scopeMessages) {
/* v8 ignore next -- the plugin passes its workspace-only pending projection. */
if (!isWorkspaceContextSource(message.source)) continue
if (!isAgentInstructionsSource(message.source)) continue
for (const change of workspaceInstructionChanges(message.source)) {
if (!options.includeBaselineScopes && baselineScopes.has(change.scope)) continue
scopes.add(change.scope)
@@ -428,7 +428,7 @@ export async function reconcileInstructionContext(
// next pass retry instead of spamming notice-only contexts.
if (rendered.text.length === 0 || rendered.changes.length === 0) return undefined
return {
context: workspaceContextHook(rendered.text, rendered.changes),
context: agentInstructionsHook(rendered.text, rendered.changes),
versionUpdates: retainedInstructionVersionUpdates(versionUpdates, rendered.changes),
}
}
@@ -9,7 +9,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import * as WorkspaceContext from '@deepseek-ai/dsh-agent-instructions'
import * as AgentInstructions from '@deepseek-ai/dsh-agent-instructions'
import { candidateScopeKey } from '../src/render.ts'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
@@ -39,7 +39,7 @@ async function harness(): Promise<{ ctx: Context; agent: Agent }> {
})
await ctx.plugin(LocalFileSystem, { cwd: '/' })
await ctx.plugin(ToolFs)
await ctx.plugin(WorkspaceContext, { maxBytes: 65536 })
await ctx.plugin(AgentInstructions, { maxBytes: 65536 })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LlmDeepSeek, { models: [{ id: 'deepseek-v4-flash' }] })
const handle = await ctx.agents.create({
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/jobs/jobs-local/README.md
README.md: 89c0f2018fd2cf26f9dc130e79ca6a568bc34b9e
README.zh.md: c8e5568dec32317c4c3e2da5a03fcd3cdaaf1067
README.md: 51fbffc4892f6a4781343e13863770fb4e8e8e6a
README.zh.md: 710c02c923994df596e544345cac4cd4d0bd2809
+2 -2
View File
@@ -69,7 +69,7 @@ This section explains the design decisions behind the registry and points at the
### Design philosophy
- **In-memory records, fresh snapshots.** `LocalJobRegistry` keeps one `TrackedTask` per job and projects a new read-only snapshot per call; callers never receive live state.
- **In-memory records, fresh snapshots.** `LocalJobRegistry` keeps one `TrackedJob` per job and projects a new read-only snapshot per call; callers never receive live state.
- **Owner-relative layers, one process-wide registry.** Controllers, completion listeners, and change observers are filed into the scope that registered them (`ScopedLayers`), and reads union the global layer with the owner's scope chain — so one preset's job controls never hold `start()` open for an agent whose own composition loads none, and a settlement reaches only the listeners its owner's composition registered.
- **Preflight before start.** `start()` checks controller service, spec validity, live ownership, and capacity before invoking the producer, so a rejection leaves no job id or execution resource; registration commits without a later failable step.
- **First-wins settlement, completion last.** The earliest terminal outcome records once, releases waiters, and notifies listeners once with per-listener containment; completion is announced after the record is committed and the visible-set change published, because a reporter may open a model turn synchronously.
@@ -88,7 +88,7 @@ This section explains the design decisions behind the registry and points at the
### Admission and settlement
`activeTaskCount` counts authoritative records per exact owner or in the shared unowned bucket. `settle` marks a job reported when waiters are pending, resolves every waiter, records the terminal snapshot, announces the visible-set change, then notifies completion listeners. Pending waits mark the job reported before listeners run so completion reporters do not duplicate notices; a teardown cancel marks it for the same reason — nothing will read a notice addressed to an owner being destroyed.
`activeJobCount` counts authoritative records per exact owner or in the shared unowned bucket. `settle` marks a job reported when waiters are pending, resolves every waiter, records the terminal snapshot, announces the visible-set change, then notifies completion listeners. Pending waits mark the job reported before listeners run so completion reporters do not duplicate notices; a teardown cancel marks it for the same reason — nothing will read a notice addressed to an owner being destroyed.
### Teardown
+2 -2
View File
@@ -69,7 +69,7 @@ kind: "package-reference"
### 设计理念
- **内存记录,全新快照。** `LocalJobRegistry` 为每个任务保存一条 `TrackedTask`,每次调用都投影出新的只读快照;调用方永远不会拿到实时状态。
- **内存记录,全新快照。** `LocalJobRegistry` 为每个任务保存一条 `TrackedJob`,每次调用都投影出新的只读快照;调用方永远不会拿到实时状态。
- **按所有者分层,一个进程级注册表。** 控制器、完成监听器与变更观察者归档到注册方所在的 scope(`ScopedLayers`),读取把全局层与所有者的 scope 链求并集——因此某个 preset 的任务控制绝不会为自身组合未加载任何控制器的 agent 保持 `start()` 可用,一次结算也只会抵达其所有者所属组合注册的监听器。
- **启动前先预检。** `start()` 在调用生产方之前检查控制器服务、spec 有效性、仍存活的所有权与容量,因此拒绝不会留下 job id 或执行资源;注册一旦提交,后续不再有可失败步骤。
- **结算首次优先,完成最后。** 最早的终止结果只记录一次,释放等待方,并只通知监听器一次,各监听器故障单独隔离;完成在记录提交且可见集变更发布之后才宣布,因为报告方可能同步开启一个模型轮次。
@@ -88,7 +88,7 @@ kind: "package-reference"
### 准入与结算
`activeTaskCount` 按精确所有者或共享无主桶统计权威记录。`settle` 在存在挂起等待方时把任务标为已报告,解析每个等待方,记录终止快照,宣布可见集变更,然后通知完成监听器。挂起的等待会在监听器运行前把任务标为已报告,因此完成报告方不会重复通知;销毁时的取消出于同样理由标记——面向正在被销毁的所有者的通知不会有人读到。
`activeJobCount` 按精确所有者或共享无主桶统计权威记录。`settle` 在存在挂起等待方时把任务标为已报告,解析每个等待方,记录终止快照,宣布可见集变更,然后通知完成监听器。挂起的等待会在监听器运行前把任务标为已报告,因此完成报告方不会重复通知;销毁时的取消出于同样理由标记——面向正在被销毁的所有者的通知不会有人读到。
### 销毁
+12 -12
View File
@@ -25,7 +25,7 @@ import type {
export const TASK_WAIT_TIMEOUT = 'TASK_WAIT_TIMEOUT'
/** Default maximum number of active jobs in one exact-owner bucket. */
const DEFAULT_MAX_CONCURRENT_TASKS_PER_OWNER = 10
const DEFAULT_MAX_CONCURRENT_JOBS_PER_OWNER = 10
/** Configuration for the process-local job registry. */
export interface Config {
@@ -37,7 +37,7 @@ export interface Config {
}
/** The registry's mutable per-job record (never handed out — see {@link LocalJobRegistry.snapshot}). */
interface TrackedTask {
interface TrackedJob {
id: JobId
kind: JobKind
label: string
@@ -94,12 +94,12 @@ export class LocalJobRegistry extends JobRegistry {
.step(1)
.min(1)
.max(Number.MAX_SAFE_INTEGER)
.default(DEFAULT_MAX_CONCURRENT_TASKS_PER_OWNER),
.default(DEFAULT_MAX_CONCURRENT_JOBS_PER_OWNER),
})
/** Schemastery-defaulted active-job limit. */
private readonly maxConcurrentJobsPerOwner: number
private store = new Map<JobId, TrackedTask>()
private store = new Map<JobId, TrackedJob>()
private counters = new Map<string, number>()
/**
* Surfaces and listeners layered by the scope that registered them, in the
@@ -140,7 +140,7 @@ export class LocalJobRegistry extends JobRegistry {
}
if (spec.owner !== undefined) this.ensureOwnerCleanup(spec.owner)
const active = this.activeTaskCount(spec.owner)
const active = this.activeJobCount(spec.owner)
if (active >= this.maxConcurrentJobsPerOwner) {
throw new Error(
`background job limit reached for this owner (limit: ${this.maxConcurrentJobsPerOwner}); use job_kill to stop an unneeded job, wait for it to finish, then retry`,
@@ -154,7 +154,7 @@ export class LocalJobRegistry extends JobRegistry {
let markSettled!: () => void
const settled = new Promise<void>((resolve) => { markSettled = resolve })
const job: TrackedTask = {
const job: TrackedJob = {
id,
kind: spec.kind,
label: spec.label,
@@ -319,7 +319,7 @@ export class LocalJobRegistry extends JobRegistry {
}
/** Count authoritative active records for one exact owner or the shared unowned bucket. */
private activeTaskCount(owner: Agent | undefined): number {
private activeJobCount(owner: Agent | undefined): number {
let count = 0
for (const job of this.store.values()) {
if (job.owner === owner && (job.status === 'running' || job.status === 'stopping')) count += 1
@@ -342,7 +342,7 @@ export class LocalJobRegistry extends JobRegistry {
}
/** Look up a job or fail loud. */
private expect(id: JobId): TrackedTask {
private expect(id: JobId): TrackedJob {
const job = this.store.get(id)
if (job === undefined) throw new Error(`unknown job ${id}`)
return job
@@ -353,14 +353,14 @@ export class LocalJobRegistry extends JobRegistry {
* whose session id matches (`!== undefined` semantics — an unowned job is
* open, and a no-agent caller can never match an owned one).
*/
private assertAccess(job: TrackedTask, caller?: Agent): void {
private assertAccess(job: TrackedJob, caller?: Agent): void {
if (job.owner !== undefined && job.owner.id !== caller?.id) {
throw new Error(`job ${job.id} belongs to another session`)
}
}
/** Project a fresh read-only snapshot from the mutable record. */
private snapshot(job: TrackedTask): JobSnapshot {
private snapshot(job: TrackedJob): JobSnapshot {
const ownerSession = job.owner?.id
return {
id: job.id,
@@ -413,7 +413,7 @@ export class LocalJobRegistry extends JobRegistry {
* synchronously: every other observer of this settlement must already have
* seen the committed record.
*/
private settle(job: TrackedTask, outcome: JobOutcome): void {
private settle(job: TrackedJob, outcome: JobOutcome): void {
if (isTerminal(job.status)) return
job.status = outcome.status
job.detail = outcome.detail
@@ -504,7 +504,7 @@ export class LocalJobRegistry extends JobRegistry {
* force-fails the record and reports a possible orphan; a cancel that returns
* without settling remains indistinguishable from a slow stop and may stall.
*/
private cancelForTeardown(jobs: TrackedTask[], reason: string): void {
private cancelForTeardown(jobs: TrackedJob[], reason: string): void {
for (const job of jobs) {
if (isTerminal(job.status)) continue
// Teardown cancellation is a kill without a caller, so it claims the
+11 -11
View File
@@ -63,7 +63,7 @@ export interface PublicJobSnapshot {
}
/** Shared schema for job-control outputs. */
const PUBLIC_TASK_SCHEMA = {
const PUBLIC_JOB_SCHEMA = {
type: 'object',
additionalProperties: false,
properties: {
@@ -197,7 +197,7 @@ function validateJobId(value: string): JobId {
}
/** Pending presentation shared by the three generic job controls. */
function presentTaskCall(title: string, kind: 'read' | 'execute', rawInput?: string): GenericCallView {
function presentJobCall(title: string, kind: 'read' | 'execute', rawInput?: string): GenericCallView {
return { card: 'generic', title, kind, ...rawInput !== undefined ? { rawInput } : {} }
}
@@ -234,7 +234,7 @@ export function apply(ctx: Context, config: Config): void {
if (maxBytes !== undefined) outputLimits.set(exec, maxBytes)
return next()
}, { prepend: true })
const finalizeTaskContent: NonNullable<ToolDefinition['finalizeContent']> = (exec, result) => {
const finalizeJobContent: NonNullable<ToolDefinition['finalizeContent']> = (exec, result) => {
const maxBytes = outputLimits.get(exec) ?? visibleOutputLimit(ctx, exec)
outputLimits.delete(exec)
if (maxBytes === undefined) return undefined
@@ -310,14 +310,14 @@ export function apply(ctx: Context, config: Config): void {
wait: { type: 'boolean', description: 'Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job 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.' },
},
finalizeContent: finalizeTaskContent,
finalizeContent: finalizeJobContent,
output: {
schema: {
type: 'object',
additionalProperties: false,
properties: {
text: { type: 'string', required: true },
job: { ...PUBLIC_TASK_SCHEMA, required: true },
job: { ...PUBLIC_JOB_SCHEMA, required: true },
},
},
render: (_args, value) => {
@@ -335,7 +335,7 @@ export function apply(ctx: Context, config: Config): void {
const read = ctx.jobs.read(id, exec.agent)
return { text: read.text, job: publicJob(read.snapshot) }
},
presentCall: args => presentTaskCall(`Read output from background job ${args.job_id}`, 'read', args.job_id),
presentCall: args => presentJobCall(`Read output from background job ${args.job_id}`, 'read', args.job_id),
}))
ctx.tools.register(defineTool({
@@ -343,7 +343,7 @@ export function apply(ctx: Context, config: Config): void {
description: 'List your background jobs (running and finished) with their ids, kinds, and statuses.',
parameters: {},
output: {
schema: { type: 'array', items: PUBLIC_TASK_SCHEMA },
schema: { type: 'array', items: PUBLIC_JOB_SCHEMA },
render: (_args, jobs) => [{
type: 'text',
text: jobs.length === 0
@@ -355,7 +355,7 @@ export function apply(ctx: Context, config: Config): void {
const jobs = ctx.jobs.list(exec.agent)
return Promise.resolve(jobs.map(publicJob))
},
presentCall: () => presentTaskCall('List background jobs', 'read'),
presentCall: () => presentJobCall('List background jobs', 'read'),
}))
ctx.tools.register(defineTool({
@@ -365,7 +365,7 @@ export function apply(ctx: Context, config: Config): void {
job_id: { type: 'string', required: true, description: 'Job 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 job.' },
},
finalizeContent: finalizeTaskContent,
finalizeContent: finalizeJobContent,
output: {
schema: {
type: 'object',
@@ -376,7 +376,7 @@ export function apply(ctx: Context, config: Config): void {
required: true,
enum: ['cancellation-requested', 'already-finished'],
},
job: { ...PUBLIC_TASK_SCHEMA, required: true },
job: { ...PUBLIC_JOB_SCHEMA, required: true },
},
},
render: (_args, value) => [{
@@ -396,6 +396,6 @@ export function apply(ctx: Context, config: Config): void {
job: snapshot,
})
},
presentCall: args => presentTaskCall(`Kill background job ${args.job_id}`, 'execute', args.job_id),
presentCall: args => presentJobCall(`Kill background job ${args.job_id}`, 'execute', args.job_id),
}))
}
+12 -12
View File
@@ -11,7 +11,7 @@ import { bindScopeParent, createScope, scopeOf } from '@deepseek-ai/dsh-scope'
import { JobId } from '@deepseek-ai/dsh-jobs'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import type { JobHooks, JobOutcome, JobSnapshot, JobStart } from '@deepseek-ai/dsh-jobs'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import { statusLine } from '@deepseek-ai/dsh-tool-jobs'
const testToolSignal = new AbortController().signal
@@ -19,13 +19,13 @@ const testToolSignal = new AbortController().signal
const agentRegistryDisposers = new WeakMap<Agent, () => void>()
const agentScopeFibers = new WeakMap<Agent, { dispose: () => Promise<void> }>()
async function setup(config: ToolTasks.Config = {}) {
async function setup(config: ToolJobs.Config = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
const agentsFiber = await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalJobRegistry)
const toolsFiber = await ctx.plugin(ToolTasks, config)
const toolsFiber = await ctx.plugin(ToolJobs, config)
return { ctx, agentsFiber, toolsFiber }
}
@@ -124,15 +124,15 @@ describe('tool-jobs setup', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalJobRegistry)
await expect(ctx.plugin(ToolTasks, { waitTimeoutMs: 100, maxWaitTimeoutMs: 50 }))
await expect(ctx.plugin(ToolJobs, { waitTimeoutMs: 100, maxWaitTimeoutMs: 50 }))
.rejects.toThrow('waitTimeoutMs (100) exceeds maxWaitTimeoutMs (50)')
})
it('defaults delivery to wakeup and rejects an unknown lane', () => {
expect(ToolTasks.Config({}).completionDelivery).toBe('wakeup')
expect(ToolTasks.Config({}).maxConsecutiveWakes).toBe(3)
expect(() => ToolTasks.Config({ completionDelivery: 'loud' as never })).toThrow()
expect(() => ToolTasks.Config({ maxConsecutiveWakes: 0 })).toThrow()
expect(ToolJobs.Config({}).completionDelivery).toBe('wakeup')
expect(ToolJobs.Config({}).maxConsecutiveWakes).toBe(3)
expect(() => ToolJobs.Config({ completionDelivery: 'loud' as never })).toThrow()
expect(() => ToolJobs.Config({ maxConsecutiveWakes: 0 })).toThrow()
})
it('rejects a wake budget that cannot bound anything', async () => {
@@ -143,7 +143,7 @@ describe('tool-jobs setup', () => {
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalJobRegistry)
try {
await ctx.plugin(ToolTasks, { maxConsecutiveWakes })
await ctx.plugin(ToolJobs, { maxConsecutiveWakes })
return 'loaded'
} catch (error: unknown) {
return String(error)
@@ -170,7 +170,7 @@ describe('tool-jobs setup', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalJobRegistry)
ToolTasks.apply(ctx, {})
ToolJobs.apply(ctx, {})
expect(ctx.tools.get('job_output')).toBeDefined()
expect(() => ctx.jobs.start(producer().spec)).not.toThrow()
})
@@ -521,8 +521,8 @@ describe('completion notices across scoped mounts', () => {
const standingA = createScope(ctx, {})
const standingB = createScope(ctx, {})
await standingA.ctx.plugin(ToolTasks)
await standingB.ctx.plugin(ToolTasks)
await standingA.ctx.plugin(ToolJobs)
await standingB.ctx.plugin(ToolJobs)
// The agent joins preset A exactly as `agentPresets.compose` binds it.
const agentKey = {}
@@ -10,7 +10,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
@@ -31,7 +31,7 @@ async function harness(adapter: MockAdapter, sessionRoot?: string, dshHome?: str
}
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(BashEnvPlugin, dshHome === undefined ? {} : { dshHome })
await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 })
+11 -11
View File
@@ -13,7 +13,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
import { SessionId } from '@deepseek-ai/dsh-session'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import ApprovalService from '@deepseek-ai/dsh-user-approval'
import type { ApprovalOutcome } from '@deepseek-ai/dsh-user-approval'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
@@ -48,13 +48,13 @@ async function setup() {
}
/** Full harness: the generic job runtime + its controller, then the bash tool. */
async function setupWithTasks() {
async function setupWithJobs() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
await ctx.plugin(LocalSubprocessRuntime)
;(ctx.subprocess as LocalSubprocessRuntime).internals = { spillDir }
await ctx.plugin(BashEnvPlugin)
@@ -192,7 +192,7 @@ async function setupSandboxed(withApproval = false) {
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
await ctx.plugin(SessionProjectionRegistry)
ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
await ctx.plugin(SandboxPolicyService, {})
@@ -456,7 +456,7 @@ describe('bash tool', () => {
describe('background execution through the job runtime', () => {
it('run_in_background acks with the job id, readable through the REAL job_output tool', async () => {
const ctx = await setupWithTasks()
const ctx = await setupWithJobs()
const started = await call(ctx, 'bash', { command: 'echo bg-ok', description: 'test command', run_in_background: true })
expect(started.isError).toBe(false)
if (started.isError) throw new Error('expected background bash success')
@@ -471,7 +471,7 @@ describe('background execution through the job runtime', () => {
})
it('a running background job is killable through the REAL job_kill tool', async () => {
const ctx = await setupWithTasks()
const ctx = await setupWithJobs()
await call(ctx, 'bash', { command: 'sleep 60', description: 'test command', run_in_background: true })
const killed = await call(ctx, 'job_kill', { job_id: 'bash-1' })
@@ -483,7 +483,7 @@ describe('background execution through the job runtime', () => {
})
it('a self-signal background exit is reported as killed through the REAL job_output tool', async () => {
const ctx = await setupWithTasks()
const ctx = await setupWithJobs()
await call(ctx, 'bash', { command: 'kill -TERM $$', description: 'test command', run_in_background: true })
const final = await call(ctx, 'job_output', { job_id: 'bash-1', wait: true })
@@ -492,7 +492,7 @@ describe('background execution through the job runtime', () => {
it('a background job started by an agent is registered with that agent as owner', async () => {
// The producer must forward exec.agent as the job owner.
const ctx = await setupWithTasks()
const ctx = await setupWithJobs()
const agent = registerFakeAgent(ctx, 'sess-owner')
const started = await call(ctx, 'bash', { command: 'sleep 60', description: 'test command', run_in_background: true }, agent)
expect(text(started)).toBe('started background job bash-1')
@@ -507,7 +507,7 @@ describe('background execution through the job runtime', () => {
})
it('fails loud when the job runtime is not loaded', async () => {
const ctx = await setup() // no LocalJobRegistry / ToolTasks
const ctx = await setup() // no LocalJobRegistry / ToolJobs
const result = await call(ctx, 'bash', { command: 'sleep 60', description: 'test command', run_in_background: true })
expect(result.isError).toBe(true)
expect(text(result)).toContain('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs')
@@ -519,7 +519,7 @@ describe('background execution through the job runtime', () => {
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
await ctx.plugin(CountingStartExecutor)
await ctx.plugin(BashEnvPlugin)
await ctx.plugin(ToolBash)
@@ -1125,7 +1125,7 @@ describe('the model-facing bash tool builds its request from named args only (no
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
await ctx.plugin(BashEnvPlugin, { dshHome: recordingDshHome })
await ctx.plugin(RecordingBashExecutor)
await ctx.plugin(ToolBash)
@@ -19,7 +19,7 @@ import { ToolCallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { TOOL_ABORTED } from '@deepseek-ai/dsh-tools'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { PwshLocalExecutor, resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
import * as ToolPwsh from '@deepseek-ai/dsh-tool-pwsh'
@@ -61,7 +61,7 @@ describe.skipIf(!hasPwsh)('pwsh tool over the real pwsh executor', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(BashEnvPlugin)
await ctx.plugin(PwshLocalExecutor, { timeoutMs: 20_000, graceMs: 200 })
+9 -9
View File
@@ -19,7 +19,7 @@ import { ToolCallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime, { TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
@@ -148,13 +148,13 @@ async function setup(toolConfig: Partial<ToolPwsh.Config> = {}, dshHome?: string
}
/** Full harness: the generic job runtime + its controller, then the pwsh tool. */
async function setupWithTasks(toolConfig: Partial<ToolPwsh.Config> = {}, dshHome?: string) {
async function setupWithJobs(toolConfig: Partial<ToolPwsh.Config> = {}, dshHome?: string) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
await ctx.plugin(BashEnvPlugin, dshHome === undefined ? {} : { dshHome })
await ctx.plugin(FakeBash)
await ctx.plugin(ToolPwsh, toolConfig)
@@ -216,7 +216,7 @@ async function setupSandboxed(withApproval = false) {
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
await ctx.plugin(BashEnvPlugin)
await ctx.plugin(SessionProjectionRegistry)
// The loop's turnBoundary unit (the open-turn fold) is not mounted in this
@@ -740,7 +740,7 @@ describe('sandbox escalation through ctx.approval', () => {
describe('background execution through the job runtime', () => {
it('run_in_background acks with the job id, readable through the REAL job_output tool', async () => {
const { ctx } = await setupWithTasks()
const { ctx } = await setupWithJobs()
const started = await call(ctx, 'pwsh', { command: 'Write-Output bg-ok', description: 'test command', run_in_background: true })
expect(started.isError).toBe(false)
if (started.isError) throw new Error('expected background pwsh success')
@@ -755,7 +755,7 @@ describe('background execution through the job runtime', () => {
})
it('a running background job is killable through the REAL job_kill tool', async () => {
const { ctx, bash } = await setupWithTasks()
const { ctx, bash } = await setupWithJobs()
bash.backgroundHandler = () => killableProcess()
await call(ctx, 'pwsh', { command: 'Start-Sleep -Seconds 60', description: 'test command', run_in_background: true })
@@ -768,7 +768,7 @@ describe('background execution through the job runtime', () => {
})
it('a background job started by an agent is registered with that agent as owner', async () => {
const { ctx } = await setupWithTasks()
const { ctx } = await setupWithJobs()
const agent = registerFakeAgent(ctx, 'sess-owner')
const started = await call(ctx, 'pwsh', { command: 'Start-Sleep -Seconds 60', description: 'test command', run_in_background: true }, agent)
expect(text(started)).toBe('started background job pwsh-1')
@@ -783,14 +783,14 @@ describe('background execution through the job runtime', () => {
})
it('fails loud when the job runtime is not loaded', async () => {
const { ctx } = await setup() // no LocalJobRegistry / ToolTasks
const { ctx } = await setup() // no LocalJobRegistry / ToolJobs
const result = await call(ctx, 'pwsh', { command: 'Start-Sleep -Seconds 60', description: 'test command', run_in_background: true })
expect(result.isError).toBe(true)
expect(text(result)).toContain('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs')
})
it('a pre-aborted call is skipped before the process starts', async () => {
const { ctx, bash } = await setupWithTasks()
const { ctx, bash } = await setupWithJobs()
const controller = new AbortController()
controller.abort()
const result = await ctx.tools.execute({
@@ -17,7 +17,7 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { loadStoredSession } from '../../subagent/tests/persistence-helpers.ts'
import * as mock from './scripted-provider.ts'
@@ -819,7 +819,7 @@ describe('dsh-tool-subagent background mode', () => {
const ctx = await setup(toolConfig, mockConfig)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks, {})
await ctx.plugin(ToolJobs, {})
return ctx
}
@@ -1191,7 +1191,7 @@ describe('dsh-tool-subagent continuable background mode', () => {
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks, {})
await ctx.plugin(ToolJobs, {})
await ctx.plugin(tool, { provider: 'spawn', backgroundMode: 'continuable' })
ctx.llm.registerAdapter(['mock'], new MockAdapter([
textResponse('continuable answer'),
+2 -2
View File
@@ -105,7 +105,7 @@ const SESSION_SNAPSHOT_SCHEMA = {
properties: SESSION_SNAPSHOT_PROPERTIES,
} as const
const BACKGROUND_TASK_OUTPUT_SCHEMA = {
const BACKGROUND_JOB_OUTPUT_SCHEMA = {
type: 'object',
additionalProperties: false,
properties: {
@@ -210,7 +210,7 @@ export function apply(ctx: Context, config: Config = {}): void {
output: {
schema: {
oneOf: [
BACKGROUND_TASK_OUTPUT_SCHEMA,
BACKGROUND_JOB_OUTPUT_SCHEMA,
{
type: 'object',
additionalProperties: false,
@@ -10,7 +10,7 @@ import type { ToolSdkSchema } from '@deepseek-ai/dsh-tools/src/ts-types.ts'
import TerminalSessionService, { TerminalSessionId } from '@deepseek-ai/dsh-terminal'
import type { TerminalBackend, TerminalBackendSession, TerminalSendOperation, TerminalSendRequest, TerminalSessionStatus, TerminalSignal } from '@deepseek-ai/dsh-terminal'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import * as ToolPty from '@deepseek-ai/dsh-tool-terminal'
import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
@@ -114,7 +114,7 @@ async function setupBase(jobs: boolean) {
ctx.terminals.registerBackend(stub.backend)
if (jobs) {
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
}
return { ctx, stub, agent: fakeAgent(ctx, jobs ? 'with-tasks' : 'foreground') }
}
+2 -2
View File
@@ -58,7 +58,7 @@ import Lsp from '@deepseek-ai/dsh-lsp'
import * as ToolLsp from '@deepseek-ai/dsh-tool-lsp'
import * as ToolSkill from '@deepseek-ai/dsh-tool-skill'
import * as ToolSessionQuery from '@deepseek-ai/dsh-tool-session-query'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import type TeamService from '@deepseek-ai/dsh-experimental-agent-team'
import * as ToolTeam from '@deepseek-ai/dsh-experimental-tool-agent-team'
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
@@ -515,7 +515,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
writes: ['tool/call', 'tool/result', 'user/message via agent.inject() for background completion notices'],
async mount(ctx) {
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks)
await ctx.plugin(ToolJobs)
},
note:
'The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers\' `ctx.jobs.start()`.',