refactor: align instruction and job helper symbols
This commit is contained in:
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,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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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` 在存在挂起等待方时把任务标为已报告,解析每个等待方,记录终止快照,宣布可见集变更,然后通知完成监听器。挂起的等待会在监听器运行前把任务标为已报告,因此完成报告方不会重复通知;销毁时的取消出于同样理由标记——面向正在被销毁的所有者的通知不会有人读到。
|
||||
|
||||
### 销毁
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}))
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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') }
|
||||
}
|
||||
|
||||
@@ -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()`.',
|
||||
|
||||
Reference in new issue
Block a user