Skip to content

Commit 8130996

Browse files
xuyushun441-sysos-zhuangclaude
authored
fix(service-ai): stamp agent_id on auto-created chat conversations (#2243)
`/api/v1/ai/agents/:agentName/chat` auto-created its conversation via autoCreateConversation() with only userId + metadata, leaving ai_conversations.agent_id NULL. That makes per-agent attribution (analytics, and cloud's per-agent AI metering) impossible — every conversation looked agent-less. Thread the agent through: add ToolExecutionContext.agentId (spec), set it to the path agentName in agent-routes, and forward ctx.agentId into conversationService.create({ agentId }) in autoCreateConversation. Additive and backward-compatible (undefined → null, unchanged for the general /ai/chat route and system invocations). Test: ai-service auto-creates the conversation with the context agentId. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 87d1e37 commit 8130996

4 files changed

Lines changed: 35 additions & 1 deletion

File tree

packages/services/service-ai/src/__tests__/ai-service.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,27 @@ describe('AIService', () => {
394394
expect(after?.messages[1].content).toBe('hello back');
395395
});
396396

397+
it('auto-creates the conversation with the agentId from the execution context', async () => {
398+
const conversationService = new InMemoryConversationService();
399+
const adapter: LLMAdapter = {
400+
name: 'agentid-test',
401+
chat: async () => ({ content: 'ok', model: 'test' }),
402+
complete: async () => ({ content: '' }),
403+
};
404+
const service = new AIService({ adapter, conversationService, logger: silentLogger });
405+
406+
// No conversationId → autoCreateConversation fires (needs an actor); it must
407+
// stamp agent_id so downstream per-agent attribution/metering isn't null.
408+
await service.chatWithTools(
409+
[{ role: 'user', content: 'hi' }],
410+
{ toolExecutionContext: { actor: { id: 'u1' }, agentId: 'ask' } },
411+
);
412+
413+
const mine = await conversationService.list({ agentId: 'ask' });
414+
expect(mine.length).toBe(1);
415+
expect(mine[0].agentId).toBe('ask');
416+
});
417+
397418
it('chatWithTools persists assistant + tool turns across iterations', async () => {
398419
const conversationService = new InMemoryConversationService();
399420
const toolRegistry = new ToolRegistry();

packages/services/service-ai/src/ai-service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,13 +322,16 @@ export class AIService implements IAIService {
322322
* which case we silently fall back to non-persisted chat).
323323
*/
324324
private async autoCreateConversation(
325-
ctx: { actor?: { id?: string }; environmentId?: string } | undefined,
325+
ctx: { actor?: { id?: string }; environmentId?: string; agentId?: string } | undefined,
326326
): Promise<string | undefined> {
327327
const actorId = ctx?.actor?.id;
328328
if (!actorId) return undefined;
329329
try {
330330
const conv = await this.conversationService.create({
331331
userId: actorId,
332+
// Stamp the agent so the conversation (and its messages' usage) is
333+
// attributable per-agent instead of null (e.g. cloud per-agent metering).
334+
agentId: ctx?.agentId,
332335
metadata: ctx?.environmentId ? { environmentId: ctx.environmentId } : undefined,
333336
});
334337
this.logger.debug('[AI] auto-created conversation', { conversationId: conv.id, actorId });

packages/services/service-ai/src/routes/agent-routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,9 @@ export function buildAgentRoutes(
256256
typeof chatContext?.environmentId === 'string'
257257
? chatContext.environmentId
258258
: undefined,
259+
// The agent this chat runs as → stamped onto an auto-created
260+
// conversation's agent_id (per-agent attribution / metering).
261+
agentId: agentName,
259262
// The object/view the user has open — lets built-in data
260263
// tools fall back to "this object" when the request doesn't
261264
// name one (ADR-aligned with the schema injection above).

packages/spec/src/contracts/ai-service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,13 @@ export interface ToolExecutionContext {
410410
messageId?: string;
411411
/** Active environment (multi-tenant project) id, if known. */
412412
environmentId?: string;
413+
/**
414+
* The agent this chat runs as (e.g. the `:agentName` from
415+
* `/api/v1/ai/agents/:agentName/chat`). Stamped onto an auto-created
416+
* conversation's `agent_id` so downstream analytics / per-agent metering can
417+
* attribute usage to the right agent instead of leaving it null.
418+
*/
419+
agentId?: string;
413420
/**
414421
* Object the user is currently viewing in the UI (e.g. the list/detail
415422
* page they have open). Built-in data tools use this as a fallback target

0 commit comments

Comments
 (0)