Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/petite-ends-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inkeep/agents-api": patch
---

Allowed Artifacts to be Passed as Tool Arguments
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor: Changeset message should use sentence case

Issue: The changeset message uses title case ("Allowed Artifacts to be Passed as Tool Arguments") rather than sentence case per CLAUDE.md guidelines.

Why: Consistency with changelog conventions.

Fix:

Suggested change
Allowed Artifacts to be Passed as Tool Arguments
Allow artifacts to be passed as tool arguments

Refs:

4 changes: 4 additions & 0 deletions agents-api/src/__tests__/run/agents/Agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ describe('Agent Integration with SystemPromptBuilder', () => {
dataComponents: [],
artifacts: [],
artifactComponents: [],
allProjectArtifactComponents: [],
hasAgentArtifactComponents: false,
includeDataComponents: false,
clientCurrentTime: undefined,
Expand All @@ -593,6 +594,7 @@ describe('Agent Integration with SystemPromptBuilder', () => {
dataComponents: [],
artifacts: [],
artifactComponents: [],
allProjectArtifactComponents: [],
hasAgentArtifactComponents: false,
includeDataComponents: false,
clientCurrentTime: undefined,
Expand All @@ -618,6 +620,7 @@ describe('Agent Integration with SystemPromptBuilder', () => {
dataComponents: [],
artifacts: [],
artifactComponents: [],
allProjectArtifactComponents: [],
hasAgentArtifactComponents: false,
includeDataComponents: false,
clientCurrentTime: undefined,
Expand Down Expand Up @@ -654,6 +657,7 @@ describe('Agent Integration with SystemPromptBuilder', () => {
dataComponents: [],
artifacts: [],
artifactComponents: [],
allProjectArtifactComponents: [],
hasAgentArtifactComponents: false,
includeDataComponents: false,
clientCurrentTime: undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { beforeEach, describe, expect, test } from 'vitest';
import { SystemPromptBuilder } from '../../../domains/run/agents/SystemPromptBuilder';
import type { SystemPromptV1 } from '../../../domains/run/agents/types';
import { PromptConfig } from '../../../domains/run/agents/versions/v1/PromptConfig';

const testArtifactComponents = [
{
id: 'comp-1',
name: 'ResearchDoc',
description: 'A research document',
props: {
type: 'object',
properties: {
title: { type: 'string', description: 'Document title', inPreview: true },
summary: { type: 'string', description: 'Short summary', inPreview: true },
content: { type: 'string', description: 'Full content', inPreview: false },
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags',
inPreview: false,
},
},
},
},
];

function makeArtifact(overrides: Record<string, any> = {}) {
return {
artifactId: 'art-1',
name: 'My Research Doc',
description: 'A research document artifact',
taskId: 'task-1',
toolCallId: 'tool-call-1',
createdAt: '2024-01-01T00:00:00Z',
parts: [{ kind: 'data', data: { summary: { title: 'Test', summary: 'Test summary' } } }],
...overrides,
} as any;
}

describe('PromptConfig — artifact type and schema in generated XML', () => {
let builder: SystemPromptBuilder<SystemPromptV1>;

beforeEach(() => {
builder = new SystemPromptBuilder('v1', new PromptConfig());
});

test('artifact XML includes <type> filled from artifact.type', () => {
const config: SystemPromptV1 = {
corePrompt: 'You are a helpful assistant.',
tools: [],
dataComponents: [],
artifacts: [makeArtifact({ type: 'ResearchDoc' })],
allProjectArtifactComponents: testArtifactComponents,
};

const result = builder.buildSystemPrompt(config);
expect(result.prompt).toContain('<type>ResearchDoc</type>');
});

test('artifact XML includes <type>unknown</type> when artifact has no type', () => {
const config: SystemPromptV1 = {
corePrompt: 'You are a helpful assistant.',
tools: [],
dataComponents: [],
artifacts: [makeArtifact()],
allProjectArtifactComponents: testArtifactComponents,
};

const result = builder.buildSystemPrompt(config);
expect(result.prompt).toContain('<type>unknown</type>');
});

test('type_schema shows PREVIEW and FULL sections when type matches a component', () => {
const config: SystemPromptV1 = {
corePrompt: 'You are a helpful assistant.',
tools: [],
dataComponents: [],
artifacts: [makeArtifact({ type: 'ResearchDoc' })],
allProjectArtifactComponents: testArtifactComponents,
};

const result = builder.buildSystemPrompt(config);
const typeSchemaMatch = result.prompt.match(/<type_schema>([\s\S]*?)<\/type_schema>/);
expect(typeSchemaMatch).not.toBeNull();
expect(typeSchemaMatch?.[1]).toContain('PREVIEW');
expect(typeSchemaMatch?.[1]).toContain('FULL');
});

test('type_schema preview contains only inPreview fields', () => {
const config: SystemPromptV1 = {
corePrompt: 'You are a helpful assistant.',
tools: [],
dataComponents: [],
artifacts: [makeArtifact({ type: 'ResearchDoc' })],
allProjectArtifactComponents: testArtifactComponents,
};

const result = builder.buildSystemPrompt(config);
const typeSchemaMatch = result.prompt.match(/<type_schema>([\s\S]*?)<\/type_schema>/);
const typeSchemaContent = typeSchemaMatch?.[1] ?? '';
const previewIndex = typeSchemaContent.indexOf('PREVIEW');
const fullIndex = typeSchemaContent.indexOf('FULL');
const previewSection = typeSchemaContent.slice(previewIndex, fullIndex);
expect(previewSection).toContain('"title"');
expect(previewSection).toContain('"summary"');
expect(previewSection).not.toContain('"content"');
expect(previewSection).not.toContain('"tags"');
});

test('type_schema full section contains all fields', () => {
const config: SystemPromptV1 = {
corePrompt: 'You are a helpful assistant.',
tools: [],
dataComponents: [],
artifacts: [makeArtifact({ type: 'ResearchDoc' })],
allProjectArtifactComponents: testArtifactComponents,
};

const result = builder.buildSystemPrompt(config);
const typeSchemaMatch = result.prompt.match(/<type_schema>([\s\S]*?)<\/type_schema>/);
const typeSchemaContent = typeSchemaMatch?.[1] ?? '';
const fullIndex = typeSchemaContent.indexOf('FULL');
const fullSection = typeSchemaContent.slice(fullIndex);
expect(fullSection).toContain('"title"');
expect(fullSection).toContain('"summary"');
expect(fullSection).toContain('"content"');
expect(fullSection).toContain('"tags"');
});

test('type_schema shows "Schema not available" when type is not in component map', () => {
const config: SystemPromptV1 = {
corePrompt: 'You are a helpful assistant.',
tools: [],
dataComponents: [],
artifacts: [makeArtifact({ type: 'UnknownType' })],
allProjectArtifactComponents: testArtifactComponents,
};

const result = builder.buildSystemPrompt(config);
expect(result.prompt).toContain('Schema not available');
});

test('uses allProjectArtifactComponents for type schema map over artifactComponents', () => {
const differentComponents = [
{
id: 'comp-2',
name: 'SpecialDoc',
description: 'Special doc',
props: {
type: 'object',
properties: {
headline: { type: 'string', inPreview: true },
},
},
},
];

const config: SystemPromptV1 = {
corePrompt: 'You are a helpful assistant.',
tools: [],
dataComponents: [],
artifacts: [makeArtifact({ type: 'SpecialDoc' })],
artifactComponents: testArtifactComponents,
allProjectArtifactComponents: differentComponents,
};

const result = builder.buildSystemPrompt(config);
expect(result.prompt).toContain('<type>SpecialDoc</type>');
expect(result.prompt).not.toContain('Schema not available');
expect(result.prompt).toContain('"headline"');
});

test('falls back to artifactComponents when allProjectArtifactComponents is absent', () => {
const config: SystemPromptV1 = {
corePrompt: 'You are a helpful assistant.',
tools: [],
dataComponents: [],
artifacts: [makeArtifact({ type: 'ResearchDoc' })],
artifactComponents: testArtifactComponents,
};

const result = builder.buildSystemPrompt(config);
expect(result.prompt).toContain('<type>ResearchDoc</type>');
expect(result.prompt).not.toContain('Schema not available');
});

test('handles artifact with empty allProjectArtifactComponents gracefully', () => {
const config: SystemPromptV1 = {
corePrompt: 'You are a helpful assistant.',
tools: [],
dataComponents: [],
artifacts: [makeArtifact({ type: 'ResearchDoc' })],
allProjectArtifactComponents: [],
};

const result = builder.buildSystemPrompt(config);
expect(result.prompt).toContain('Schema not available');
});
});
130 changes: 130 additions & 0 deletions agents-api/src/__tests__/run/data/conversations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { describe, expect, it } from 'vitest';
import { reconstructMessageText } from '../../../domains/run/data/conversations';

describe('reconstructMessageText', () => {
it('falls back to content.text when content has no parts array', () => {
const msg = { content: { text: 'Hello world' } };
expect(reconstructMessageText(msg)).toBe('Hello world');
});

it('falls back to content.text when parts is empty', () => {
const msg = { content: { text: 'fallback text', parts: [] } };
expect(reconstructMessageText(msg)).toBe('fallback text');
});

it('returns empty string when content has no text and no parts', () => {
const msg = { content: {} };
expect(reconstructMessageText(msg)).toBe('');
});

it('concatenates text parts in order', () => {
const msg = {
content: {
parts: [
{ type: 'text', text: 'Hello ' },
{ type: 'text', text: 'world' },
],
},
};
expect(reconstructMessageText(msg)).toBe('Hello world');
});

it('converts data parts with artifactId + toolCallId to artifact:ref tags', () => {
const msg = {
content: {
parts: [{ type: 'data', data: { artifactId: 'art-1', toolCallId: 'tool-1' } }],
},
};
expect(reconstructMessageText(msg)).toBe('<artifact:ref id="art-1" tool="tool-1" />');
});

it('interleaves text and artifact:ref tags correctly', () => {
const msg = {
content: {
parts: [
{ type: 'text', text: 'Here is the result. ' },
{ type: 'data', data: { artifactId: 'art-abc', toolCallId: 'toolu_xyz' } },
{ type: 'text', text: ' And more text.' },
],
},
};
expect(reconstructMessageText(msg)).toBe(
'Here is the result. <artifact:ref id="art-abc" tool="toolu_xyz" /> And more text.'
);
});

it('handles data parts with JSON string data', () => {
const msg = {
content: {
parts: [
{
type: 'data',
data: JSON.stringify({ artifactId: 'art-json', toolCallId: 'tool-json' }),
},
],
},
};
expect(reconstructMessageText(msg)).toBe('<artifact:ref id="art-json" tool="tool-json" />');
});

it('ignores data parts without artifactId', () => {
const msg = {
content: {
parts: [{ type: 'data', data: { toolCallId: 'tool-1' } }],
},
};
expect(reconstructMessageText(msg)).toBe('');
});

it('ignores data parts without toolCallId', () => {
const msg = {
content: {
parts: [{ type: 'data', data: { artifactId: 'art-1' } }],
},
};
expect(reconstructMessageText(msg)).toBe('');
});

it('ignores data parts with unparseable JSON string', () => {
const msg = {
content: {
parts: [{ type: 'data', data: 'not-valid-json' }],
},
};
expect(reconstructMessageText(msg)).toBe('');
});

it('returns empty string for unknown part types', () => {
const msg = {
content: {
parts: [{ type: 'image', url: 'http://example.com/img.png' }],
},
};
expect(reconstructMessageText(msg)).toBe('');
});

it('handles multiple artifact refs in a single message', () => {
const msg = {
content: {
parts: [
{ type: 'text', text: 'First: ' },
{ type: 'data', data: { artifactId: 'art-1', toolCallId: 'tool-1' } },
{ type: 'text', text: ' Second: ' },
{ type: 'data', data: { artifactId: 'art-2', toolCallId: 'tool-2' } },
],
},
};
expect(reconstructMessageText(msg)).toBe(
'First: <artifact:ref id="art-1" tool="tool-1" /> Second: <artifact:ref id="art-2" tool="tool-2" />'
);
});

it('handles missing text property in text part gracefully', () => {
const msg = {
content: {
parts: [{ type: 'text' }],
},
};
expect(reconstructMessageText(msg)).toBe('');
});
});
Loading
Loading