-
Notifications
You must be signed in to change notification settings - Fork 0
fix(agents): emit model call context on project agent runs #3373
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
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 |
|---|---|---|
|
|
@@ -51,6 +51,8 @@ import { | |
| mapRuntimeEventToAgUi, | ||
| parseSseJsonEvents, | ||
| } from "./ag-ui-sse.ts"; | ||
| import type { AgentRunEvent, AgentRunEventSink } from "#veryfront/runtime/model-call-context.ts"; | ||
| import { runWithMandatoryRunEventSink } from "#veryfront/runtime/run-event-sink-context.ts"; | ||
| import { AgentRunCancelledError, type AgentRunSessionManager } from "./session-manager.ts"; | ||
| import { composeInternalAgentRunSystemPrompt } from "./run-system-prompt.ts"; | ||
| import type { RuntimeRunAgentInput } from "./schema.ts"; | ||
|
|
@@ -64,6 +66,12 @@ const INTERNAL_AGENT_RUNTIME_HEARTBEAT_INTERVAL_MS = 25_000; | |
| const INTERNAL_AGENT_RUNTIME_HEARTBEAT_FRAME = new TextEncoder().encode( | ||
| ": internal-agent-runtime-heartbeat\n\n", | ||
| ); | ||
| /** | ||
| * SSE frame name carrying AGENT_RUN_MODEL_CALL_CONTEXT to veryfront-api. Not an | ||
| * AG-UI event: veryfront-api persists it under its own event type rather than | ||
| * folding it into the run's public event sequence. | ||
| */ | ||
| export const MODEL_CALL_CONTEXT_SSE_EVENT_NAME = "AgentRunModelCallContext"; | ||
|
|
||
| type RuntimeFilteredAgent = Agent & { | ||
| config: Agent["config"] & { | ||
|
|
@@ -638,6 +646,31 @@ function compactRuntimeMessagesForStream( | |
| ) as Message[]; | ||
| } | ||
|
|
||
| /** | ||
| * Relays run events produced by the runtime into the run's SSE stream. | ||
| * | ||
| * The first model call is dispatched while `runtime.stream()` is still being | ||
| * awaited, before there is a controller to enqueue into, so events raised | ||
| * before `attach` are buffered and replayed once the stream opens. | ||
| */ | ||
| function createModelCallContextRelay(): { | ||
| sink: AgentRunEventSink; | ||
| attach: (emit: (event: AgentRunEvent) => void) => void; | ||
| } { | ||
| const buffered: AgentRunEvent[] = []; | ||
| let emit: ((event: AgentRunEvent) => void) | undefined; | ||
| return { | ||
| sink: (event) => { | ||
| if (emit) emit(event); | ||
| else buffered.push(event); | ||
| }, | ||
| attach: (next) => { | ||
| emit = next; | ||
| for (const event of buffered.splice(0)) next(event); | ||
| }, | ||
| }; | ||
| } | ||
|
Comment on lines
+656
to
+672
|
||
|
|
||
| export async function createRuntimeAgentStreamResponse( | ||
| input: RuntimeRunAgentInput, | ||
| agent: Agent, | ||
|
|
@@ -659,6 +692,7 @@ export async function createRuntimeAgentStreamResponse( | |
| let completedResponse: AgentResponse | null = null; | ||
| let runtimeStream: ReadableStream<Uint8Array>; | ||
| let closeSandbox = createIdempotentAsyncCleanup(); | ||
| const modelCallContextRelay = createModelCallContextRelay(); | ||
| try { | ||
| const forwardedAllowedRemoteToolNames = getAllowedRemoteToolNames(input.forwardedProps); | ||
| const sourceAllowedRemoteToolNames = getAgentAllowedRemoteToolNames(agent); | ||
|
|
@@ -788,27 +822,34 @@ export async function createRuntimeAgentStreamResponse( | |
| runtimeToolNames.length, | ||
| ); | ||
| const maxOutputTokens = getForwardedMaxOutputTokens(input.forwardedProps); | ||
| const candidateRuntimeStream = await runtime.stream( | ||
| runtimeMessages, | ||
| { | ||
| threadId: input.threadId, | ||
| runId: input.runId, | ||
| ...(deps.projectAgentSandbox?.authToken | ||
| ? { authToken: deps.projectAgentSandbox.authToken } | ||
| : {}), | ||
| ...(input.parentRunId ? { parentRunId: input.parentRunId } : {}), | ||
| ...(input.state !== undefined ? { state: input.state } : {}), | ||
| context: input.context, | ||
| forwardedProps: input.forwardedProps, | ||
| }, | ||
| { | ||
| onFinish: (response) => { | ||
| completedResponse = response; | ||
| }, | ||
| }, | ||
| undefined, | ||
| maxOutputTokens, | ||
| abortSignal, | ||
| // Scoped here because the runtime dispatches the run's first model call | ||
| // before stream() resolves. Later steps inherit this scope through the | ||
| // stream they are pumped from. | ||
| const candidateRuntimeStream = await runWithMandatoryRunEventSink( | ||
| modelCallContextRelay.sink, | ||
| () => | ||
| runtime.stream( | ||
| runtimeMessages, | ||
| { | ||
| threadId: input.threadId, | ||
| runId: input.runId, | ||
| ...(deps.projectAgentSandbox?.authToken | ||
| ? { authToken: deps.projectAgentSandbox.authToken } | ||
| : {}), | ||
| ...(input.parentRunId ? { parentRunId: input.parentRunId } : {}), | ||
| ...(input.state !== undefined ? { state: input.state } : {}), | ||
| context: input.context, | ||
| forwardedProps: input.forwardedProps, | ||
| }, | ||
| { | ||
| onFinish: (response) => { | ||
| completedResponse = response; | ||
| }, | ||
| }, | ||
| undefined, | ||
| maxOutputTokens, | ||
| abortSignal, | ||
| ), | ||
| ); | ||
| if (candidateRuntimeStream.locked) { | ||
| throw new TypeError("Internal agent runtime returned a locked stream"); | ||
|
|
@@ -960,6 +1001,14 @@ export async function createRuntimeAgentStreamResponse( | |
| threadId: input.threadId, | ||
| agentId: agent.id, | ||
| }); | ||
| // Replays whatever the first model call already produced, then | ||
| // forwards later steps as they happen. RunStarted stays first. | ||
| modelCallContextRelay.attach((event) => | ||
| enqueueIfAttached( | ||
| MODEL_CALL_CONTEXT_SSE_EVENT_NAME, | ||
| event as unknown as Record<string, unknown>, | ||
| ) | ||
| ); | ||
|
Comment on lines
+1004
to
+1011
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. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- run-stream structure ---'
ast-grep outline src/internal-agents/run-stream.ts | sed -n '1,220p'
printf '%s\n' '--- stream relay and response headers ---'
sed -n '960,1040p' src/internal-agents/run-stream.ts
sed -n '1180,1250p' src/internal-agents/run-stream.ts
printf '%s\n' '--- response callers and cache directives ---'
rg -n -C 4 'createRuntimeAgentStreamResponse|Cache-Control|cache-control|no-cache|no-store|text/event-stream' --glob '*.{ts,tsx}' .
printf '%s\n' '--- SSE helper ---'
sed -n '1,240p' src/internal-agents/ag-ui-sse.tsRepository: veryfront/veryfront-code Length of output: 50381 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target response implementation ---'
sed -n '980,1030p' src/internal-agents/run-stream.ts
sed -n '1200,1240p' src/internal-agents/run-stream.ts
printf '%s\n' '--- exact response symbol references ---'
rg -n -C 3 'createRuntimeAgentStreamResponse' src --glob '*.{ts,tsx}' --glob '!**/*.generated.*'
printf '%s\n' '--- cache directives in relevant server paths ---'
rg -n -C 3 'Cache-Control|cache-control|no-cache|no-store' src/internal-agents src/server --glob '*.{ts,tsx}' --glob '!**/*.generated.*' | head -n 500
printf '%s\n' '--- SSE helper implementation ---'
wc -l src/internal-agents/ag-ui-sse.ts
cat -n src/internal-agents/ag-ui-sse.ts | sed -n '1,220p'
printf '%s\n' '--- cache-control semantic probe ---'
python3 - <<'PY'
from email.message import Message
for value in ("no-cache", "no-store"):
m = Message()
m["Cache-Control"] = value
print(value, "=", [x.strip().lower() for x in m["Cache-Control"].split(",")])
PYRepository: veryfront/veryfront-code Length of output: 50380 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- production caller ---'
sed -n '820,930p' src/server/handlers/request/agent-stream.handler.ts
printf '%s\n' '--- request handler return and middleware path ---'
rg -n -C 8 'runAgentStream|internalFetch|toNativeResponse|agent-stream' src/server/handlers/request/agent-stream.handler.ts src/server/index.ts src/server --glob '*.{ts,tsx}' --glob '!**/*.generated.*' | head -n 350
printf '%s\n' '--- response mutation utilities on the exact path ---'
rg -n -C 5 'new Response|Response\(|toNativeResponse|headers\.set|headers\.append|headers\.delete|Cache-Control|cache-control' src/server/index.ts src/server/handlers/request/agent-stream.handler.ts src/server/middleware src/server/runtime-handler --glob '*.{ts,tsx}' 2>/dev/null | head -n 500
printf '%s\n' '--- focused tests for stream response headers ---'
rg -n -C 8 'content-type|Cache-Control|cache-control|text/event-stream|keep-alive|headers\.get' src/internal-agents/run-stream.test.ts src/server/handlers/request --glob '*.{ts,tsx}' | head -n 300
printf '%s\n' '--- read-only source verifier ---'
python3 - <<'PY'
from pathlib import Path
stream = Path("src/internal-agents/run-stream.ts").read_text()
handler = Path("src/server/handlers/request/agent-stream.handler.ts").read_text()
assert '"Cache-Control": "no-cache"' in stream
assert '"Content-Type": "text/event-stream"' in stream
assert "createRuntimeAgentStreamResponse(runtimeInput, runtimeAgent" in handler
start = stream.index('return new Response(response, {')
end = stream.index(' });', start) + len(' });')
response_block = stream[start:end]
print("direct_response_block_contains_no_cache=", '"Cache-Control": "no-cache"' in response_block)
print("direct_response_block_contains_no_store=", '"Cache-Control": "no-store"' in response_block)
print("production_caller_count=", handler.count("createRuntimeAgentStreamResponse(runtimeInput, runtimeAgent"))
PYRepository: veryfront/veryfront-code Length of output: 50383 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- agent-stream trust boundary ---'
sed -n '120,190p' src/server/handlers/request/agent-stream.handler.ts
sed -n '730,825p' src/server/handlers/request/agent-stream.handler.ts
sed -n '930,980p' src/server/handlers/request/agent-stream.handler.ts
printf '%s\n' '--- event name and context payload definitions ---'
rg -n -C 8 'MODEL_CALL_CONTEXT_SSE_EVENT_NAME|modelCallContext|ModelCallContext|MODEL_CALL_CONTEXT' src/internal-agents src --glob '*.{ts,tsx}' --glob '!**/*.generated.*' | head -n 350
printf '%s\n' '--- request lifecycle stream handling ---'
sed -n '100,165p' src/server/runtime-handler/request-lifecycle.ts
printf '%s\n' '--- focused header verifier ---'
python3 - <<'PY'
from pathlib import Path
stream = Path("src/internal-agents/run-stream.ts").read_text()
handler = Path("src/server/handlers/request/agent-stream.handler.ts").read_text()
server = Path("src/server/index.ts").read_text()
lifecycle = Path("src/server/runtime-handler/request-lifecycle.ts").read_text()
start = stream.index('return new Response(response, {')
end = stream.index(' });', start) + len(' });')
block = stream[start:end]
assert '"Cache-Control": "no-cache"' in block
assert '"Cache-Control": "no-store"' not in block
assert 'if (!headers.has(key))' in handler
assert 'headers: src.headers' in server
assert 'return response;' in lifecycle
print("stream_header=", "no-cache")
print("wrapper_overrides_existing_cache_control=", False)
print("native_adapter_preserves_headers=", True)
print("lifecycle_returns_event_stream_without_header_rewrite=", True)Repository: veryfront/veryfront-code Length of output: 36274 Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information Reachability: Internal · Exploitability: Difficult Reachability pathSet 🤖 Prompt for AI Agents |
||
| heartbeatTimer = setInterval( | ||
| enqueueHeartbeatIfAttached, | ||
| INTERNAL_AGENT_RUNTIME_HEARTBEAT_INTERVAL_MS, | ||
|
|
@@ -968,7 +1017,13 @@ export async function createRuntimeAgentStreamResponse( | |
| while (true) { | ||
| throwIfAborted(); | ||
|
|
||
| const { done, value } = await reader.read(); | ||
| // A runtime that dispatches later model calls from its pull() | ||
| // rather than from a continuation of stream() needs the sink in | ||
| // scope on the read that triggers the pull. | ||
| const { done, value } = await runWithMandatoryRunEventSink( | ||
| modelCallContextRelay.sink, | ||
| () => reader.read(), | ||
| ); | ||
| throwIfAborted(); | ||
|
|
||
| if (done) { | ||
|
|
||
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.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Test ordering against a runtime step event.
Lines 2644-2650 return a stream with no mapped runtime step. The assertion only proves that model-call context follows
RunStarted. It does not prove that model-call context precedes the step it describes. Emit a runtime SSE step frame and assert that the context frame occurs before that mapped frame.As per coding guidelines, “For behavior changes, add or update a focused failing test before changing the implementation.”
🤖 Prompt for AI Agents
Source: Coding guidelines