Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/services/service-ai/src/__tests__/ai-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,27 @@ describe('AIService', () => {
expect(after?.messages[1].content).toBe('hello back');
});

it('auto-creates the conversation with the agentId from the execution context', async () => {
const conversationService = new InMemoryConversationService();
const adapter: LLMAdapter = {
name: 'agentid-test',
chat: async () => ({ content: 'ok', model: 'test' }),
complete: async () => ({ content: '' }),
};
const service = new AIService({ adapter, conversationService, logger: silentLogger });

// No conversationId → autoCreateConversation fires (needs an actor); it must
// stamp agent_id so downstream per-agent attribution/metering isn't null.
await service.chatWithTools(
[{ role: 'user', content: 'hi' }],
{ toolExecutionContext: { actor: { id: 'u1' }, agentId: 'ask' } },
);

const mine = await conversationService.list({ agentId: 'ask' });
expect(mine.length).toBe(1);
expect(mine[0].agentId).toBe('ask');
});

it('chatWithTools persists assistant + tool turns across iterations', async () => {
const conversationService = new InMemoryConversationService();
const toolRegistry = new ToolRegistry();
Expand Down
5 changes: 4 additions & 1 deletion packages/services/service-ai/src/ai-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,13 +322,16 @@ export class AIService implements IAIService {
* which case we silently fall back to non-persisted chat).
*/
private async autoCreateConversation(
ctx: { actor?: { id?: string }; environmentId?: string } | undefined,
ctx: { actor?: { id?: string }; environmentId?: string; agentId?: string } | undefined,
): Promise<string | undefined> {
const actorId = ctx?.actor?.id;
if (!actorId) return undefined;
try {
const conv = await this.conversationService.create({
userId: actorId,
// Stamp the agent so the conversation (and its messages' usage) is
// attributable per-agent instead of null (e.g. cloud per-agent metering).
agentId: ctx?.agentId,
metadata: ctx?.environmentId ? { environmentId: ctx.environmentId } : undefined,
});
this.logger.debug('[AI] auto-created conversation', { conversationId: conv.id, actorId });
Expand Down
3 changes: 3 additions & 0 deletions packages/services/service-ai/src/routes/agent-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ export function buildAgentRoutes(
typeof chatContext?.environmentId === 'string'
? chatContext.environmentId
: undefined,
// The agent this chat runs as → stamped onto an auto-created
// conversation's agent_id (per-agent attribution / metering).
agentId: agentName,
// The object/view the user has open — lets built-in data
// tools fall back to "this object" when the request doesn't
// name one (ADR-aligned with the schema injection above).
Expand Down
7 changes: 7 additions & 0 deletions packages/spec/src/contracts/ai-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,13 @@ export interface ToolExecutionContext {
messageId?: string;
/** Active environment (multi-tenant project) id, if known. */
environmentId?: string;
/**
* The agent this chat runs as (e.g. the `:agentName` from
* `/api/v1/ai/agents/:agentName/chat`). Stamped onto an auto-created
* conversation's `agent_id` so downstream analytics / per-agent metering can
* attribute usage to the right agent instead of leaving it null.
*/
agentId?: string;
/**
* Object the user is currently viewing in the UI (e.g. the list/detail
* page they have open). Built-in data tools use this as a fallback target
Expand Down