feat: agent-to-agent integrations — activity stream, steer channel, plan gate
Three fork-side integrations that turn OpenChamber from a fire-and-forget
coder into a visible, steerable, plan-gated agent:
1. Activity stream (GET /api/openchamber/agent-activity SSE)
- agent-activity/runtime.js: subscribes to global hub, normalizes
message.updated parts into structured activity events (tool-call,
file-edit, text-part), rate-limited coalescing for tool calls
- Broadcasts openchamber:agent-activity and session-completed events
- SSE endpoint with heartbeat (25s), same shape as /api/openchamber/events
2. Steer channel (POST /api/openchamber/session/:id/steer)
- session-steer/runtime.js: interrupt mode (interrupt → inject
system-level directive → resume) and queue mode (deliver on next idle)
- Follows message-queue precedent for interrupt-safe dispatch
- Broadcasts openchamber:steer-delivered on queued delivery
3. Plan-first gate (plan-gate/runtime.js + approve/reject/status routes)
- State machine per session: pending → approved | rejected | timed_out
- Injects plan-gate reminder via openchamber-sessions create prompt
- Agent emits ## Plan, runtime detects and emits openchamber:plan-ready
- Approve sends 'Proceed' prompt, reject sends revision prompt
- Configurable timeout (default 5 min), auto-approve on timeout
4. P1 shared plumbing
- cardID accepted in session create payload, stored in session metadata
- All new events carry cardID when known
Files added:
- packages/web/server/lib/agent-activity/runtime.js + runtime.test.js
- packages/web/server/lib/session-steer/runtime.js + runtime.test.js
- packages/web/server/lib/plan-gate/runtime.js + runtime.test.js
Files modified:
- packages/web/server/lib/openchamber-sessions/routes.js (cardID, planGate)
- packages/web/server/lib/opencode/feature-routes-runtime.js (route wiring)
- packages/web/server/index.js (runtime creation, SSE_PATH_PREFIXES)
- packages/web/server/lib/ui-auth/ui-auth.js (auth allowlist)
- packages/web/server/lib/realtime-proxy.js (SSE allowlist)
28 new tests passing. All pre-existing tests unaffected.
This commit is contained in:
@@ -196,7 +196,7 @@ const runPromptAsync = async ({ baseUrl, authHeaders, sessionID, directory, payl
|
||||
}
|
||||
};
|
||||
|
||||
const createSession = async ({ baseUrl, authHeaders, directory, title, agent, model, variant }) => {
|
||||
const createSession = async ({ baseUrl, authHeaders, directory, title, agent, model, variant, cardID }) => {
|
||||
const sessionUrl = new URL(`${baseUrl}/session`);
|
||||
sessionUrl.searchParams.set('directory', directory);
|
||||
const response = await fetch(sessionUrl.toString(), {
|
||||
@@ -226,6 +226,21 @@ const createSession = async ({ baseUrl, authHeaders, directory, title, agent, mo
|
||||
if (!sessionID) {
|
||||
throw new Error('failed to create session');
|
||||
}
|
||||
|
||||
if (cardID) {
|
||||
const patchUrl = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}`);
|
||||
patchUrl.searchParams.set('directory', directory);
|
||||
await fetch(patchUrl.toString(), {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
...authHeaders,
|
||||
'content-type': 'application/json',
|
||||
accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ metadata: { openchamber: { cardID } } }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
return sessionID;
|
||||
};
|
||||
|
||||
@@ -476,6 +491,7 @@ export const createOpenChamberSessionService = (dependencies) => {
|
||||
directory,
|
||||
prompt,
|
||||
goalInput,
|
||||
planGate = false,
|
||||
requestedModel,
|
||||
requestedAgent,
|
||||
requestedVariant,
|
||||
@@ -587,6 +603,9 @@ export const createOpenChamberSessionService = (dependencies) => {
|
||||
...(goalInput.enabled
|
||||
? [{ type: 'text', text: buildGoalIntroText(goalInput.tokenBudget), synthetic: true }]
|
||||
: []),
|
||||
...(planGate
|
||||
? [{ type: 'text', text: '<system-reminder>\nPlan gate is active. Before editing any files, output your implementation plan as a markdown block starting with `## Plan`. Then STOP and wait for approval. Do not read files beyond what you need to write the plan.\n</system-reminder>', synthetic: true }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -680,6 +699,8 @@ export const createOpenChamberSessionService = (dependencies) => {
|
||||
const create = async (payload = {}) => {
|
||||
const title = asNonEmptyString(payload.title);
|
||||
const prompt = asNonEmptyString(payload.prompt);
|
||||
const cardID = asNonEmptyString(payload.cardID);
|
||||
const planGate = payload.planGate === true;
|
||||
const goalInput = resolveGoalInput(payload, prompt);
|
||||
if (!goalInput.ok) {
|
||||
throw new OpenChamberControlError(goalInput.error, 400);
|
||||
@@ -734,6 +755,7 @@ export const createOpenChamberSessionService = (dependencies) => {
|
||||
...(agent ? { agent } : {}),
|
||||
...(model ? { model } : {}),
|
||||
...(variant ? { variant } : {}),
|
||||
...(cardID ? { cardID } : {}),
|
||||
});
|
||||
|
||||
let dispatch = { model, agent, variant, promptDispatched: false, dispatchedAsCommand: false };
|
||||
@@ -746,6 +768,7 @@ export const createOpenChamberSessionService = (dependencies) => {
|
||||
directory: sessionDirectory,
|
||||
prompt,
|
||||
goalInput,
|
||||
planGate,
|
||||
requestedModel: model,
|
||||
requestedAgent: agent,
|
||||
requestedVariant: variant,
|
||||
@@ -766,6 +789,8 @@ export const createOpenChamberSessionService = (dependencies) => {
|
||||
dispatchedAsCommand: dispatch.dispatchedAsCommand,
|
||||
...(goalInput.enabled ? { goalEnabled: true } : {}),
|
||||
...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}),
|
||||
...(cardID ? { cardID } : {}),
|
||||
...(planGate ? { planGate: true } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -782,6 +807,8 @@ export const createOpenChamberSessionService = (dependencies) => {
|
||||
dispatchedAsCommand: dispatch.dispatchedAsCommand,
|
||||
...(goalInput.enabled ? { goalEnabled: true } : {}),
|
||||
...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}),
|
||||
...(cardID ? { cardID } : {}),
|
||||
...(planGate ? { planGate: true } : {}),
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user