Skip to content

Commit 96b257e

Browse files
authored
Merge branch 'feature' into codex/feature-x-card-runtime
2 parents 633a5e6 + 7080d8d commit 96b257e

30 files changed

Lines changed: 3092 additions & 43 deletions
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
AGENT_COMMAND_PROTOCOL,
3+
AGENT_COMMAND_PROTOCOL_VERSION,
4+
agentCommandTypes,
5+
createAgentCommandFactory,
6+
getAgentActionKey,
7+
getAgentCommandActionKey,
8+
getAgentCommandKey,
9+
isAgentCommand,
10+
isAgentCommandEnvelope,
11+
} from '..';
12+
13+
describe('agent commands', () => {
14+
const factory = createAgentCommandFactory({
15+
sessionId: 'session-1',
16+
runId: 'run-1',
17+
now: () => 100,
18+
createCommandId: (type) => `command:${type}`,
19+
createIdempotencyKey: (commandId, type) => `${type}:${commandId}:key`,
20+
});
21+
22+
it('creates commands with defaults and explicit envelope options', () => {
23+
const retry = factory.create('tool.retry', { toolCallId: 'tool-1' });
24+
expect(retry).toEqual({
25+
commandProtocol: AGENT_COMMAND_PROTOCOL,
26+
commandProtocolVersion: AGENT_COMMAND_PROTOCOL_VERSION,
27+
type: 'tool.retry',
28+
commandId: 'command:tool.retry',
29+
idempotencyKey: 'tool.retry:command:tool.retry:key',
30+
sessionId: 'session-1',
31+
runId: 'run-1',
32+
timestamp: 100,
33+
payload: { toolCallId: 'tool-1' },
34+
});
35+
36+
const cancel = factory.create(
37+
'run.cancel',
38+
{ reason: 'stop' },
39+
{
40+
commandId: 'custom-command',
41+
idempotencyKey: 'custom-key',
42+
timestamp: 200,
43+
meta: { source: 'test' },
44+
},
45+
);
46+
expect(cancel).toMatchObject({
47+
commandId: 'custom-command',
48+
idempotencyKey: 'custom-key',
49+
timestamp: 200,
50+
meta: { source: 'test' },
51+
});
52+
53+
const defaultFactory = createAgentCommandFactory({ sessionId: 's', runId: 'r' });
54+
expect(defaultFactory.create('run.cancel', {}).commandId).toContain('r:command:');
55+
});
56+
57+
it('validates every command payload and rejects malformed envelopes', () => {
58+
const approval = factory.create('approval.resolve', {
59+
approvalId: 'approval-1',
60+
decision: 'approved',
61+
expectedVersion: 1,
62+
});
63+
const modified = factory.create('approval.resolve', {
64+
approvalId: 'approval-1',
65+
decision: 'modified',
66+
expectedVersion: 'v2',
67+
});
68+
const retry = factory.create('tool.retry', { toolCallId: 'tool-1' });
69+
const cancel = factory.create('run.cancel', { reason: 'done' });
70+
71+
expect(agentCommandTypes).toEqual(['approval.resolve', 'tool.retry', 'run.cancel']);
72+
expect([approval, modified, retry, cancel].every(isAgentCommand)).toBe(true);
73+
expect(isAgentCommandEnvelope({ ...approval, meta: { trace: true } })).toBe(true);
74+
75+
const invalidEnvelopes = [
76+
null,
77+
[],
78+
{ ...approval, commandProtocol: 'other' },
79+
{ ...approval, commandProtocolVersion: '9' },
80+
{ ...approval, type: 'unknown' },
81+
{ ...approval, commandId: '' },
82+
{ ...approval, idempotencyKey: '' },
83+
{ ...approval, sessionId: '' },
84+
{ ...approval, runId: '' },
85+
{ ...approval, timestamp: Number.NaN },
86+
{ ...approval, payload: [] },
87+
{ ...approval, meta: [] },
88+
];
89+
invalidEnvelopes.forEach((command) => {
90+
expect(isAgentCommandEnvelope(command)).toBe(false);
91+
});
92+
93+
expect(isAgentCommand({ ...approval, payload: { ...approval.payload, approvalId: '' } })).toBe(
94+
false,
95+
);
96+
expect(
97+
isAgentCommand({ ...approval, payload: { ...approval.payload, decision: 'expired' } }),
98+
).toBe(false);
99+
expect(
100+
isAgentCommand({ ...approval, payload: { ...approval.payload, expectedVersion: Infinity } }),
101+
).toBe(false);
102+
expect(isAgentCommand({ ...retry, payload: { toolCallId: '' } })).toBe(false);
103+
expect(isAgentCommand({ ...cancel, payload: { reason: 1 } })).toBe(false);
104+
expect(isAgentCommand(factory.create('run.cancel', {}))).toBe(true);
105+
});
106+
107+
it('creates collision-safe command and action keys', () => {
108+
const approval = factory.create('approval.resolve', {
109+
approvalId: 'approval-1',
110+
decision: 'rejected',
111+
});
112+
const retry = factory.create('tool.retry', { toolCallId: 'tool-1' });
113+
const cancel = factory.create('run.cancel', {});
114+
115+
expect(getAgentCommandKey(approval)).toBe(approval.commandId);
116+
expect(getAgentActionKey({ runId: 'run-1', type: 'run.cancel' })).toBe('5:run-1:run.cancel:');
117+
expect(getAgentCommandActionKey(approval)).toBe('5:run-1:approval.resolve:approval-1');
118+
expect(getAgentCommandActionKey(retry)).toBe('5:run-1:tool.retry:tool-1');
119+
expect(getAgentCommandActionKey(cancel)).toBe('5:run-1:run.cancel:');
120+
});
121+
});
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import type { ApprovalDecision } from '../protocol';
2+
3+
export const AGENT_COMMAND_PROTOCOL = 'agent-command' as const;
4+
export const AGENT_COMMAND_PROTOCOL_VERSION = '0.1' as const;
5+
6+
export type AgentCommandProtocolVersion = typeof AGENT_COMMAND_PROTOCOL_VERSION;
7+
export type AgentCommandDecision = Exclude<ApprovalDecision, 'expired'>;
8+
export type AgentCommandMeta = Readonly<Record<string, unknown>>;
9+
10+
export interface AgentCommandPayloadMap {
11+
'approval.resolve': {
12+
approvalId: string;
13+
decision: AgentCommandDecision;
14+
data?: unknown;
15+
expectedVersion?: string | number;
16+
};
17+
'tool.retry': {
18+
toolCallId: string;
19+
};
20+
'run.cancel': {
21+
reason?: string;
22+
};
23+
}
24+
25+
export type AgentCommandType = keyof AgentCommandPayloadMap;
26+
27+
const agentCommandTypeMap = {
28+
'approval.resolve': true,
29+
'tool.retry': true,
30+
'run.cancel': true,
31+
} satisfies Record<AgentCommandType, true>;
32+
33+
export const agentCommandTypes = Object.keys(agentCommandTypeMap) as AgentCommandType[];
34+
35+
export interface AgentCommandEnvelope<Type extends AgentCommandType> {
36+
commandProtocol: typeof AGENT_COMMAND_PROTOCOL;
37+
commandProtocolVersion: AgentCommandProtocolVersion;
38+
type: Type;
39+
commandId: string;
40+
idempotencyKey: string;
41+
sessionId: string;
42+
runId: string;
43+
timestamp: number;
44+
payload: AgentCommandPayloadMap[Type];
45+
meta?: AgentCommandMeta;
46+
}
47+
48+
export type AgentCommandOf<Type extends AgentCommandType> = AgentCommandEnvelope<Type>;
49+
50+
export type AgentCommand = {
51+
[Type in AgentCommandType]: AgentCommandOf<Type>;
52+
}[AgentCommandType];
53+
54+
export interface UnknownAgentCommandEnvelope {
55+
commandProtocol: typeof AGENT_COMMAND_PROTOCOL;
56+
commandProtocolVersion: AgentCommandProtocolVersion;
57+
type: AgentCommandType;
58+
commandId: string;
59+
idempotencyKey: string;
60+
sessionId: string;
61+
runId: string;
62+
timestamp: number;
63+
payload: Record<string, unknown>;
64+
meta?: AgentCommandMeta;
65+
}
66+
67+
const isRecord = (value: unknown): value is Record<string, unknown> =>
68+
!!value && typeof value === 'object' && !Array.isArray(value);
69+
70+
const hasString = (value: Record<string, unknown>, key: string) =>
71+
typeof value[key] === 'string' && value[key] !== '';
72+
73+
export function isAgentCommandEnvelope(value: unknown): value is UnknownAgentCommandEnvelope {
74+
if (!isRecord(value)) return false;
75+
76+
return (
77+
value.commandProtocol === AGENT_COMMAND_PROTOCOL &&
78+
value.commandProtocolVersion === AGENT_COMMAND_PROTOCOL_VERSION &&
79+
typeof value.type === 'string' &&
80+
agentCommandTypes.includes(value.type as AgentCommandType) &&
81+
hasString(value, 'commandId') &&
82+
hasString(value, 'idempotencyKey') &&
83+
hasString(value, 'sessionId') &&
84+
hasString(value, 'runId') &&
85+
typeof value.timestamp === 'number' &&
86+
Number.isFinite(value.timestamp) &&
87+
isRecord(value.payload) &&
88+
(value.meta === undefined || isRecord(value.meta))
89+
);
90+
}
91+
92+
export function isAgentCommand(value: unknown): value is AgentCommand {
93+
if (!isAgentCommandEnvelope(value)) return false;
94+
const { payload } = value;
95+
96+
switch (value.type) {
97+
case 'approval.resolve':
98+
return (
99+
hasString(payload, 'approvalId') &&
100+
['approved', 'rejected', 'modified'].includes(payload.decision as string) &&
101+
(payload.expectedVersion === undefined ||
102+
typeof payload.expectedVersion === 'string' ||
103+
(typeof payload.expectedVersion === 'number' && Number.isFinite(payload.expectedVersion)))
104+
);
105+
case 'tool.retry':
106+
return hasString(payload, 'toolCallId');
107+
case 'run.cancel':
108+
return payload.reason === undefined || typeof payload.reason === 'string';
109+
}
110+
}
111+
112+
export function getAgentCommandKey(command: AgentCommand): string {
113+
return command.commandId;
114+
}
115+
116+
export interface AgentActionKeyOptions {
117+
runId: string;
118+
type: AgentCommandType;
119+
entityId?: string;
120+
}
121+
122+
export function getAgentActionKey({ runId, type, entityId = '' }: AgentActionKeyOptions): string {
123+
return `${runId.length}:${runId}:${type}:${entityId}`;
124+
}
125+
126+
export function getAgentCommandActionKey(command: AgentCommand): string {
127+
switch (command.type) {
128+
case 'approval.resolve':
129+
return getAgentActionKey({
130+
runId: command.runId,
131+
type: command.type,
132+
entityId: command.payload.approvalId,
133+
});
134+
case 'tool.retry':
135+
return getAgentActionKey({
136+
runId: command.runId,
137+
type: command.type,
138+
entityId: command.payload.toolCallId,
139+
});
140+
case 'run.cancel':
141+
return getAgentActionKey({ runId: command.runId, type: command.type });
142+
}
143+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { AgentCommandOf, AgentCommandType } from './commands';
2+
import { AGENT_COMMAND_PROTOCOL, AGENT_COMMAND_PROTOCOL_VERSION } from './commands';
3+
import type {
4+
AgentCommandFactory,
5+
AgentCommandFactoryOptions,
6+
CreateAgentCommandOptions,
7+
} from './types';
8+
9+
let nextCommandId = 0;
10+
11+
export function createAgentCommandFactory(
12+
options: AgentCommandFactoryOptions,
13+
): AgentCommandFactory {
14+
const now = options.now ?? Date.now;
15+
const createCommandId =
16+
options.createCommandId ??
17+
((type: AgentCommandType) => {
18+
nextCommandId += 1;
19+
return `${options.runId}:command:${nextCommandId}:${type}`;
20+
});
21+
const createIdempotencyKey =
22+
options.createIdempotencyKey ?? ((commandId: string) => `${commandId}:idempotency`);
23+
24+
return {
25+
create(type, payload, commandOptions: CreateAgentCommandOptions = {}) {
26+
const commandId = commandOptions.commandId ?? createCommandId(type);
27+
const command = {
28+
commandProtocol: AGENT_COMMAND_PROTOCOL,
29+
commandProtocolVersion: AGENT_COMMAND_PROTOCOL_VERSION,
30+
type,
31+
commandId,
32+
idempotencyKey: commandOptions.idempotencyKey ?? createIdempotencyKey(commandId, type),
33+
sessionId: options.sessionId,
34+
runId: options.runId,
35+
timestamp: commandOptions.timestamp ?? now(),
36+
payload,
37+
} as AgentCommandOf<typeof type>;
38+
if (commandOptions.meta !== undefined) command.meta = commandOptions.meta;
39+
return command;
40+
},
41+
};
42+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export type {
2+
AgentActionKeyOptions,
3+
AgentCommand,
4+
AgentCommandDecision,
5+
AgentCommandEnvelope,
6+
AgentCommandMeta,
7+
AgentCommandOf,
8+
AgentCommandPayloadMap,
9+
AgentCommandProtocolVersion,
10+
AgentCommandType,
11+
UnknownAgentCommandEnvelope,
12+
} from './commands';
13+
export {
14+
AGENT_COMMAND_PROTOCOL,
15+
AGENT_COMMAND_PROTOCOL_VERSION,
16+
agentCommandTypes,
17+
getAgentActionKey,
18+
getAgentCommandActionKey,
19+
getAgentCommandKey,
20+
isAgentCommand,
21+
isAgentCommandEnvelope,
22+
} from './commands';
23+
export { createAgentCommandFactory } from './factory';
24+
export type {
25+
AgentCommandFactory,
26+
AgentCommandFactoryOptions,
27+
CreateAgentCommandOptions,
28+
} from './types';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type {
2+
AgentCommandMeta,
3+
AgentCommandOf,
4+
AgentCommandPayloadMap,
5+
AgentCommandType,
6+
} from './commands';
7+
8+
export interface AgentCommandFactoryOptions {
9+
sessionId: string;
10+
runId: string;
11+
now?: () => number;
12+
createCommandId?: (type: AgentCommandType) => string;
13+
createIdempotencyKey?: (commandId: string, type: AgentCommandType) => string;
14+
}
15+
16+
export interface CreateAgentCommandOptions {
17+
commandId?: string;
18+
idempotencyKey?: string;
19+
timestamp?: number;
20+
meta?: AgentCommandMeta;
21+
}
22+
23+
export interface AgentCommandFactory {
24+
create<Type extends AgentCommandType>(
25+
type: Type,
26+
payload: AgentCommandPayloadMap[Type],
27+
options?: CreateAgentCommandOptions,
28+
): AgentCommandOf<Type>;
29+
}

packages/x-sdk/src/agent/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export * from './command';
12
export * from './protocol';
23
export * from './reducer';
4+
export * from './selectors';
35
export * from './store';

0 commit comments

Comments
 (0)