@amalgm/agents 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PURPOSE.md +27 -25
- package/README.md +46 -19
- package/dist/agents.d.ts +26 -4
- package/dist/agents.js +87 -4
- package/dist/bin/mcp.js +1 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +13 -2
- package/dist/cli/open.d.ts +1 -1
- package/dist/cli/open.js +5 -1
- package/dist/cli/run.js +8 -3
- package/dist/cli/session-commands.d.ts +4 -0
- package/dist/cli/session-commands.js +54 -0
- package/dist/drivers.d.ts +4 -0
- package/dist/drivers.js +25 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +2 -0
- package/dist/event-store.d.ts +13 -0
- package/dist/event-store.js +50 -0
- package/dist/http/server.js +7 -1
- package/dist/http/session-routes.d.ts +2 -0
- package/dist/http/session-routes.js +67 -0
- package/dist/http/stream.d.ts +3 -0
- package/dist/http/stream.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/agent-tools.js +1 -1
- package/dist/mcp/session-tools.d.ts +3 -0
- package/dist/mcp/session-tools.js +81 -0
- package/dist/mcp/tools.js +2 -1
- package/dist/messages.d.ts +3 -0
- package/dist/messages.js +56 -0
- package/dist/rows.d.ts +4 -1
- package/dist/rows.js +39 -0
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.js +176 -0
- package/dist/schema.js +101 -53
- package/dist/session-store.d.ts +15 -0
- package/dist/session-store.js +83 -0
- package/dist/turn-store.d.ts +31 -0
- package/dist/turn-store.js +164 -0
- package/dist/types.d.ts +106 -0
- package/docs/ARCHITECTURE.md +33 -21
- package/docs/CLI.md +43 -3
- package/docs/DATA_MODEL.md +46 -7
- package/docs/DEFINITIONS.md +4 -4
- package/docs/DRIVERS.md +72 -0
- package/docs/ENGINE_INTEGRATION.md +67 -22
- package/docs/MCP.md +19 -3
- package/docs/REST.md +97 -14
- package/docs/SDK.md +61 -12
- package/docs/SECURITY.md +35 -6
- package/examples/basic.ts +22 -8
- package/package.json +5 -5
- package/skills/amalgm-agents/SKILL.md +12 -3
- package/skills/amalgm-agents/agents/openai.yaml +2 -2
package/dist/http/server.js
CHANGED
|
@@ -5,7 +5,9 @@ import { routeAgentConfig } from './config-routes.js';
|
|
|
5
5
|
import { routeAgentBundles } from './bundle-routes.js';
|
|
6
6
|
import { routeAgents } from './agent-routes.js';
|
|
7
7
|
import { guardRequest, readJson, sendJson } from './request.js';
|
|
8
|
+
import { routeSessions } from './session-routes.js';
|
|
8
9
|
import { routeSkills } from './skill-routes.js';
|
|
10
|
+
import { streamEvents } from './stream.js';
|
|
9
11
|
export function createRestServer(options = {}) {
|
|
10
12
|
const agents = options.agents || new Agents(options);
|
|
11
13
|
const owned = !options.agents;
|
|
@@ -55,7 +57,11 @@ export function createRestServer(options = {}) {
|
|
|
55
57
|
sendJson(response, 404, { error: { code: 'not_found', message: 'Route not found.' } });
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
58
|
-
if (
|
|
60
|
+
if (path[0] === 'sessions' && path[2] === 'events' && path[3] === 'stream' && request.method === 'GET') {
|
|
61
|
+
streamEvents(agents, path[1] || '', request, response, Number(url.searchParams.get('after') || 0));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (await routeAgents(context) || await routeSessions(context))
|
|
59
65
|
return;
|
|
60
66
|
sendJson(response, 404, { error: { code: 'not_found', message: 'Route not found.' } });
|
|
61
67
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AgentError } from '../errors.js';
|
|
2
|
+
export async function routeSessions(context) {
|
|
3
|
+
const [resource, id, child] = context.path;
|
|
4
|
+
if (resource !== 'sessions')
|
|
5
|
+
return false;
|
|
6
|
+
if (!id && context.method === 'GET') {
|
|
7
|
+
const agentId = context.url.searchParams.get('agent_id') || undefined;
|
|
8
|
+
const archived = context.url.searchParams.get('include_archived') === 'true';
|
|
9
|
+
context.json(200, { sessions: context.agents.listSessions(agentId, archived) });
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (!id && context.method === 'POST') {
|
|
13
|
+
const body = await context.body();
|
|
14
|
+
if (typeof body.agentId !== 'string')
|
|
15
|
+
throw new AgentError('invalid_input', 'agentId is required.');
|
|
16
|
+
context.json(201, { session: context.agents.startSession({
|
|
17
|
+
agentId: body.agentId,
|
|
18
|
+
...(typeof body.revisionId === 'string' ? { revisionId: body.revisionId } : {}),
|
|
19
|
+
...(typeof body.sessionId === 'string' ? { sessionId: body.sessionId } : {}),
|
|
20
|
+
...(body.metadata ? { metadata: body.metadata } : {}),
|
|
21
|
+
}) });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (!id)
|
|
25
|
+
throw new AgentError('not_found', 'Route not found.');
|
|
26
|
+
if (!child && context.method === 'GET') {
|
|
27
|
+
const session = context.agents.getSession(id);
|
|
28
|
+
if (!session)
|
|
29
|
+
throw new AgentError('not_found', `Session not found: ${id}`);
|
|
30
|
+
context.json(200, { session });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (!child && context.method === 'DELETE') {
|
|
34
|
+
context.json(200, { session: context.agents.archiveSession(id) });
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (child === 'events' && context.method === 'GET') {
|
|
38
|
+
const after = Number(context.url.searchParams.get('after') || 0);
|
|
39
|
+
const limit = Number(context.url.searchParams.get('limit') || 200);
|
|
40
|
+
context.json(200, { events: context.agents.listEvents(id, after, limit) });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (child === 'turns' && context.method === 'GET') {
|
|
44
|
+
context.json(200, { turns: context.agents.listTurns(id) });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (child === 'cancel' && context.method === 'POST') {
|
|
48
|
+
context.json(202, { turn: context.agents.cancelSession(id) });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (child === 'messages' && context.method === 'POST') {
|
|
52
|
+
const body = await context.body();
|
|
53
|
+
if (body.message === undefined)
|
|
54
|
+
throw new AgentError('invalid_input', 'message is required.');
|
|
55
|
+
const enqueued = context.agents.enqueue(id, {
|
|
56
|
+
message: body.message,
|
|
57
|
+
...(typeof body.idempotencyKey === 'string' ? { idempotencyKey: body.idempotencyKey } : {}),
|
|
58
|
+
});
|
|
59
|
+
if (body.wait === false) {
|
|
60
|
+
context.json(202, { turn: enqueued.turn, duplicate: enqueued.duplicate });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
context.json(200, { turn: await enqueued.completion, duplicate: enqueued.duplicate });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AgentError } from '../errors.js';
|
|
2
|
+
export function streamEvents(agents, sessionId, request, response, after = 0) {
|
|
3
|
+
if (!agents.getSession(sessionId))
|
|
4
|
+
throw new AgentError('not_found', `Session not found: ${sessionId}`);
|
|
5
|
+
response.writeHead(200, {
|
|
6
|
+
'content-type': 'text/event-stream',
|
|
7
|
+
'cache-control': 'no-cache, no-transform',
|
|
8
|
+
connection: 'keep-alive',
|
|
9
|
+
});
|
|
10
|
+
response.flushHeaders();
|
|
11
|
+
let last = after;
|
|
12
|
+
const write = (event) => {
|
|
13
|
+
if (event.sequence <= last || response.destroyed)
|
|
14
|
+
return;
|
|
15
|
+
last = event.sequence;
|
|
16
|
+
response.write(`id: ${event.sequence}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
17
|
+
};
|
|
18
|
+
for (const event of agents.listEvents(sessionId, after, 1000))
|
|
19
|
+
write(event);
|
|
20
|
+
const unsubscribe = agents.subscribe(sessionId, write);
|
|
21
|
+
const heartbeat = setInterval(() => {
|
|
22
|
+
if (!response.destroyed)
|
|
23
|
+
response.write(': heartbeat\n\n');
|
|
24
|
+
}, 15_000);
|
|
25
|
+
heartbeat.unref();
|
|
26
|
+
request.once('close', () => {
|
|
27
|
+
clearInterval(heartbeat);
|
|
28
|
+
unsubscribe();
|
|
29
|
+
});
|
|
30
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { Agents } from './agents.js';
|
|
2
2
|
export { AgentError, asAgentError } from './errors.js';
|
|
3
|
+
export { defineDriver, driverSpecifiers, loadDriverModules } from './drivers.js';
|
|
3
4
|
export { definitionHash, normalizeDefinition, patchDefinition } from './definition.js';
|
|
5
|
+
export { messageText, normalizeMessage } from './messages.js';
|
|
4
6
|
export type * from './types.js';
|
|
5
7
|
export { buildCanonicalSkillRoots, classifySkillRoot } from './skills/roots.js';
|
|
6
8
|
export type { SkillRootClassification, SkillRootOptions } from './skills/roots.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { Agents } from './agents.js';
|
|
2
2
|
export { AgentError, asAgentError } from './errors.js';
|
|
3
|
+
export { defineDriver, driverSpecifiers, loadDriverModules } from './drivers.js';
|
|
3
4
|
export { definitionHash, normalizeDefinition, patchDefinition } from './definition.js';
|
|
5
|
+
export { messageText, normalizeMessage } from './messages.js';
|
|
4
6
|
export { buildCanonicalSkillRoots, classifySkillRoot } from './skills/roots.js';
|
|
5
7
|
export { parseSkillFrontmatter, scanInstalledSkills } from './skills/scanner.js';
|
|
6
8
|
export { normalizeAgentConfig, stableConfigId } from './config/schema.js';
|
package/dist/mcp/agent-tools.js
CHANGED
|
@@ -41,7 +41,7 @@ export function agentTools(agents) {
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
name: 'agents_delete',
|
|
44
|
-
description: '
|
|
44
|
+
description: 'Delete an agent while retaining immutable session history.',
|
|
45
45
|
inputSchema: objectSchema({ agent_id: string }, ['agent_id']),
|
|
46
46
|
handler(input) {
|
|
47
47
|
return toolResult({ agent: agents.deleteAgent(stringArg(input, 'agent_id')) });
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AgentError } from '../errors.js';
|
|
2
|
+
import { objectSchema, toolResult } from './helpers.js';
|
|
3
|
+
const string = { type: 'string' };
|
|
4
|
+
function text(input, ...keys) {
|
|
5
|
+
for (const key of keys) {
|
|
6
|
+
if (typeof input[key] === 'string' && input[key])
|
|
7
|
+
return input[key];
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
function resolveAgentId(agents, requested) {
|
|
12
|
+
const normalized = requested.toLowerCase();
|
|
13
|
+
const matches = agents.listAgents().filter(({ id, definition }) => {
|
|
14
|
+
return id.toLowerCase() === normalized || definition.name.toLowerCase() === normalized;
|
|
15
|
+
});
|
|
16
|
+
if (matches.length === 1)
|
|
17
|
+
return matches[0].id;
|
|
18
|
+
if (matches.length > 1)
|
|
19
|
+
throw new AgentError('conflict', `Agent name is ambiguous: ${requested}`);
|
|
20
|
+
throw new AgentError('not_found', `Agent not found: ${requested}`);
|
|
21
|
+
}
|
|
22
|
+
export function sessionTools(agents) {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
name: 'agents_get_conversation',
|
|
26
|
+
description: 'Read one durable agent session, including its turns and ordered events.',
|
|
27
|
+
inputSchema: objectSchema({ conversation_id: string, session_id: string, after: { type: 'number' } }),
|
|
28
|
+
handler(input) {
|
|
29
|
+
const id = text(input, 'conversation_id', 'session_id');
|
|
30
|
+
if (!id)
|
|
31
|
+
throw new AgentError('invalid_input', 'conversation_id is required.');
|
|
32
|
+
return toolResult({
|
|
33
|
+
conversation_id: id,
|
|
34
|
+
session: agents.getSession(id),
|
|
35
|
+
turns: agents.listTurns(id),
|
|
36
|
+
events: agents.listEvents(id, Number(input.after || 0)),
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'talk_to_agent',
|
|
42
|
+
description: 'Start or continue a durable agent session. Agents sessions are not Chat conversations.',
|
|
43
|
+
inputSchema: objectSchema({
|
|
44
|
+
agent: string,
|
|
45
|
+
agent_id: string,
|
|
46
|
+
conversation_id: string,
|
|
47
|
+
session_id: string,
|
|
48
|
+
description: string,
|
|
49
|
+
prompt: string,
|
|
50
|
+
message: { type: ['string', 'object'] },
|
|
51
|
+
idempotency_key: string,
|
|
52
|
+
run_in_background: { type: 'boolean' },
|
|
53
|
+
}),
|
|
54
|
+
async handler(input) {
|
|
55
|
+
const requestedAgent = text(input, 'agent', 'agent_id');
|
|
56
|
+
if (!requestedAgent)
|
|
57
|
+
throw new AgentError('invalid_input', 'agent is required.');
|
|
58
|
+
const agentId = resolveAgentId(agents, requestedAgent);
|
|
59
|
+
const message = input.message ?? input.prompt;
|
|
60
|
+
if (message === undefined)
|
|
61
|
+
throw new AgentError('invalid_input', 'prompt or message is required.');
|
|
62
|
+
const sessionId = text(input, 'conversation_id', 'session_id');
|
|
63
|
+
const idempotencyKey = text(input, 'idempotency_key');
|
|
64
|
+
const call = agents.talk(agentId, {
|
|
65
|
+
message: message,
|
|
66
|
+
...(sessionId ? { sessionId } : {}),
|
|
67
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
68
|
+
});
|
|
69
|
+
const base = {
|
|
70
|
+
conversation_id: call.session.id,
|
|
71
|
+
session: call.session,
|
|
72
|
+
turn: call.turn,
|
|
73
|
+
duplicate: call.duplicate,
|
|
74
|
+
};
|
|
75
|
+
if (input.run_in_background === true)
|
|
76
|
+
return toolResult(base);
|
|
77
|
+
return toolResult({ ...base, turn: await call.completion });
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
package/dist/mcp/tools.js
CHANGED
package/dist/messages.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AgentError } from './errors.js';
|
|
2
|
+
import { isObject } from './json.js';
|
|
3
|
+
function normalizePart(value) {
|
|
4
|
+
if (!isObject(value))
|
|
5
|
+
throw new AgentError('invalid_input', 'message parts must be objects.');
|
|
6
|
+
if (value.type === 'text') {
|
|
7
|
+
if (typeof value.text !== 'string' || !value.text) {
|
|
8
|
+
throw new AgentError('invalid_input', 'text parts require non-empty text.');
|
|
9
|
+
}
|
|
10
|
+
return { type: 'text', text: value.text };
|
|
11
|
+
}
|
|
12
|
+
if (value.type === 'reference') {
|
|
13
|
+
if (typeof value.uri !== 'string' || !value.uri.trim()) {
|
|
14
|
+
throw new AgentError('invalid_input', 'reference parts require uri.');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
type: 'reference',
|
|
18
|
+
uri: value.uri.trim(),
|
|
19
|
+
name: typeof value.name === 'string' && value.name.trim() ? value.name.trim() : null,
|
|
20
|
+
mediaType: typeof value.mediaType === 'string' && value.mediaType.trim()
|
|
21
|
+
? value.mediaType.trim()
|
|
22
|
+
: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (value.type === 'data') {
|
|
26
|
+
if (typeof value.name !== 'string' || !value.name.trim() || value.data === undefined) {
|
|
27
|
+
throw new AgentError('invalid_input', 'data parts require name and data.');
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
type: 'data',
|
|
31
|
+
name: value.name.trim(),
|
|
32
|
+
data: JSON.parse(JSON.stringify(value.data)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
throw new AgentError('invalid_input', `Unsupported message part: ${String(value.type)}`);
|
|
36
|
+
}
|
|
37
|
+
export function normalizeMessage(value, role = 'user') {
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
if (!value)
|
|
40
|
+
throw new AgentError('invalid_input', 'message must not be empty.');
|
|
41
|
+
return { role, parts: [{ type: 'text', text: value }] };
|
|
42
|
+
}
|
|
43
|
+
if (!isObject(value) || !Array.isArray(value.parts) || value.parts.length === 0) {
|
|
44
|
+
throw new AgentError('invalid_input', 'message requires at least one part.');
|
|
45
|
+
}
|
|
46
|
+
const validRoles = new Set(['user', 'assistant', 'system']);
|
|
47
|
+
if (!validRoles.has(value.role))
|
|
48
|
+
throw new AgentError('invalid_input', 'message role is invalid.');
|
|
49
|
+
return { role: value.role, parts: value.parts.map(normalizePart) };
|
|
50
|
+
}
|
|
51
|
+
export function messageText(message) {
|
|
52
|
+
return message.parts
|
|
53
|
+
.filter((part) => part.type === 'text')
|
|
54
|
+
.map((part) => part.text)
|
|
55
|
+
.join('');
|
|
56
|
+
}
|
package/dist/rows.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { AgentRecord, AgentRevision } from './types.js';
|
|
1
|
+
import type { AgentRecord, AgentRevision, AgentSession, AgentTurn, SessionEvent } from './types.js';
|
|
2
2
|
export type Row = Record<string, unknown>;
|
|
3
3
|
export declare function revisionFromRow(row: Row): AgentRevision;
|
|
4
4
|
export declare function agentFromRow(row: Row): AgentRecord;
|
|
5
|
+
export declare function sessionFromRow(row: Row): AgentSession;
|
|
6
|
+
export declare function turnFromRow(row: Row): AgentTurn;
|
|
7
|
+
export declare function eventFromRow(row: Row): SessionEvent;
|
package/dist/rows.js
CHANGED
|
@@ -24,3 +24,42 @@ export function agentFromRow(row) {
|
|
|
24
24
|
deletedAt: nullable(row.deleted_at),
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
export function sessionFromRow(row) {
|
|
28
|
+
return {
|
|
29
|
+
id: String(row.id),
|
|
30
|
+
agentId: String(row.agent_id),
|
|
31
|
+
agentRevisionId: String(row.agent_revision_id),
|
|
32
|
+
agentRevision: Number(row.agent_revision_number),
|
|
33
|
+
status: String(row.status),
|
|
34
|
+
driverSessionId: nullable(row.driver_session_id),
|
|
35
|
+
metadata: parseObject(String(row.metadata_json)),
|
|
36
|
+
createdAt: String(row.created_at),
|
|
37
|
+
updatedAt: String(row.updated_at),
|
|
38
|
+
archivedAt: nullable(row.archived_at),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function turnFromRow(row) {
|
|
42
|
+
return {
|
|
43
|
+
id: String(row.id),
|
|
44
|
+
sessionId: String(row.session_id),
|
|
45
|
+
idempotencyKey: String(row.idempotency_key),
|
|
46
|
+
status: String(row.status),
|
|
47
|
+
input: JSON.parse(String(row.input_json)),
|
|
48
|
+
result: row.result_json ? parseObject(String(row.result_json)) : null,
|
|
49
|
+
error: row.error_json ? parseObject(String(row.error_json)) : null,
|
|
50
|
+
createdAt: String(row.created_at),
|
|
51
|
+
startedAt: nullable(row.started_at),
|
|
52
|
+
completedAt: nullable(row.completed_at),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function eventFromRow(row) {
|
|
56
|
+
return {
|
|
57
|
+
id: String(row.id),
|
|
58
|
+
sessionId: String(row.session_id),
|
|
59
|
+
turnId: nullable(row.turn_id),
|
|
60
|
+
sequence: Number(row.sequence),
|
|
61
|
+
type: String(row.type),
|
|
62
|
+
data: parseObject(String(row.data_json)),
|
|
63
|
+
createdAt: String(row.created_at),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentStore } from './agent-store.js';
|
|
2
|
+
import type { EventStore } from './event-store.js';
|
|
3
|
+
import type { SessionStore } from './session-store.js';
|
|
4
|
+
import type { TurnStore } from './turn-store.js';
|
|
5
|
+
import type { AgentDriver, AgentTurn, EnqueuedTurn, SendInput } from './types.js';
|
|
6
|
+
export interface RuntimeLimits {
|
|
7
|
+
maxInputBytes: number;
|
|
8
|
+
maxEventBytes: number;
|
|
9
|
+
turnTimeoutMs: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class AgentRuntime {
|
|
12
|
+
private readonly agents;
|
|
13
|
+
private readonly sessions;
|
|
14
|
+
private readonly turns;
|
|
15
|
+
private readonly events;
|
|
16
|
+
private readonly limits;
|
|
17
|
+
private readonly drivers;
|
|
18
|
+
private readonly active;
|
|
19
|
+
constructor(agents: AgentStore, sessions: SessionStore, turns: TurnStore, events: EventStore, limits: RuntimeLimits, drivers?: AgentDriver[]);
|
|
20
|
+
register(driver: AgentDriver): void;
|
|
21
|
+
driverIds(): string[];
|
|
22
|
+
enqueue(sessionId: string, input: SendInput): EnqueuedTurn;
|
|
23
|
+
send(sessionId: string, input: SendInput): Promise<AgentTurn>;
|
|
24
|
+
cancel(sessionId: string): AgentTurn;
|
|
25
|
+
shutdown(): Promise<void>;
|
|
26
|
+
private execute;
|
|
27
|
+
private emit;
|
|
28
|
+
private normalizeResult;
|
|
29
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { AgentError, asAgentError } from './errors.js';
|
|
2
|
+
import { assertByteLimit, isObject } from './json.js';
|
|
3
|
+
import { normalizeMessage } from './messages.js';
|
|
4
|
+
export class AgentRuntime {
|
|
5
|
+
agents;
|
|
6
|
+
sessions;
|
|
7
|
+
turns;
|
|
8
|
+
events;
|
|
9
|
+
limits;
|
|
10
|
+
drivers = new Map();
|
|
11
|
+
active = new Map();
|
|
12
|
+
constructor(agents, sessions, turns, events, limits, drivers = []) {
|
|
13
|
+
this.agents = agents;
|
|
14
|
+
this.sessions = sessions;
|
|
15
|
+
this.turns = turns;
|
|
16
|
+
this.events = events;
|
|
17
|
+
this.limits = limits;
|
|
18
|
+
for (const driver of drivers)
|
|
19
|
+
this.register(driver);
|
|
20
|
+
}
|
|
21
|
+
register(driver) {
|
|
22
|
+
if (!driver || typeof driver.id !== 'string' || typeof driver.run !== 'function') {
|
|
23
|
+
throw new AgentError('invalid_input', 'Agent drivers require id and run.');
|
|
24
|
+
}
|
|
25
|
+
if (this.drivers.has(driver.id)) {
|
|
26
|
+
throw new AgentError('conflict', `Agent driver already registered: ${driver.id}`);
|
|
27
|
+
}
|
|
28
|
+
this.drivers.set(driver.id, driver);
|
|
29
|
+
}
|
|
30
|
+
driverIds() {
|
|
31
|
+
return [...this.drivers.keys()].sort();
|
|
32
|
+
}
|
|
33
|
+
enqueue(sessionId, input) {
|
|
34
|
+
const message = normalizeMessage(input.message, 'user');
|
|
35
|
+
assertByteLimit(message, this.limits.maxInputBytes, 'message');
|
|
36
|
+
const reserved = this.turns.reserve(sessionId, message, input.idempotencyKey);
|
|
37
|
+
if (reserved.duplicate) {
|
|
38
|
+
const running = this.active.get(reserved.turn.id);
|
|
39
|
+
return {
|
|
40
|
+
turn: reserved.turn,
|
|
41
|
+
completion: running?.completion || Promise.resolve(reserved.turn),
|
|
42
|
+
duplicate: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const completion = this.execute(reserved.turn, controller);
|
|
47
|
+
this.active.set(reserved.turn.id, { controller, completion });
|
|
48
|
+
void completion.then(() => this.active.delete(reserved.turn.id), () => this.active.delete(reserved.turn.id));
|
|
49
|
+
return { turn: reserved.turn, completion, duplicate: false };
|
|
50
|
+
}
|
|
51
|
+
async send(sessionId, input) {
|
|
52
|
+
return this.enqueue(sessionId, input).completion;
|
|
53
|
+
}
|
|
54
|
+
cancel(sessionId) {
|
|
55
|
+
const turn = this.turns.active(sessionId);
|
|
56
|
+
if (!turn)
|
|
57
|
+
throw new AgentError('conflict', 'Session has no active turn.');
|
|
58
|
+
const execution = this.active.get(turn.id);
|
|
59
|
+
if (!execution)
|
|
60
|
+
throw new AgentError('conflict', 'Active turn is not owned by this process.');
|
|
61
|
+
const cancelling = this.turns.requestCancellation(turn.id);
|
|
62
|
+
execution.controller.abort(new AgentError('cancelled', 'Turn cancelled by caller.'));
|
|
63
|
+
return cancelling;
|
|
64
|
+
}
|
|
65
|
+
async shutdown() {
|
|
66
|
+
const executions = [...this.active.entries()];
|
|
67
|
+
for (const [turnId, execution] of executions) {
|
|
68
|
+
const turn = this.turns.get(turnId);
|
|
69
|
+
if (turn && (turn.status === 'queued' || turn.status === 'running')) {
|
|
70
|
+
this.turns.requestCancellation(turnId);
|
|
71
|
+
}
|
|
72
|
+
execution.controller.abort(new Error('Agents service shutting down.'));
|
|
73
|
+
}
|
|
74
|
+
await Promise.allSettled(executions.map(([, execution]) => execution.completion));
|
|
75
|
+
}
|
|
76
|
+
async execute(turn, controller) {
|
|
77
|
+
const session = this.sessions.require(turn.sessionId);
|
|
78
|
+
const revision = this.agents.revision(session.agentId, session.agentRevisionId);
|
|
79
|
+
const driver = this.drivers.get(revision.definition.driver.id);
|
|
80
|
+
if (!driver) {
|
|
81
|
+
return this.turns.fail(turn.id, failure('driver_unavailable', `Agent driver is not registered: ${revision.definition.driver.id}`));
|
|
82
|
+
}
|
|
83
|
+
this.turns.markRunning(turn.id);
|
|
84
|
+
let timedOut = false;
|
|
85
|
+
const timer = this.limits.turnTimeoutMs > 0
|
|
86
|
+
? setTimeout(() => {
|
|
87
|
+
timedOut = true;
|
|
88
|
+
controller.abort(new Error('Turn timed out.'));
|
|
89
|
+
}, this.limits.turnTimeoutMs)
|
|
90
|
+
: null;
|
|
91
|
+
timer?.unref();
|
|
92
|
+
try {
|
|
93
|
+
const result = await driver.run({
|
|
94
|
+
agent: revision,
|
|
95
|
+
session,
|
|
96
|
+
turn: this.turns.require(turn.id),
|
|
97
|
+
history: this.turns.history(session.id, turn.id),
|
|
98
|
+
input: turn.input,
|
|
99
|
+
driverSessionId: session.driverSessionId,
|
|
100
|
+
}, {
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
emit: (event) => this.emit(session.id, turn.id, event),
|
|
103
|
+
});
|
|
104
|
+
if (controller.signal.aborted) {
|
|
105
|
+
if (this.turns.require(turn.id).status === 'cancelling')
|
|
106
|
+
return this.turns.cancel(turn.id);
|
|
107
|
+
return this.turns.fail(turn.id, failure('turn_timeout', 'Turn timed out.'));
|
|
108
|
+
}
|
|
109
|
+
const normalized = this.normalizeResult(result);
|
|
110
|
+
if (normalized.message) {
|
|
111
|
+
const data = {
|
|
112
|
+
message: normalized.message,
|
|
113
|
+
};
|
|
114
|
+
assertByteLimit(data, this.limits.maxEventBytes, 'driver result message');
|
|
115
|
+
this.events.append(session.id, turn.id, 'message', data);
|
|
116
|
+
}
|
|
117
|
+
if (normalized.driverSessionId) {
|
|
118
|
+
this.sessions.setDriverSession(session.id, normalized.driverSessionId);
|
|
119
|
+
}
|
|
120
|
+
const storedResult = {
|
|
121
|
+
metadata: normalized.metadata || {},
|
|
122
|
+
...(normalized.driverSessionId ? { driverSessionId: normalized.driverSessionId } : {}),
|
|
123
|
+
};
|
|
124
|
+
assertByteLimit(storedResult, this.limits.maxEventBytes, 'driver result');
|
|
125
|
+
return this.turns.complete(turn.id, storedResult);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const current = this.turns.require(turn.id);
|
|
129
|
+
if (current.status === 'cancelling')
|
|
130
|
+
return this.turns.cancel(turn.id);
|
|
131
|
+
const normalized = asAgentError(error);
|
|
132
|
+
return this.turns.fail(turn.id, failure(timedOut ? 'turn_timeout' : normalized.code === 'internal' ? 'driver_error' : normalized.code, normalized.message, normalized.details));
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
if (timer)
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
emit(sessionId, turnId, event) {
|
|
140
|
+
if (!event || typeof event.type !== 'string' || !event.type.trim() || !isObject(event.data)) {
|
|
141
|
+
throw new AgentError('invalid_input', 'Driver events require type and object data.');
|
|
142
|
+
}
|
|
143
|
+
if (/^(session|turn)\./.test(event.type)) {
|
|
144
|
+
throw new AgentError('invalid_input', 'Driver events cannot use reserved session.* or turn.* types.');
|
|
145
|
+
}
|
|
146
|
+
assertByteLimit(event.data, this.limits.maxEventBytes, 'driver event');
|
|
147
|
+
if (event.type === 'message' && event.data.message) {
|
|
148
|
+
const message = normalizeMessage(event.data.message, 'assistant');
|
|
149
|
+
if (message.role !== 'assistant') {
|
|
150
|
+
throw new AgentError('invalid_input', 'Driver message events must have assistant role.');
|
|
151
|
+
}
|
|
152
|
+
return this.events.append(sessionId, turnId, 'message', { message: message });
|
|
153
|
+
}
|
|
154
|
+
return this.events.append(sessionId, turnId, event.type.trim(), event.data);
|
|
155
|
+
}
|
|
156
|
+
normalizeResult(result) {
|
|
157
|
+
if (!result)
|
|
158
|
+
return {};
|
|
159
|
+
const normalized = {};
|
|
160
|
+
if (result.message) {
|
|
161
|
+
normalized.message = normalizeMessage(result.message, 'assistant');
|
|
162
|
+
if (normalized.message.role !== 'assistant') {
|
|
163
|
+
throw new AgentError('invalid_input', 'Driver result messages must have assistant role.');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (typeof result.driverSessionId === 'string' && result.driverSessionId.trim()) {
|
|
167
|
+
normalized.driverSessionId = result.driverSessionId.trim();
|
|
168
|
+
}
|
|
169
|
+
if (result.metadata && isObject(result.metadata))
|
|
170
|
+
normalized.metadata = result.metadata;
|
|
171
|
+
return normalized;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function failure(code, message, details = {}) {
|
|
175
|
+
return { code, message, details };
|
|
176
|
+
}
|