chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,239 @@
/**
* End-to-end integration tests for AgentCoreRunner.
*
* These tests wire a ScriptedAgent (mimicking the AG-UI event stream that
* AgentCore produces — including the pathological cases the runner exists
* to paper over) through the real InMemoryAgentRunner machinery, then assert
* the runner's run()/connect() outputs downstream.
*
* If these pass, the published package behaves correctly when consumed by
* a real CopilotRuntime + HttpAgent stack pointed at AWS AgentCore, without
* requiring AWS, Docker, or a browser.
*/
import { describe, it, expect } from "vitest";
import { EMPTY, firstValueFrom } from "rxjs";
import { toArray } from "rxjs/operators";
import {
AbstractAgent,
EventType,
type BaseEvent,
type Message,
type MessagesSnapshotEvent,
type RunAgentInput,
type RunFinishedEvent,
type RunStartedEvent,
type ToolCallResultEvent,
} from "@ag-ui/client";
import type {
AgentRunnerConnectRequest,
AgentRunnerRunRequest,
} from "@copilotkit/runtime/v2";
import { AgentCoreRunner } from "..";
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
interface ScriptedAgentCallbacks {
onEvent: (args: { event: BaseEvent }) => void | Promise<void>;
onRunStartedEvent?: () => void | Promise<void>;
}
class ScriptedAgent extends AbstractAgent {
constructor(private readonly script: BaseEvent[] = []) {
super();
}
async runAgent(
input: RunAgentInput,
callbacks: ScriptedAgentCallbacks,
): Promise<void> {
const runStarted: RunStartedEvent = {
type: EventType.RUN_STARTED,
threadId: input.threadId,
runId: input.runId,
};
await callbacks.onEvent({ event: runStarted });
await callbacks.onRunStartedEvent?.();
for (const event of this.script) await callbacks.onEvent({ event });
const hasRunFinished = this.script.some(
(e) => e.type === EventType.RUN_FINISHED,
);
if (!hasRunFinished) {
const runFinished: RunFinishedEvent = {
type: EventType.RUN_FINISHED,
threadId: input.threadId,
runId: input.runId,
};
await callbacks.onEvent({ event: runFinished });
}
}
clone(): AbstractAgent {
return new ScriptedAgent([...this.script]);
}
protected run(): ReturnType<AbstractAgent["run"]> {
return EMPTY;
}
protected connect(): ReturnType<AbstractAgent["connect"]> {
return EMPTY;
}
}
function makeRunInput(
threadId: string,
runId: string,
messages: Message[] = [],
): RunAgentInput {
return {
threadId,
runId,
parentRunId: undefined,
state: {},
messages,
tools: [],
context: [],
forwardedProps: undefined,
};
}
async function collect(
obs: ReturnType<AgentCoreRunner["run"] | AgentCoreRunner["connect"]>,
): Promise<BaseEvent[]> {
return firstValueFrom(obs.pipe(toArray()));
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("AgentCoreRunner e2e", () => {
it("emits empty snapshot when connect() is called before any run()", async () => {
const runner = new AgentCoreRunner();
const request: AgentRunnerConnectRequest = { threadId: "fresh-thread" };
const events = await collect(runner.connect(request));
expect(events.map((e) => e.type)).toEqual([
EventType.RUN_STARTED,
EventType.MESSAGES_SNAPSHOT,
EventType.RUN_FINISHED,
]);
expect((events[1] as MessagesSnapshotEvent).messages).toEqual([]);
});
it("passes text-only run events through unchanged", async () => {
const runner = new AgentCoreRunner();
const threadId = "thread-text";
const runRequest: AgentRunnerRunRequest = {
threadId,
agent: new ScriptedAgent([
{
type: EventType.TEXT_MESSAGE_START,
messageId: "m-1",
role: "assistant",
},
{
type: EventType.TEXT_MESSAGE_CONTENT,
messageId: "m-1",
delta: "hello",
},
{ type: EventType.TEXT_MESSAGE_END, messageId: "m-1" },
] as BaseEvent[]),
input: makeRunInput(threadId, "run-0", [
{ id: "u-1", role: "user", content: "hi" } as Message,
]),
};
const runEvents = await collect(runner.run(runRequest));
const textContent = runEvents.find(
(e) => e.type === EventType.TEXT_MESSAGE_CONTENT,
);
expect(textContent).toBeDefined();
expect(runEvents.at(-1)?.type).toBe(EventType.RUN_FINISHED);
});
it("synthesises TOOL_CALL_RESULT events before replayed MESSAGES_SNAPSHOT on reconnect", async () => {
const runner = new AgentCoreRunner();
const threadId = "thread-replay";
// Simulate AgentCore's memory replay: during run(), the remote emits a
// MESSAGES_SNAPSHOT that contains assistant messages with toolCalls,
// but *no* corresponding TOOL_CALL_RESULT events. This is the exact
// shape the runner is designed to compensate for.
const replayedSnapshot: MessagesSnapshotEvent = {
type: EventType.MESSAGES_SNAPSHOT,
messages: [
{ id: "u-1", role: "user", content: "what is the weather?" },
{
id: "a-1",
role: "assistant",
content: "",
toolCalls: [
{
id: "tc-1",
type: "function",
function: { name: "getWeather", arguments: "{}" },
},
],
},
] as Message[],
};
await collect(
runner.run({
threadId,
agent: new ScriptedAgent([replayedSnapshot as BaseEvent]),
input: makeRunInput(threadId, "run-0"),
}),
);
const connectEvents = await collect(runner.connect({ threadId }));
const snapshotIndex = connectEvents.findIndex(
(e) => e.type === EventType.MESSAGES_SNAPSHOT,
);
expect(snapshotIndex).toBeGreaterThan(-1);
const toolResults = connectEvents.filter(
(e): e is ToolCallResultEvent => e.type === EventType.TOOL_CALL_RESULT,
);
expect(toolResults).toHaveLength(1);
expect(toolResults[0].toolCallId).toBe("tc-1");
const toolResultIndex = connectEvents.findIndex(
(e) => e.type === EventType.TOOL_CALL_RESULT,
);
expect(toolResultIndex).toBeLessThan(snapshotIndex);
});
it("does not synthesise TOOL_CALL_RESULT events for assistant messages without tool calls", async () => {
const runner = new AgentCoreRunner();
const threadId = "thread-no-tools";
const replayedSnapshot: MessagesSnapshotEvent = {
type: EventType.MESSAGES_SNAPSHOT,
messages: [
{ id: "u-1", role: "user", content: "hi" },
{ id: "a-1", role: "assistant", content: "hello!" },
] as Message[],
};
await collect(
runner.run({
threadId,
agent: new ScriptedAgent([replayedSnapshot as BaseEvent]),
input: makeRunInput(threadId, "run-0"),
}),
);
const connectEvents = await collect(runner.connect({ threadId }));
const toolResults = connectEvents.filter(
(e) => e.type === EventType.TOOL_CALL_RESULT,
);
expect(toolResults).toEqual([]);
});
});
@@ -0,0 +1,24 @@
import { describe, it, expect } from "vitest";
import { firstValueFrom } from "rxjs";
import { toArray } from "rxjs/operators";
import { EventType, type MessagesSnapshotEvent } from "@ag-ui/client";
import type { AgentRunnerConnectRequest } from "@copilotkit/runtime/v2";
import { AgentCoreRunner } from "..";
describe("AgentCoreRunner.connect()", () => {
it("emits empty snapshot for unknown thread", async () => {
const runner = new AgentCoreRunner();
const request: AgentRunnerConnectRequest = { threadId: "unknown-thread" };
const events = await firstValueFrom(
runner.connect(request).pipe(toArray()),
);
expect(events.map((e) => e.type)).toEqual([
EventType.RUN_STARTED,
EventType.MESSAGES_SNAPSHOT,
EventType.RUN_FINISHED,
]);
const snapshot = events[1] as MessagesSnapshotEvent;
expect(snapshot.messages).toEqual([]);
});
});
@@ -0,0 +1,67 @@
import { randomUUID } from "node:crypto";
import {
EventType,
type BaseEvent,
type Message,
type MessagesSnapshotEvent,
type ToolCall,
type ToolCallResultEvent,
} from "@ag-ui/client";
import { InMemoryAgentRunner } from "@copilotkit/runtime/v2";
import { concatMap, Observable, of } from "rxjs";
/**
* AgentCore stores conversation history server-side via AgentCoreMemorySaver /
* AgentCoreMemorySessionManager. When CopilotKit reconnects to an existing
* thread (e.g. page refresh), two issues arise that this runner fixes:
*
* 1. Unknown threads — CopilotKit may call `connect()` for a thread that has
* never had a `run()` (first load). The base runner would error; we emit an
* empty snapshot instead so the UI initialises cleanly.
* 2. Missing tool-call results — AgentCore's replayed history contains assistant
* messages with tool calls but no corresponding TOOL_CALL_RESULT events.
* CopilotKit needs those to reconcile its message state, so we synthesise
* empty results for each past tool call before the snapshot.
*/
export class AgentCoreRunner extends InMemoryAgentRunner {
private readonly knownThreadIds = new Set<string>();
override run(request: Parameters<InMemoryAgentRunner["run"]>[0]) {
if (request.threadId) this.knownThreadIds.add(request.threadId);
return super.run(request);
}
override connect(request: Parameters<InMemoryAgentRunner["connect"]>[0]) {
if (!request.threadId || !this.knownThreadIds.has(request.threadId)) {
const threadId = request.threadId ?? randomUUID();
const runId = randomUUID();
return of(
{ type: EventType.RUN_STARTED, threadId, runId },
{ type: EventType.MESSAGES_SNAPSHOT, messages: [] },
{ type: EventType.RUN_FINISHED, threadId, runId },
) as Observable<BaseEvent>;
}
return super.connect(request).pipe(
concatMap((event: BaseEvent) => {
if (event.type !== EventType.MESSAGES_SNAPSHOT) return of(event);
const snapshot = event as MessagesSnapshotEvent;
const replayedResults: ToolCallResultEvent[] =
snapshot.messages.flatMap((message: Message) => {
if (message.role !== "assistant" || !message.toolCalls?.length)
return [];
return message.toolCalls.map<ToolCallResultEvent>(
(toolCall: ToolCall) => ({
type: EventType.TOOL_CALL_RESULT,
toolCallId: toolCall.id,
messageId: `${toolCall.id}-result`,
content: "",
role: "tool",
}),
);
});
return of(...replayedResults, snapshot) as Observable<BaseEvent>;
}),
);
}
}
+1
View File
@@ -0,0 +1 @@
export * from "./agentcore-runner.js";