-
Notifications
You must be signed in to change notification settings - Fork 4
feat(agent): plan mode, governance rendering, and agent instructions #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5613c8a
1a1df01
696533d
6692f12
0f47247
41d7d95
38a7169
a0a6ad9
23ad3fe
ca4ff9a
0adb183
e2047c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ import { | |
| ensureOrphanedToolResults, | ||
| expandUserSelectionContextForModel, | ||
| expandLatestUserAttachmentsForModel, | ||
| buildContinuationParts, | ||
| } from './message-pipeline.js'; | ||
| import { buildSystemPrompt } from './prompt-builder.js'; | ||
| import { buildEarlyChatContext, resolveAsyncContext } from './chat-context.js'; | ||
|
|
@@ -72,7 +73,10 @@ export default { | |
| const url = new URL(request.url); | ||
| if (url.pathname === '/chat') { | ||
| if (request.method === 'HEAD') { | ||
| return new Response(null, { status: 200, headers: CORS_HEADERS }); | ||
| return new Response(null, { | ||
| status: 200, | ||
| headers: { ...CORS_HEADERS, 'Content-Length': '0' }, | ||
| }); | ||
| } | ||
| if (request.method === 'POST') { | ||
| return handleChat(request, env); | ||
|
|
@@ -206,6 +210,14 @@ async function handleChat(request: Request, env: Env): Promise<Response> { | |
| const { allTools, mcpClients, mcpConfig, mcpErrors, generatedToolsIndex, builtInServers } = | ||
| assembled; | ||
|
|
||
| // A tool declares a post-execution "continuation approval" gate via | ||
| // providerOptions.daAgent.continuationApproval (built-in tools set it inline; | ||
| // MCP tools get it during adaptation when they match a server pattern). When such | ||
| // a tool runs we halt the agentic loop after its result and prompt the user to | ||
| // continue — the LLM never decides whether to pause. | ||
| const requiresContinuationApproval = (toolName: string): boolean => | ||
| allTools[toolName]?.providerOptions?.daAgent?.continuationApproval === true; | ||
|
|
||
| console.log(`[da-agent:perf] early=${t1 - t0}ms parallel=${t2 - t1}ms pre-stream=${t2 - t0}ms`); | ||
|
|
||
| const { messages, requestedSkills, imsToken, attachments = [], sessionId } = parsed.data; | ||
|
|
@@ -297,7 +309,7 @@ async function handleChat(request: Request, env: Env): Promise<Response> { | |
| }); | ||
|
|
||
| const stream = createUIMessageStream({ | ||
| execute: ({ writer }) => { | ||
| execute: async ({ writer }) => { | ||
| // Stream results for tools the user approved this round so the client can | ||
| // move each approved card to its result state, before the model continues. | ||
| for (const o of executedOutputs) { | ||
|
|
@@ -343,7 +355,15 @@ async function handleChat(request: Request, env: Env): Promise<Response> { | |
| system: systemPrompt, | ||
| messages: modelMessages as ModelMessage[], | ||
| tools: allTools, | ||
| stopWhen: stepCountIs(5), | ||
| // Halt after the normal step budget OR immediately after a step that ran a | ||
| // continuation-gated tool, so the user can review results before continuing. | ||
| stopWhen: [ | ||
| stepCountIs(5), | ||
| ({ steps }) => { | ||
| const last = steps.at(-1); | ||
| return !!last?.toolCalls?.some((tc) => requiresContinuationApproval(tc.toolName)); | ||
| }, | ||
| ], | ||
| experimental_telemetry: { | ||
| isEnabled: true, | ||
| functionId: 'da-agent-chat', | ||
|
|
@@ -357,7 +377,32 @@ async function handleChat(request: Request, env: Env): Promise<Response> { | |
| }, | ||
| }); | ||
|
|
||
| writer.merge(result.toUIMessageStream()); | ||
| // Merge the model stream manually so we can emit a transient `data-continuation` | ||
| // part after the model stream for any continuation-gated tool that just ran, while | ||
| // holding the terminal `finish` chunk so the ordering is | ||
| // `…tool-output-available, data-continuation, finish`. The transient part is | ||
| // delivered to the client but never merged into message history. (Outputs of tools | ||
| // approved this round were already streamed above from `executedOutputs`.) | ||
| const reader = result.toUIMessageStream().getReader(); | ||
| let finishChunk: Awaited<ReturnType<typeof reader.read>>['value'] | null = null; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the reorder here (hold finish, emit data-continuation, then finish) is the load-bearing bit and it's the one part with no test. buildContinuationParts is well covered on its own, but if the ordering regresses the client shows Continue/Stop at the wrong time or after finish. can we add an integration test that drives a gated tool through the stream and asserts …tool-output, data-continuation, finish? cc: @AlexRRR |
||
| for (;;) { | ||
| // eslint-disable-next-line no-await-in-loop -- sequential stream consumption | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| if (value.type === 'finish') { | ||
| finishChunk = value; | ||
| } else { | ||
| writer.write(value); | ||
| } | ||
| } | ||
|
|
||
| const continuationParts = buildContinuationParts( | ||
| (await result.steps).at(-1), | ||
| requiresContinuationApproval, | ||
| ); | ||
| for (const part of continuationParts) writer.write(part); | ||
|
|
||
| if (finishChunk) writer.write(finishChunk); | ||
| }, | ||
| onError: (error) => { | ||
| console.error('[da-agent] stream error:', formatErrorForLog(error)); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vercel SDK provides natively the
needsApprovalmethod to gate before a tool executes.It doesn't provide anything native for gating a tool after it finishes the execution, so this is the solution Claude came up with: found a freeform metadata, where we can mark tools that we want to gate after.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just want to point out that our tacked-on approvals system is the biggest source of crashes in the agent. We should be aware of this after merging and reconsider if we see that this is not behaving as expected.