feat(workflow): add fresh-agent Ralph tool
This commit is contained in:
@@ -39,7 +39,7 @@ Unknown options, malformed arguments, unsupported schemas, tripped caps, provide
|
||||
For each `agent()` call:
|
||||
|
||||
1. The worker sends `child-start` with a plain-data prompt and options.
|
||||
2. The host calls the configured provider through async `SubagentService.start`, passing the workflow's parent and one canonical per-run abort signal.
|
||||
2. The host calls the start request's provider override, or otherwise the configured provider, through async `SubagentService.start`, passing the workflow's parent and one canonical per-run abort signal. Provider choice applies to every child in that run and is not visible to the script.
|
||||
3. If start rejects, the host sends `child-start-error`; provider startup has already reached quiescence and no child lifecycle event is emitted.
|
||||
4. If start fulfills while the workflow still admits work, the host records the run, observes `result`, then sends `child-started`. Even an already-settled result is forwarded afterward, preserving start-before-result order.
|
||||
5. The worker emits paired `workflow/agent-start` and `workflow/agent-end` narration and requests child disposal after collection.
|
||||
@@ -81,6 +81,8 @@ The host keeps a ledger of forwarded child starts. A graceful worker supplies th
|
||||
| `syncTimeoutMs` | `5000` | VM timeout for the script's initial synchronous slice. |
|
||||
| `disposeGraceMs` | `5000` | Bound before force-settlement/termination and for public disposal. |
|
||||
|
||||
An owning consumer may set `WorkflowStartRequest.subagentProvider` for one run. This is an engine-level route, not a script hook or a model-facing option; the ordinary `workflow` tool leaves it unset.
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Child-agent requests
|
||||
|
||||
@@ -137,6 +137,7 @@ class WorkerWorkflowEngine extends WorkflowService {
|
||||
// the now-inactive engine fiber and break the seam's holder-owned lifetime.
|
||||
const runCtx = this.ctx
|
||||
const subagents = runCtx.subagents
|
||||
const subagentProvider = request.subagentProvider ?? this.config.provider
|
||||
const workerRun = new WorkerRun(
|
||||
runCtx,
|
||||
subagents,
|
||||
@@ -144,7 +145,7 @@ class WorkerWorkflowEngine extends WorkflowService {
|
||||
meta,
|
||||
request.parent,
|
||||
init,
|
||||
this.config.provider,
|
||||
subagentProvider,
|
||||
this.config.disposeGraceMs,
|
||||
{
|
||||
phase: (title) => { this.emitWorkflowEvent('workflow/phase', info, title) },
|
||||
|
||||
@@ -27,16 +27,30 @@ import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(WorkerWorkflowEngine, {})
|
||||
let selectedStarts = 0
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'built-selected',
|
||||
capabilities: { outputSchema: true, depthLimit: false, toolFilter: false, persona: false },
|
||||
inheritsParentContext: false,
|
||||
async start() {
|
||||
selectedStarts += 1
|
||||
return {
|
||||
id: 'built-child',
|
||||
result: Promise.resolve({ output: [], structured: { answer: 42 }, stopReason: 'completed' }),
|
||||
dispose: () => Promise.resolve(),
|
||||
}
|
||||
},
|
||||
})
|
||||
await ctx.plugin(WorkerWorkflowEngine, { provider: 'must-not-be-used' })
|
||||
const run = ctx.workflows.start({
|
||||
script: 'return 6 * 7',
|
||||
script: "const value = await agent('answer', { schema: { type: 'object', properties: { answer: { type: 'number' } }, required: ['answer'] } }); return value.answer",
|
||||
meta: { name: 'built-smoke', description: 'built worker smoke' },
|
||||
// A zero-agent script never touches the provider.
|
||||
subagentProvider: 'built-selected',
|
||||
parent: { id: 'built-smoke-parent', options: {} },
|
||||
})
|
||||
const result = await run.result
|
||||
await run.dispose()
|
||||
if (result.stopReason !== 'completed' || result.value !== 42) {
|
||||
if (result.stopReason !== 'completed' || result.value !== 42 || selectedStarts !== 1) {
|
||||
console.error('unexpected result: ' + JSON.stringify(result))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -232,6 +232,26 @@ describe('dsh-workflow-workerthread', () => {
|
||||
expect(provider.runs[0]!.request.agentOptions).toEqual({ provider: 'openai' })
|
||||
})
|
||||
|
||||
it('a start-request provider override selects every child without changing the engine default', async () => {
|
||||
const { ctx, parent, provider } = await setup()
|
||||
const selected = new StubProvider('selected', () => text('selected reply'))
|
||||
ctx.subagents.registerProvider(selected)
|
||||
|
||||
const overridden = ctx.workflows.start({
|
||||
...scripted("return await agent('route this run')"),
|
||||
parent,
|
||||
subagentProvider: 'selected',
|
||||
})
|
||||
expect((await overridden.result).value).toBe('selected reply')
|
||||
await overridden.dispose()
|
||||
expect(selected.runs).toHaveLength(1)
|
||||
expect(provider.runs).toHaveLength(0)
|
||||
|
||||
const ordinary = await run(ctx, parent, scripted("return await agent('use the default')"))
|
||||
expect(ordinary.value).toBe('stub reply')
|
||||
expect(provider.runs).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('a fatal hook error inside the worker kills the script and reports the error', async () => {
|
||||
const { ctx, parent } = await setup()
|
||||
const result = await run(ctx, parent, scripted("return await parallel([() => agent('x', { isolation: 'worktree' })])"))
|
||||
|
||||
Reference in New Issue
Block a user