Skip to content

Commit b46bcb3

Browse files
committed
refactor(core): share protocol primitives
1 parent e1096c5 commit b46bcb3

34 files changed

Lines changed: 1319 additions & 1743 deletions

apps/tanstack-chat-demo/src/components/chat/settings-popover.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ export function SettingsPopover({ settings, onChange, onRefresh }: Props) {
2929
<PopoverContent
3030
align="end"
3131
sideOffset={8}
32-
className="w-[360px] rounded-[16px] border-0 bg-bg-elevated p-0 shadow-[0_20px_60px_-15px_rgba(0,0,0,0.25),0_0_0_0.5px_rgba(0,0,0,0.08)]"
32+
className="max-h-[var(--radix-popover-content-available-height)] w-[360px] overflow-hidden rounded-[16px] border-0 bg-bg-elevated p-0 shadow-[0_20px_60px_-15px_rgba(0,0,0,0.25),0_0_0_0.5px_rgba(0,0,0,0.08)]"
3333
>
34-
<div className="px-4 pt-4 pb-3">
34+
<div className="shrink-0 px-4 pt-4 pb-3">
3535
<h3 className="text-[17px] font-semibold text-label" style={{ letterSpacing: '-0.43px' }}>
3636
Settings
3737
</h3>
@@ -40,7 +40,7 @@ export function SettingsPopover({ settings, onChange, onRefresh }: Props) {
4040
</p>
4141
</div>
4242

43-
<div className="px-4">
43+
<div className="min-h-0 overflow-y-auto px-4">
4444
<div className="overflow-hidden rounded-[12px] bg-bg-2">
4545
<Field label="OpenAI key">
4646
<Input
@@ -79,13 +79,13 @@ export function SettingsPopover({ settings, onChange, onRefresh }: Props) {
7979
value={settings.systemPrompt}
8080
onChange={(e) => onChange({ systemPrompt: e.target.value })}
8181
placeholder="Optional instructions for the assistant"
82-
className="mt-1.5 resize-none rounded-[8px] border-0 bg-bg px-2.5 py-2 text-[14px] leading-[1.4] text-label shadow-none placeholder:text-label-3 focus-visible:ring-0"
82+
className="mt-1.5 max-h-[min(40dvh,320px)] resize-y overflow-y-auto rounded-[8px] border-0 bg-bg px-2.5 py-2 text-[14px] leading-[1.4] text-label shadow-none ![field-sizing:fixed] placeholder:text-label-3 focus-visible:ring-0"
8383
style={{ letterSpacing: '-0.15px' }}
8484
/>
8585
</div>
8686
</div>
8787

88-
<div className="mt-4 flex items-center justify-between border-t border-hairline px-4 py-3">
88+
<div className="mt-4 flex shrink-0 items-center justify-between border-t border-hairline px-4 py-3">
8989
<span className="text-[12.5px] text-label-2">Cached in this browser</span>
9090
<button
9191
type="button"

packages/agent/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"test:watch": "jest --watch"
3030
},
3131
"dependencies": {
32+
"@agentic-kit/core": "workspace:*",
3233
"agentic-kit": "workspace:*"
3334
},
3435
"keywords": []

packages/agent/src/agent-loop.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import {
2+
type AssistantMessage,
3+
type Context,
4+
createToolResultMessage,
5+
type Message,
6+
type StreamOptions,
7+
} from '@agentic-kit/core';
8+
import { stream as defaultStream } from 'agentic-kit';
9+
10+
import type {
11+
AgentEvent,
12+
AgentOptions,
13+
AgentState,
14+
AgentTool,
15+
AgentToolResult,
16+
} from './types.js';
17+
import { validateToolArguments as defaultValidateToolArguments } from './validation.js';
18+
19+
export type AgentEventSink = (event: AgentEvent) => void | Promise<void>;
20+
21+
export type AgentLoopConfig = {
22+
initialMessages?: Message[];
23+
state: AgentState;
24+
streamFn?: AgentOptions['streamFn'];
25+
transformContext?: AgentOptions['transformContext'];
26+
validateToolArguments?: AgentOptions['validateToolArguments'];
27+
signal: AbortSignal;
28+
};
29+
30+
export async function runAgentLoop(config: AgentLoopConfig, emit: AgentEventSink): Promise<Message[]> {
31+
const messages = [...config.state.messages];
32+
33+
await emit({ type: 'agent_start' });
34+
35+
if (config.initialMessages && config.initialMessages.length > 0) {
36+
for (const message of config.initialMessages) {
37+
await emit({ type: 'message_start', message });
38+
messages.push(message);
39+
await emit({ type: 'message_end', message });
40+
}
41+
}
42+
43+
while (true) {
44+
await emit({ type: 'turn_start' });
45+
46+
const assistantMessage = await generateAssistantMessage(config, messages, emit);
47+
messages.push(assistantMessage);
48+
await emit({ type: 'message_end', message: assistantMessage });
49+
50+
if (assistantMessage.stopReason === 'error' || assistantMessage.stopReason === 'aborted') {
51+
await emit({ type: 'turn_end', message: assistantMessage, toolResults: [] });
52+
break;
53+
}
54+
55+
const toolCalls = assistantMessage.content.filter((block) => block.type === 'toolCall');
56+
if (toolCalls.length === 0) {
57+
await emit({ type: 'turn_end', message: assistantMessage, toolResults: [] });
58+
break;
59+
}
60+
61+
const toolResults = await executeToolCalls(config, toolCalls, emit);
62+
for (const toolResult of toolResults) {
63+
await emit({ type: 'message_start', message: toolResult });
64+
messages.push(toolResult);
65+
await emit({ type: 'message_end', message: toolResult });
66+
}
67+
68+
await emit({ type: 'turn_end', message: assistantMessage, toolResults });
69+
}
70+
71+
await emit({ type: 'agent_end', messages });
72+
return messages;
73+
}
74+
75+
async function generateAssistantMessage(
76+
config: AgentLoopConfig,
77+
messages: Message[],
78+
emit: AgentEventSink
79+
): Promise<AssistantMessage> {
80+
const transformedMessages = config.transformContext
81+
? await config.transformContext(messages, config.signal)
82+
: messages;
83+
84+
const context: Context = {
85+
systemPrompt: config.state.systemPrompt,
86+
tools: config.state.tools,
87+
messages: transformedMessages,
88+
};
89+
90+
const streamFn = config.streamFn ?? defaultStream;
91+
const streamResult = streamFn(config.state.model, context, {
92+
...(config.state.streamOptions ?? {}),
93+
signal: config.signal,
94+
} as StreamOptions);
95+
96+
for await (const event of streamResult) {
97+
switch (event.type) {
98+
case 'start':
99+
await emit({ type: 'message_start', message: event.partial });
100+
break;
101+
case 'text_start':
102+
case 'text_delta':
103+
case 'text_end':
104+
case 'thinking_start':
105+
case 'thinking_delta':
106+
case 'thinking_end':
107+
case 'toolcall_start':
108+
case 'toolcall_delta':
109+
case 'toolcall_end':
110+
await emit({
111+
type: 'message_update',
112+
message: event.partial,
113+
assistantMessageEvent: event,
114+
});
115+
break;
116+
case 'done':
117+
case 'error':
118+
break;
119+
}
120+
}
121+
122+
return streamResult.result();
123+
}
124+
125+
async function executeToolCalls(
126+
config: AgentLoopConfig,
127+
toolCalls: Array<Extract<AssistantMessage['content'][number], { type: 'toolCall' }>>,
128+
emit: AgentEventSink
129+
) {
130+
const results = [];
131+
132+
for (const toolCall of toolCalls) {
133+
const tool = config.state.tools.find((candidate) => candidate.name === toolCall.name);
134+
await emit({
135+
type: 'tool_execution_start',
136+
toolCallId: toolCall.id,
137+
toolName: toolCall.name,
138+
args: toolCall.arguments as Record<string, unknown>,
139+
});
140+
141+
let result: AgentToolResult;
142+
let isError = false;
143+
144+
try {
145+
if (!tool) {
146+
throw new Error(`Tool '${toolCall.name}' not found`);
147+
}
148+
149+
const validateToolArguments = config.validateToolArguments ?? defaultValidateToolArguments;
150+
const validatedArgs = validateToolArguments(
151+
tool.parameters,
152+
toolCall.arguments as Record<string, unknown>
153+
);
154+
155+
result = await executeTool(tool, toolCall.id, validatedArgs, config.signal, emit);
156+
} catch (error) {
157+
result = {
158+
content: [
159+
{
160+
type: 'text',
161+
text: error instanceof Error ? error.message : String(error),
162+
},
163+
],
164+
};
165+
isError = true;
166+
}
167+
168+
await emit({
169+
type: 'tool_execution_end',
170+
toolCallId: toolCall.id,
171+
toolName: toolCall.name,
172+
result,
173+
isError,
174+
});
175+
176+
results.push(
177+
createToolResultMessage(toolCall.id, toolCall.name, result.content, isError)
178+
);
179+
}
180+
181+
return results;
182+
}
183+
184+
async function executeTool(
185+
tool: AgentTool,
186+
toolCallId: string,
187+
args: Record<string, unknown>,
188+
signal: AbortSignal,
189+
emit: AgentEventSink
190+
): Promise<AgentToolResult> {
191+
const updateEvents: Promise<void>[] = [];
192+
193+
try {
194+
return await tool.execute(toolCallId, args, signal, (partialResult) => {
195+
updateEvents.push(
196+
Promise.resolve(
197+
emit({
198+
type: 'tool_execution_update',
199+
toolCallId,
200+
toolName: tool.name,
201+
args,
202+
partialResult,
203+
})
204+
)
205+
);
206+
});
207+
} finally {
208+
await Promise.all(updateEvents);
209+
}
210+
}

0 commit comments

Comments
 (0)