feat: chat runtime - pause/resume, SSE transport, React bindings#5
Conversation
|
wow great work, this is pretty complicated. I just have some design questions:
|
|
Thanks for the deep review — all four questions addressed in the latest push: 1. Decision-resume ordering. Went with reject-with-pointer. 2. Concurrency contract for 3. 4. |
|
Looks good, thanks! I'll merge and publish now |
…gaps (#7) * fix(react): close three useChat gaps surfaced by PR #5 review - send(): sync messagesRef before runStream so rapid synchronous sends both reach the outgoing request body - useState init: hydrate pendingDecisions from initialMessages so rehydrated paused tool calls render decision UI immediately - unmount: abort the in-flight fetch on cleanup to prevent leaked streams when the consumer unmounts mid-request * test: lock down regressions surfaced by PR #5 review Adds four regression tests that act as acceptance criteria for the fixes shipped in PR #5 and the companion useChat fixes in this branch. agent.test.ts: - injectDeferralResults() + prompt() places the synthetic toolResult adjacent to its assistant block (verifies the documented "user typed instead of deciding" recovery pattern produces provider-valid order). use-chat.test.ts: - initialMessages with a paused tool call hydrates pendingDecisions. - Two rapid synchronous send() calls both reach the outgoing body. - Unmount aborts the in-flight fetch.
Builds on
feat/features-complete. Adds the chat-runtime layer on top of the redesigned core: pausable tool execution, an SSE-serializable run handle, a headless React hook, a Next.js reference demo, and shared test infrastructure.Summary
@agentic-kit/agent— pausable tools,AgentRunHandle(events /ReadableStream/ SSEResponse),maxSteps, decision lookup bytoolCallId.@agentic-kit/react(new package) —useChathook that POSTs to an SSE endpoint and folds events into messages, streaming snapshot, pending decisions, and executing tools.apps/nextjs-chat-demo(new) — Next.js App Router demo wiringagent.prompt(...).toResponse()touseChat, with a tool-approval UI.agentic-kit—injectDeferralResultshelper for the "user types instead of approving" flow;cross-fetchdropped in the OpenAI adapter in favor of nativefetch.tools/test/(scripted provider, SSE stub, fixtures), SSE parser tests, run-handle tests (443 LOC),useChattests (1011 LOC).What's New
@agentic-kit/agent— pause/resume + SSEdecisionJSON Schema. When the agent reaches a call with no attached decision, it emitstool_decision_pendingand stops. Attach the decision to the matchingtoolCallblock and callcontinue()to resume.AgentRunHandlereturned byprompt()/continue(), consumable exactly once as:await handle— run to completionhandle.events()— async iterator ofAgentEventshandle.toReadableStream()—ReadableStream<AgentEvent>handle.toResponse()— SSEResponseready to return from a Next.js / Hono / Express handlerparseSSEStream()exported from the package for clients consumingtoResponse().maxStepscap on model invocations per run (resets inprompt(), persists acrosscontinue());stopReason: 'completed' | 'max_steps'onagent_end.continue()and the underlying loop walk the message log backwards to find the most recent un-decidedtoolCallmatching a giventoolCallId, so callers may append unrelated messages between the pause and the response.@agentic-kit/react— new packageuseChat({ api, body?, initialMessages?, fetch?, on* }).messages,streamingMessage,isStreaming,pendingDecisions: ReadonlyMap<string, ToolDecisionPendingEvent>,executingToolCallIds: ReadonlySet<string>,error.send,sendMessages,setMessages(array or updater),respondWithDecision(toolCallId, value),abort().abort()finalizes any visible streamed text as an assistant message and drops orphantoolCallblocks so the next call doesn't re-pause.onMessage,onFinish,onDecisionPending,onToolExecutionStart/End,onError.runId. State lives in the message log.agentic-kit—injectDeferralResultsFor the case where the user types a new message while a tool is paused: synthesizes a stand-in
toolResultfor everytoolCallthat lacks both a decision and a paired result, so the server picks up a well-formed transcript.apps/nextjs-chat-demo/api/chat/route.tsconstructs anAgent, applies prior messages, and returnsagent.prompt(...).toResponse().useChatwithchat-input,chat-messages,tool-call-card,tool-approval-cardcomponents.Test infrastructure
tools/test/— repo-internal helpers (nopackage.json, imported via tsconfigpaths). Scripted provider, SSE stub, fixtures, shared index.pnpm teststays deterministic and offline.sse.test.ts(parser),run-handle.test.ts(443 LOC),use-chat.test.ts(1011 LOC underjsdom),inject-deferral-results.test.ts.@agentic-kit/reactis the only package onjsdom; everything else stays onnode.Cleanup
cross-fetchremoved from the OpenAI adapter — runtimes are expected to providefetch.sourceexport condition so workspace consumers can resolve TypeScript directly.Test Plan
pnpm install && pnpm build && pnpm testis green across packagesapps/nextjs-chat-demoboots, streams a chat turn, and a paused tool can be approved/denied viarespondWithDecisionsend()does not re-pauseinjectDeferralResultsflow: pause a tool, send a fresh user message instead of deciding, verify the next request carries synthesized stand-in results