feat: phase 3-5 — workflow engine, background subagents, multi-modal, cache shape, inline diff

Phase 3: Dynamic Workflow Engine
- VM sandbox (node:vm) with agent/parallel/pipeline API, Claude Code compatible
- Workflow file discovery (.boocode/workflows/*.js + ~/.boocode/workflows/*.js)
- Workflow manager with session/chat creation and inference dispatch
- Built-in catalog: deep-research, review-code, find-issues
- Resumability cache: SHA-256 hash of agent spec, in-memory Map

Phase 4: Background Subagents
- background-task.ts service: spawn/poll/cancel lifecycle
- spawn_subagent, subagent_status, subagent_result tools in ALL_TOOLS

Phase 5: Multi-modal + Cache Shape
- Multi-modal stub with type defs and hook point in payload.ts
- CacheShapeBadge component in trace viewer (colored bar + %)
This commit is contained in:
2026-06-08 03:11:39 +00:00
parent 74da084521
commit 45a1140fd3
23 changed files with 2938 additions and 33 deletions

View File

@@ -21,10 +21,11 @@ import { registerSkillsRoutes } from './routes/skills.js';
import { registerTraceRoutes } from './routes/traces.js';
import { registerToolsRoutes } from './routes/tools.js';
import { registerAnalyticsRoutes } from './routes/analytics.js';
import { registerMemoryRoutes } from './routes/memory.js';
import { registerInferenceSettingsRoutes } from './routes/inference-settings.js';
import { createInferenceRunner } from './services/inference/index.js';
import { createInferenceRunner, runInferenceWithModel } from './services/inference/index.js';
import { createBroker } from './services/broker.js';
import { setBackgroundInferenceEnqueuer } from './services/background-task.js';
import { listSkills } from './services/skills.js';
import * as compaction from './services/compaction.js';
import { configureModelContext } from './services/model-context.js';
@@ -125,11 +126,37 @@ async function main() {
registerModelRoutes(app, config);
registerAgentRoutes(app, sql);
registerSidebarRoutes(app, sql);
registerChatRoutes(app, sql, broker);
registerChatRoutes(app, sql, broker, config, {
enqueueCompare: (sessionId, chatId, assistantMessageId, modelOverride, compareGroupId) => {
// Reuse the inference runner's context pattern for compare mode.
// Each compare run gets its own AbortController; cancellation keyed by
// chatId (cancels ALL parallel runs in that compare group).
const compareCtx: import('./services/inference/types.js').InferenceContext = {
sql,
config,
log: app.log,
publish: (sid, frame) => {
broker.publishFrame(sid, frame as unknown as import('@boocode/contracts/ws-frames').WsFrame);
},
publishUser: (frame) => {
broker.publishUserFrame('default', frame as unknown as import('@boocode/contracts/ws-frames').WsFrame);
},
broker,
hooks: hasHooks ? hookRunner : undefined,
};
compareCtx.publishUser({ type: 'chat_status', chat_id: chatId, status: 'streaming', at: new Date().toISOString() });
void runInferenceWithModel(compareCtx, sessionId, chatId, assistantMessageId, modelOverride, compareGroupId).catch(
(err: Error) => app.log.error({ err, chatId, modelOverride }, 'compare inference failed'),
);
},
cancelInference: async (_sessionId, chatId) => {
return inference.cancel(_sessionId, chatId);
},
hasActiveInference: (chatId) => inference.hasActive(chatId),
});
registerTraceRoutes(app, sql);
registerToolsRoutes(app, sql);
registerAnalyticsRoutes(app, sql);
registerMemoryRoutes(app, sql);
registerInferenceSettingsRoutes(app);
// Batch 9.6: warm the skills cache at boot and surface the count. Empty or
@@ -167,6 +194,13 @@ async function main() {
broker.publishUserFrame(user, frame as unknown as import('@boocode/contracts/ws-frames').WsFrame);
}
);
// v2.x: wire the background subagent task system to the inference runner.
// Tools (spawn_subagent) dispatch fire-and-forget inference via this
// module-level reference — no import cycle through the tool registry.
setBackgroundInferenceEnqueuer((sessionId, chatId, assistantId, user) => {
inference.enqueue(sessionId, chatId, assistantId, user);
});
registerMessageRoutes(app, sql, config, broker, {
enqueueInference: (sessionId, chatId, assistantId, user) => {
inference.enqueue(sessionId, chatId, assistantId, user);