@finalai/agent-provider 0.1.0
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.
Potentially problematic release.
This version of @finalai/agent-provider might be problematic. Click here for more details.
- package/README.md +12 -0
- package/dist/modules/channel-context.d.mts +102 -0
- package/dist/modules/channel-context.mjs +52 -0
- package/dist/modules/channel-context.mjs.map +7 -0
- package/dist/modules/channel-socket.d.mts +65 -0
- package/dist/modules/channel-socket.mjs +188 -0
- package/dist/modules/channel-socket.mjs.map +7 -0
- package/dist/modules/channel.d.mts +382 -0
- package/dist/modules/channel.mjs +146 -0
- package/dist/modules/channel.mjs.map +7 -0
- package/dist/modules/conformance/agent.d.mts +20 -0
- package/dist/modules/conformance/agent.mjs +623 -0
- package/dist/modules/conformance/agent.mjs.map +7 -0
- package/dist/modules/conformance/fixtures.d.mts +41 -0
- package/dist/modules/conformance/fixtures.mjs +190 -0
- package/dist/modules/conformance/fixtures.mjs.map +7 -0
- package/dist/modules/conformance/harness-kit.d.mts +82 -0
- package/dist/modules/conformance/harness-kit.mjs +1303 -0
- package/dist/modules/conformance/harness-kit.mjs.map +7 -0
- package/dist/modules/conformance/image.d.mts +2 -0
- package/dist/modules/conformance/image.mjs +79 -0
- package/dist/modules/conformance/image.mjs.map +7 -0
- package/dist/modules/conformance/index.d.mts +8 -0
- package/dist/modules/conformance/index.mjs +12 -0
- package/dist/modules/conformance/index.mjs.map +7 -0
- package/dist/modules/conformance/local-harness.d.mts +25 -0
- package/dist/modules/conformance/local-harness.mjs +108 -0
- package/dist/modules/conformance/local-harness.mjs.map +7 -0
- package/dist/modules/conformance/protocol.d.mts +30 -0
- package/dist/modules/conformance/protocol.mjs +56 -0
- package/dist/modules/conformance/protocol.mjs.map +7 -0
- package/dist/modules/conformance/sandbox-kit.d.mts +19 -0
- package/dist/modules/conformance/sandbox-kit.mjs +320 -0
- package/dist/modules/conformance/sandbox-kit.mjs.map +7 -0
- package/dist/modules/conformance/scripted-model/chat-completions.d.mts +30 -0
- package/dist/modules/conformance/scripted-model/chat-completions.mjs +125 -0
- package/dist/modules/conformance/scripted-model/chat-completions.mjs.map +7 -0
- package/dist/modules/conformance/scripted-model/directives.d.mts +84 -0
- package/dist/modules/conformance/scripted-model/directives.mjs +304 -0
- package/dist/modules/conformance/scripted-model/directives.mjs.map +7 -0
- package/dist/modules/conformance/scripted-model/handler.d.mts +14 -0
- package/dist/modules/conformance/scripted-model/handler.mjs +133 -0
- package/dist/modules/conformance/scripted-model/handler.mjs.map +7 -0
- package/dist/modules/conformance/scripted-model/index.d.mts +6 -0
- package/dist/modules/conformance/scripted-model/index.mjs +7 -0
- package/dist/modules/conformance/scripted-model/index.mjs.map +7 -0
- package/dist/modules/conformance/scripted-model/messages.d.mts +39 -0
- package/dist/modules/conformance/scripted-model/messages.mjs +139 -0
- package/dist/modules/conformance/scripted-model/messages.mjs.map +7 -0
- package/dist/modules/conformance/scripted-model/responses.d.mts +62 -0
- package/dist/modules/conformance/scripted-model/responses.mjs +273 -0
- package/dist/modules/conformance/scripted-model/responses.mjs.map +7 -0
- package/dist/modules/conformance/scripted-model/sse.d.mts +15 -0
- package/dist/modules/conformance/scripted-model/sse.mjs +75 -0
- package/dist/modules/conformance/scripted-model/sse.mjs.map +7 -0
- package/dist/modules/conformance/v1-agent.d.mts +24 -0
- package/dist/modules/conformance/v1-agent.mjs +40 -0
- package/dist/modules/conformance/v1-agent.mjs.map +7 -0
- package/dist/modules/credential-routes.d.mts +24 -0
- package/dist/modules/credential-routes.mjs +25 -0
- package/dist/modules/credential-routes.mjs.map +7 -0
- package/dist/modules/credentials.d.mts +206 -0
- package/dist/modules/credentials.mjs +207 -0
- package/dist/modules/credentials.mjs.map +7 -0
- package/dist/modules/dynamic.d.mts +32 -0
- package/dist/modules/dynamic.mjs +77 -0
- package/dist/modules/dynamic.mjs.map +7 -0
- package/dist/modules/harness.d.mts +397 -0
- package/dist/modules/harness.mjs +213 -0
- package/dist/modules/harness.mjs.map +7 -0
- package/dist/modules/index.d.mts +15 -0
- package/dist/modules/index.mjs +40 -0
- package/dist/modules/index.mjs.map +7 -0
- package/dist/modules/json.d.mts +12 -0
- package/dist/modules/json.mjs +19 -0
- package/dist/modules/json.mjs.map +7 -0
- package/dist/modules/local-channel-socket.d.mts +23 -0
- package/dist/modules/local-channel-socket.mjs +141 -0
- package/dist/modules/local-channel-socket.mjs.map +7 -0
- package/dist/modules/manifest.d.mts +558 -0
- package/dist/modules/manifest.mjs +266 -0
- package/dist/modules/manifest.mjs.map +7 -0
- package/dist/modules/mcp-content.d.mts +21 -0
- package/dist/modules/mcp-content.mjs +86 -0
- package/dist/modules/mcp-content.mjs.map +7 -0
- package/dist/modules/provider-ref.d.mts +29 -0
- package/dist/modules/provider-ref.mjs +55 -0
- package/dist/modules/provider-ref.mjs.map +7 -0
- package/dist/modules/sandbox/bytes.d.mts +9 -0
- package/dist/modules/sandbox/bytes.mjs +154 -0
- package/dist/modules/sandbox/bytes.mjs.map +7 -0
- package/dist/modules/sandbox/client.d.mts +51 -0
- package/dist/modules/sandbox/client.mjs +500 -0
- package/dist/modules/sandbox/client.mjs.map +7 -0
- package/dist/modules/sandbox/confined-files.d.mts +22 -0
- package/dist/modules/sandbox/confined-files.mjs +142 -0
- package/dist/modules/sandbox/confined-files.mjs.map +7 -0
- package/dist/modules/sandbox/convenience.d.mts +10 -0
- package/dist/modules/sandbox/convenience.mjs +116 -0
- package/dist/modules/sandbox/convenience.mjs.map +7 -0
- package/dist/modules/sandbox/editor.d.mts +49 -0
- package/dist/modules/sandbox/editor.mjs +43 -0
- package/dist/modules/sandbox/editor.mjs.map +7 -0
- package/dist/modules/sandbox/frame-channel.d.mts +47 -0
- package/dist/modules/sandbox/frame-channel.mjs +153 -0
- package/dist/modules/sandbox/frame-channel.mjs.map +7 -0
- package/dist/modules/sandbox/index.d.mts +18 -0
- package/dist/modules/sandbox/index.mjs +70 -0
- package/dist/modules/sandbox/index.mjs.map +7 -0
- package/dist/modules/sandbox/mcp-channel.d.mts +9 -0
- package/dist/modules/sandbox/mcp-channel.mjs +77 -0
- package/dist/modules/sandbox/mcp-channel.mjs.map +7 -0
- package/dist/modules/sandbox/mcp-hosted.d.mts +46 -0
- package/dist/modules/sandbox/mcp-hosted.mjs +221 -0
- package/dist/modules/sandbox/mcp-hosted.mjs.map +7 -0
- package/dist/modules/sandbox/mcp-search.d.mts +51 -0
- package/dist/modules/sandbox/mcp-search.mjs +129 -0
- package/dist/modules/sandbox/mcp-search.mjs.map +7 -0
- package/dist/modules/sandbox/mcp-shell.d.mts +21 -0
- package/dist/modules/sandbox/mcp-shell.mjs +205 -0
- package/dist/modules/sandbox/mcp-shell.mjs.map +7 -0
- package/dist/modules/sandbox/mcp-testing.d.mts +11 -0
- package/dist/modules/sandbox/mcp-testing.mjs +33 -0
- package/dist/modules/sandbox/mcp-testing.mjs.map +7 -0
- package/dist/modules/sandbox/mcp.d.mts +83 -0
- package/dist/modules/sandbox/mcp.mjs +971 -0
- package/dist/modules/sandbox/mcp.mjs.map +7 -0
- package/dist/modules/sandbox/memory-sandbox.d.mts +32 -0
- package/dist/modules/sandbox/memory-sandbox.mjs +416 -0
- package/dist/modules/sandbox/memory-sandbox.mjs.map +7 -0
- package/dist/modules/sandbox/memory-shell.d.mts +29 -0
- package/dist/modules/sandbox/memory-shell.mjs +278 -0
- package/dist/modules/sandbox/memory-shell.mjs.map +7 -0
- package/dist/modules/sandbox/open-scope-node.d.mts +4 -0
- package/dist/modules/sandbox/open-scope-node.mjs +19 -0
- package/dist/modules/sandbox/open-scope-node.mjs.map +7 -0
- package/dist/modules/sandbox/open-scope.d.mts +16 -0
- package/dist/modules/sandbox/open-scope.mjs +66 -0
- package/dist/modules/sandbox/open-scope.mjs.map +7 -0
- package/dist/modules/sandbox/paths.d.mts +7 -0
- package/dist/modules/sandbox/paths.mjs +50 -0
- package/dist/modules/sandbox/paths.mjs.map +7 -0
- package/dist/modules/sandbox/rpc-peer.d.mts +35 -0
- package/dist/modules/sandbox/rpc-peer.mjs +224 -0
- package/dist/modules/sandbox/rpc-peer.mjs.map +7 -0
- package/dist/modules/sandbox/seeds.d.mts +26 -0
- package/dist/modules/sandbox/seeds.mjs +228 -0
- package/dist/modules/sandbox/seeds.mjs.map +7 -0
- package/dist/modules/sandbox/server.d.mts +66 -0
- package/dist/modules/sandbox/server.mjs +567 -0
- package/dist/modules/sandbox/server.mjs.map +7 -0
- package/dist/modules/sandbox/snapshots.d.mts +49 -0
- package/dist/modules/sandbox/snapshots.mjs +55 -0
- package/dist/modules/sandbox/snapshots.mjs.map +7 -0
- package/dist/modules/sandbox/stream-mux.d.mts +18 -0
- package/dist/modules/sandbox/stream-mux.mjs +285 -0
- package/dist/modules/sandbox/stream-mux.mjs.map +7 -0
- package/dist/modules/sandbox/testing.d.mts +25 -0
- package/dist/modules/sandbox/testing.mjs +64 -0
- package/dist/modules/sandbox/testing.mjs.map +7 -0
- package/dist/modules/sandbox/types.d.mts +352 -0
- package/dist/modules/sandbox/types.mjs +86 -0
- package/dist/modules/sandbox/types.mjs.map +7 -0
- package/dist/modules/sandbox/wire.d.mts +671 -0
- package/dist/modules/sandbox/wire.mjs +537 -0
- package/dist/modules/sandbox/wire.mjs.map +7 -0
- package/dist/modules/sandbox/workspace-root.d.mts +8 -0
- package/dist/modules/sandbox/workspace-root.mjs +79 -0
- package/dist/modules/sandbox/workspace-root.mjs.map +7 -0
- package/dist/modules/schemas.d.mts +121 -0
- package/dist/modules/schemas.mjs +146 -0
- package/dist/modules/schemas.mjs.map +7 -0
- package/dist/modules/semantic-events.d.mts +31 -0
- package/dist/modules/semantic-events.mjs +23 -0
- package/dist/modules/semantic-events.mjs.map +7 -0
- package/package.json +199 -0
|
@@ -0,0 +1,1303 @@
|
|
|
1
|
+
import {
|
|
2
|
+
client,
|
|
3
|
+
methods,
|
|
4
|
+
PROTOCOL_VERSION
|
|
5
|
+
} from "@agentclientprotocol/sdk/experimental/v2";
|
|
6
|
+
import {
|
|
7
|
+
accumulateAgentMessages,
|
|
8
|
+
acpSchemas,
|
|
9
|
+
advertisedFinalExtensions,
|
|
10
|
+
isUnsupportedProtocolVersionError,
|
|
11
|
+
jsonValueSchema,
|
|
12
|
+
MAX_ACP_FRAME_BYTES,
|
|
13
|
+
MAX_INLINE_MEDIA_BYTES,
|
|
14
|
+
requireProtocolV2,
|
|
15
|
+
RESOURCE_LINK_DESCRIPTION_BYTES,
|
|
16
|
+
resumeReplayFor,
|
|
17
|
+
serializedFrameBytes,
|
|
18
|
+
watchInitializeVersion
|
|
19
|
+
} from "@finalai/acp-session";
|
|
20
|
+
import * as z from "zod";
|
|
21
|
+
import {
|
|
22
|
+
harnessDescriptionSchema,
|
|
23
|
+
McpTransportKind,
|
|
24
|
+
PromptWhileRunning
|
|
25
|
+
} from "../harness.mjs";
|
|
26
|
+
import { emptyManifest } from "../manifest.mjs";
|
|
27
|
+
import {
|
|
28
|
+
answerConformanceMcpMessage,
|
|
29
|
+
createConformanceMcpHttpHandler
|
|
30
|
+
} from "./fixtures.mjs";
|
|
31
|
+
import {
|
|
32
|
+
CONFORMANCE_ACP_SERVER_ID,
|
|
33
|
+
CONFORMANCE_ACP_SERVER_NAME,
|
|
34
|
+
CONFORMANCE_ELICITATION_ANSWER,
|
|
35
|
+
CONFORMANCE_HTTP_SERVER_NAME,
|
|
36
|
+
CONFORMANCE_MCP_TOOL,
|
|
37
|
+
ConformanceDirective,
|
|
38
|
+
conformanceEchoText,
|
|
39
|
+
directive,
|
|
40
|
+
FINAL_META_KEY,
|
|
41
|
+
OBSERVE_METHOD
|
|
42
|
+
} from "./protocol.mjs";
|
|
43
|
+
import { createLegacyProtocolAgent } from "./v1-agent.mjs";
|
|
44
|
+
var ConformanceProfile = /* @__PURE__ */ ((ConformanceProfile2) => {
|
|
45
|
+
ConformanceProfile2["Final"] = "final";
|
|
46
|
+
ConformanceProfile2["PlainAcpV2"] = "plain-acp-v2";
|
|
47
|
+
return ConformanceProfile2;
|
|
48
|
+
})(ConformanceProfile || {});
|
|
49
|
+
var ConformanceCaseStatus = /* @__PURE__ */ ((ConformanceCaseStatus2) => {
|
|
50
|
+
ConformanceCaseStatus2["Passed"] = "passed";
|
|
51
|
+
ConformanceCaseStatus2["Failed"] = "failed";
|
|
52
|
+
ConformanceCaseStatus2["Skipped"] = "skipped";
|
|
53
|
+
return ConformanceCaseStatus2;
|
|
54
|
+
})(ConformanceCaseStatus || {});
|
|
55
|
+
var HarnessConformanceCase = /* @__PURE__ */ ((HarnessConformanceCase2) => {
|
|
56
|
+
HarnessConformanceCase2["Describe"] = "describe";
|
|
57
|
+
HarnessConformanceCase2["Open"] = "open";
|
|
58
|
+
HarnessConformanceCase2["Initialize"] = "initialize";
|
|
59
|
+
HarnessConformanceCase2["NewSession"] = "session/new";
|
|
60
|
+
HarnessConformanceCase2["Prompt"] = "prompt";
|
|
61
|
+
HarnessConformanceCase2["Stream"] = "stream";
|
|
62
|
+
HarnessConformanceCase2["Permission"] = "permission";
|
|
63
|
+
HarnessConformanceCase2["Elicitation"] = "elicitation";
|
|
64
|
+
HarnessConformanceCase2["McpAcp"] = "mcp over acp";
|
|
65
|
+
HarnessConformanceCase2["McpHttp"] = "mcp over http";
|
|
66
|
+
HarnessConformanceCase2["Observe"] = "observe";
|
|
67
|
+
HarnessConformanceCase2["ErrorStop"] = "error stop";
|
|
68
|
+
HarnessConformanceCase2["Cancel"] = "cancel";
|
|
69
|
+
HarnessConformanceCase2["ResumeReplay"] = "resume with replay";
|
|
70
|
+
HarnessConformanceCase2["ParkResume"] = "park/resume";
|
|
71
|
+
HarnessConformanceCase2["PromptWhileRunning"] = "prompt while running";
|
|
72
|
+
HarnessConformanceCase2["CancelWhileRunning"] = "cancel reaching a prompt sent while running";
|
|
73
|
+
HarnessConformanceCase2["NoResend"] = "unanswered prompt across a dropped connection";
|
|
74
|
+
HarnessConformanceCase2["DisconnectMidTurn"] = "disconnect mid-turn";
|
|
75
|
+
HarnessConformanceCase2["DisconnectMidPermission"] = "disconnect mid-permission";
|
|
76
|
+
HarnessConformanceCase2["NativeIdPreservation"] = "native session identity across resume";
|
|
77
|
+
HarnessConformanceCase2["NativeIdChange"] = "native session id change";
|
|
78
|
+
HarnessConformanceCase2["OversizedFrame"] = "multi-megabyte tool result and image";
|
|
79
|
+
HarnessConformanceCase2["GracefulClose"] = "graceful close";
|
|
80
|
+
HarnessConformanceCase2["V1Rejected"] = "ACP v1 agent rejected";
|
|
81
|
+
HarnessConformanceCase2["Health"] = "health";
|
|
82
|
+
HarnessConformanceCase2["Close"] = "close";
|
|
83
|
+
return HarnessConformanceCase2;
|
|
84
|
+
})(HarnessConformanceCase || {});
|
|
85
|
+
const HARNESS_CONFORMANCE_CASES = Object.values(HarnessConformanceCase);
|
|
86
|
+
const failure = (message) => new Error(message);
|
|
87
|
+
const check = (condition, message) => {
|
|
88
|
+
if (!condition) throw failure(message);
|
|
89
|
+
};
|
|
90
|
+
const withTimeout = async (promise, ms, what) => {
|
|
91
|
+
let timer = null;
|
|
92
|
+
try {
|
|
93
|
+
return await Promise.race([
|
|
94
|
+
promise,
|
|
95
|
+
new Promise((_, reject) => {
|
|
96
|
+
timer = setTimeout(() => reject(failure(`${what} timed out after ${ms} ms`)), ms);
|
|
97
|
+
})
|
|
98
|
+
]);
|
|
99
|
+
} finally {
|
|
100
|
+
if (timer != null) clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
104
|
+
const textOfUpdates = (updates) => accumulateAgentMessages(updates).map((message) => message.text).join("");
|
|
105
|
+
const isIdle = (update) => update.sessionUpdate === "state_update" && "state" in update && update.state === "idle";
|
|
106
|
+
const stopReasonSchema = z.object({ stopReason: z.string() });
|
|
107
|
+
const stopReasonOf = (update) => stopReasonSchema.safeParse(update).data?.stopReason ?? null;
|
|
108
|
+
const echoSchema = z.object({
|
|
109
|
+
sessionUpdate: z.union([z.literal("user_message"), z.literal("user_message_chunk")]),
|
|
110
|
+
messageId: z.string(),
|
|
111
|
+
content: z.unknown().optional()
|
|
112
|
+
});
|
|
113
|
+
const echoOf = (update) => {
|
|
114
|
+
const parsed = echoSchema.safeParse(update);
|
|
115
|
+
return parsed.success ? { messageId: parsed.data.messageId, content: JSON.stringify(parsed.data.content ?? null) } : null;
|
|
116
|
+
};
|
|
117
|
+
const isRunning = (update) => update.sessionUpdate === "state_update" && "state" in update && update.state === "running";
|
|
118
|
+
const toolContentSchema = z.object({
|
|
119
|
+
sessionUpdate: z.literal("tool_call_update"),
|
|
120
|
+
content: z.array(
|
|
121
|
+
z.object({ type: z.literal("content"), content: z.looseObject({ type: z.string() }) })
|
|
122
|
+
)
|
|
123
|
+
});
|
|
124
|
+
const resourceLinkSchema = z.object({
|
|
125
|
+
type: z.literal("resource_link"),
|
|
126
|
+
uri: z.string().min(1),
|
|
127
|
+
mimeType: z.string().nullish(),
|
|
128
|
+
size: z.number().nullish(),
|
|
129
|
+
description: z.string().nullish()
|
|
130
|
+
});
|
|
131
|
+
const inlineTextSchema = z.object({ type: z.literal("text"), text: z.string() });
|
|
132
|
+
const rawMcpBlocks = (value, depth = 0) => {
|
|
133
|
+
if (depth > 4) return [];
|
|
134
|
+
const wrapper = z.object({ result: jsonValueSchema }).safeParse(value);
|
|
135
|
+
if (wrapper.success) return rawMcpBlocks(wrapper.data.result, depth + 1);
|
|
136
|
+
const result = z.object({ content: z.array(jsonValueSchema) }).safeParse(value);
|
|
137
|
+
if (!result.success) return [];
|
|
138
|
+
return result.data.content.flatMap((block) => {
|
|
139
|
+
const text = z.object({ type: z.literal("text"), text: z.string() }).safeParse(block);
|
|
140
|
+
if (text.success) {
|
|
141
|
+
try {
|
|
142
|
+
const nested = rawMcpBlocks(jsonValueSchema.parse(JSON.parse(text.data.text)), depth + 1);
|
|
143
|
+
if (nested.length > 0) return nested;
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const content = acpSchemas.zContentBlock.safeParse(block);
|
|
148
|
+
return content.success ? [content.data] : [];
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
const toolBlocksOf = (updates) => updates.flatMap((update) => {
|
|
152
|
+
const content = (toolContentSchema.safeParse(update).data?.content ?? []).map(
|
|
153
|
+
(entry) => entry.content
|
|
154
|
+
);
|
|
155
|
+
const raw = z.object({ sessionUpdate: z.literal("tool_call_update"), rawOutput: jsonValueSchema }).safeParse(update);
|
|
156
|
+
return [...content, ...raw.success ? rawMcpBlocks(raw.data.rawOutput) : []];
|
|
157
|
+
});
|
|
158
|
+
const requestIdSchema = z.object({ id: z.union([z.string(), z.number()]), method: z.string() });
|
|
159
|
+
const responseIdSchema = z.object({ id: z.union([z.string(), z.number(), z.null()]) });
|
|
160
|
+
const isBatch = (wire) => Array.isArray(wire);
|
|
161
|
+
const BIG_RESULT_BYTES = 3 * 1024 * 1024;
|
|
162
|
+
const providerErrorSchema = z.object({
|
|
163
|
+
[FINAL_META_KEY]: z.object({
|
|
164
|
+
providerError: z.object({ code: z.string(), message: z.string(), retryable: z.boolean() })
|
|
165
|
+
})
|
|
166
|
+
});
|
|
167
|
+
const conformanceOpenArgs = (overrides = {}) => {
|
|
168
|
+
const manifest = emptyManifest("Conformance");
|
|
169
|
+
return {
|
|
170
|
+
session: {
|
|
171
|
+
sessionId: crypto.randomUUID(),
|
|
172
|
+
organizationId: crypto.randomUUID(),
|
|
173
|
+
definitionId: crypto.randomUUID(),
|
|
174
|
+
revisionId: crypto.randomUUID(),
|
|
175
|
+
sessionServiceUrl: "http://conformance.invalid"
|
|
176
|
+
},
|
|
177
|
+
harnessSessionId: crypto.randomUUID(),
|
|
178
|
+
config: {},
|
|
179
|
+
manifest: {
|
|
180
|
+
...manifest,
|
|
181
|
+
harness: { harness: manifest.harness.harness, config: {} },
|
|
182
|
+
mcpServers: [],
|
|
183
|
+
sandboxes: {}
|
|
184
|
+
},
|
|
185
|
+
mcpServers: [],
|
|
186
|
+
baseTools: {},
|
|
187
|
+
sandboxes: {},
|
|
188
|
+
credentials: {
|
|
189
|
+
brokerUrl: "http://conformance.invalid/broker",
|
|
190
|
+
grants: [],
|
|
191
|
+
atOpen: null,
|
|
192
|
+
refresh: null
|
|
193
|
+
},
|
|
194
|
+
secrets: { get: async () => null },
|
|
195
|
+
trace: { traceparent: null, tracestate: null },
|
|
196
|
+
...overrides
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
const runHarnessConformance = async (args) => {
|
|
200
|
+
const profile = args.profile === "final" /* Final */ ? "final" /* Final */ : "plain-acp-v2" /* PlainAcpV2 */;
|
|
201
|
+
const final = profile === "final" /* Final */;
|
|
202
|
+
const timeoutMs = args.timeoutMs ?? 15e3;
|
|
203
|
+
const replayGraceMs = args.replayGraceMs ?? 300;
|
|
204
|
+
const cwd = args.cwd ?? "/workspace";
|
|
205
|
+
const nonce = crypto.randomUUID().slice(0, 8);
|
|
206
|
+
const cases = [];
|
|
207
|
+
const passed = /* @__PURE__ */ new Set();
|
|
208
|
+
const cleanups = [];
|
|
209
|
+
const logs = /* @__PURE__ */ new Map();
|
|
210
|
+
const logFor = (sessionId2) => {
|
|
211
|
+
let log = logs.get(sessionId2);
|
|
212
|
+
if (log == null) {
|
|
213
|
+
log = { items: [], listeners: /* @__PURE__ */ new Set() };
|
|
214
|
+
logs.set(sessionId2, log);
|
|
215
|
+
}
|
|
216
|
+
return log;
|
|
217
|
+
};
|
|
218
|
+
const permissionRequests = [];
|
|
219
|
+
const elicitations = [];
|
|
220
|
+
const mcpMessages = [];
|
|
221
|
+
const permissionIds = [];
|
|
222
|
+
let epoch = 0;
|
|
223
|
+
let holdPermissions = false;
|
|
224
|
+
const releaseHeld = [];
|
|
225
|
+
const inboundFrameBytes = [];
|
|
226
|
+
const promptDrop = {
|
|
227
|
+
armed: false,
|
|
228
|
+
id: null,
|
|
229
|
+
dropped: false
|
|
230
|
+
};
|
|
231
|
+
const knownSessions = /* @__PURE__ */ new Set();
|
|
232
|
+
const foreignUpdates = [];
|
|
233
|
+
const kitClient = client({ name: "final-conformance-kit" }).onRequest(methods.client.session.requestPermission, ({ params, requestId }) => {
|
|
234
|
+
permissionRequests.push({ optionKinds: params.options.map((option2) => option2.kind) });
|
|
235
|
+
permissionIds.push({ requestId, epoch });
|
|
236
|
+
if (holdPermissions) {
|
|
237
|
+
return new Promise((resolve) => {
|
|
238
|
+
releaseHeld.push(() => resolve({ outcome: { outcome: "cancelled" } }));
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const option = params.options.find((entry) => entry.kind === "allow_once") ?? params.options[0];
|
|
242
|
+
return option == null ? { outcome: { outcome: "cancelled" } } : { outcome: { outcome: "selected", optionId: option.optionId } };
|
|
243
|
+
}).onRequest(methods.client.elicitation.create, ({ params }) => {
|
|
244
|
+
elicitations.push({ mode: params.mode });
|
|
245
|
+
return { action: "accept", content: { answer: CONFORMANCE_ELICITATION_ANSWER } };
|
|
246
|
+
}).onRequest(methods.client.mcp.message, ({ params }) => {
|
|
247
|
+
mcpMessages.push({ serverId: params.serverId, method: params.method });
|
|
248
|
+
return answerConformanceMcpMessage(nonce, params);
|
|
249
|
+
}).onNotification(methods.client.session.update, ({ params }) => {
|
|
250
|
+
if (!knownSessions.has(params.sessionId)) foreignUpdates.push(params.sessionId);
|
|
251
|
+
const log = logFor(params.sessionId);
|
|
252
|
+
log.items.push(params.update);
|
|
253
|
+
for (const listener of [...log.listeners]) listener();
|
|
254
|
+
});
|
|
255
|
+
const waitFor = (sessionId2, predicate, what) => {
|
|
256
|
+
const log = logFor(sessionId2);
|
|
257
|
+
return withTimeout(
|
|
258
|
+
new Promise((resolve) => {
|
|
259
|
+
const listener = () => {
|
|
260
|
+
if (!predicate(log.items)) return;
|
|
261
|
+
log.listeners.delete(listener);
|
|
262
|
+
resolve();
|
|
263
|
+
};
|
|
264
|
+
log.listeners.add(listener);
|
|
265
|
+
listener();
|
|
266
|
+
}),
|
|
267
|
+
timeoutMs,
|
|
268
|
+
what
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
let description = null;
|
|
272
|
+
const described = () => description;
|
|
273
|
+
let endpoint = null;
|
|
274
|
+
let connection = null;
|
|
275
|
+
let stream = null;
|
|
276
|
+
let advertised = advertisedFinalExtensions({});
|
|
277
|
+
let sessionId = null;
|
|
278
|
+
let mcpServers = [];
|
|
279
|
+
let firstPrompt = null;
|
|
280
|
+
let echoText = "";
|
|
281
|
+
const step = async (name, options, run) => {
|
|
282
|
+
const started = Date.now();
|
|
283
|
+
const missing = (options.requires ?? []).filter((required) => !passed.has(required));
|
|
284
|
+
const skip = args.surfaceLimits?.[name] ?? options.skip ?? null;
|
|
285
|
+
if (skip != null || missing.length > 0) {
|
|
286
|
+
cases.push({
|
|
287
|
+
name,
|
|
288
|
+
status: "skipped" /* Skipped */,
|
|
289
|
+
durationMs: 0,
|
|
290
|
+
error: null,
|
|
291
|
+
detail: skip ?? `requires ${missing.join(", ")}`
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const detail = await withTimeout(
|
|
297
|
+
run(),
|
|
298
|
+
name === "park/resume" /* ParkResume */ ? args.parkTimeoutMs ?? timeoutMs : timeoutMs,
|
|
299
|
+
name
|
|
300
|
+
);
|
|
301
|
+
passed.add(name);
|
|
302
|
+
cases.push({
|
|
303
|
+
name,
|
|
304
|
+
status: "passed" /* Passed */,
|
|
305
|
+
durationMs: Date.now() - started,
|
|
306
|
+
error: null,
|
|
307
|
+
detail: detail ?? null
|
|
308
|
+
});
|
|
309
|
+
} catch (cause) {
|
|
310
|
+
cases.push({
|
|
311
|
+
name,
|
|
312
|
+
status: "failed" /* Failed */,
|
|
313
|
+
durationMs: Date.now() - started,
|
|
314
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
315
|
+
detail: null
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const requireConnection = () => {
|
|
320
|
+
if (connection == null) throw failure("no connection");
|
|
321
|
+
return connection;
|
|
322
|
+
};
|
|
323
|
+
const requireSession = () => {
|
|
324
|
+
if (sessionId == null) throw failure("no session");
|
|
325
|
+
return sessionId;
|
|
326
|
+
};
|
|
327
|
+
const promptMeta = {
|
|
328
|
+
[FINAL_META_KEY]: {
|
|
329
|
+
author: { participantId: "conformance-user", kind: "user", displayName: "Conformance" },
|
|
330
|
+
replyExpected: true
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const sendPrompt = async (text) => {
|
|
334
|
+
const id = requireSession();
|
|
335
|
+
const from = logFor(id).items.length;
|
|
336
|
+
const response = await requireConnection().agent.request(methods.agent.session.prompt, {
|
|
337
|
+
sessionId: id,
|
|
338
|
+
prompt: [{ type: "text", text }],
|
|
339
|
+
_meta: promptMeta
|
|
340
|
+
});
|
|
341
|
+
return { messageId: response.messageId, from };
|
|
342
|
+
};
|
|
343
|
+
const awaitTurn = async (sent) => {
|
|
344
|
+
const id = requireSession();
|
|
345
|
+
const hasIdle = (items2) => items2.slice(sent.from).some(isIdle);
|
|
346
|
+
await waitFor(id, hasIdle, "turn end");
|
|
347
|
+
const items = logFor(id).items;
|
|
348
|
+
const updates = [];
|
|
349
|
+
let idle = null;
|
|
350
|
+
for (const update of items.slice(sent.from)) {
|
|
351
|
+
updates.push(update);
|
|
352
|
+
if (isIdle(update)) {
|
|
353
|
+
idle = update;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (idle == null) throw failure("turn did not end");
|
|
358
|
+
return {
|
|
359
|
+
messageId: sent.messageId,
|
|
360
|
+
updates,
|
|
361
|
+
idle,
|
|
362
|
+
stopReason: stopReasonOf(idle),
|
|
363
|
+
text: textOfUpdates(updates)
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
const turn = async (text) => awaitTurn(await sendPrompt(text));
|
|
367
|
+
const tap = (inner) => {
|
|
368
|
+
const writer = inner.writable.getWriter();
|
|
369
|
+
const writable = new WritableStream({
|
|
370
|
+
write: async (wire) => {
|
|
371
|
+
for (const message of isBatch(wire) ? wire : [wire]) {
|
|
372
|
+
const request = requestIdSchema.safeParse(message);
|
|
373
|
+
if (promptDrop.armed && promptDrop.id == null && request.success && request.data.method === methods.agent.session.prompt) {
|
|
374
|
+
promptDrop.id = JSON.stringify(request.data.id);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
await writer.write(wire);
|
|
378
|
+
},
|
|
379
|
+
close: () => writer.close(),
|
|
380
|
+
abort: (reason) => writer.abort(reason)
|
|
381
|
+
});
|
|
382
|
+
const readable = inner.readable.pipeThrough(
|
|
383
|
+
new TransformStream({
|
|
384
|
+
transform: (wire, controller) => {
|
|
385
|
+
inboundFrameBytes.push(serializedFrameBytes(wire));
|
|
386
|
+
const response = responseIdSchema.safeParse(wire);
|
|
387
|
+
if (promptDrop.id != null && !isBatch(wire) && !("method" in wire) && response.success && JSON.stringify(response.data.id) === promptDrop.id) {
|
|
388
|
+
promptDrop.id = null;
|
|
389
|
+
promptDrop.armed = false;
|
|
390
|
+
promptDrop.dropped = true;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
controller.enqueue(wire);
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
);
|
|
397
|
+
return { readable, writable };
|
|
398
|
+
};
|
|
399
|
+
let initialized = null;
|
|
400
|
+
const initializeResponse = () => initialized;
|
|
401
|
+
const initializeOver = (target, watch) => (
|
|
402
|
+
// The session service's check: any version but 2 is refused.
|
|
403
|
+
requireProtocolV2({
|
|
404
|
+
watch,
|
|
405
|
+
initialize: target.agent.request(methods.agent.initialize, {
|
|
406
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
407
|
+
info: { name: "final-conformance-kit", version: "1" },
|
|
408
|
+
capabilities: { elicitation: { form: {} } }
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
);
|
|
412
|
+
const connect = async (target) => {
|
|
413
|
+
stream = await args.dial(target);
|
|
414
|
+
const watch = watchInitializeVersion(tap(stream));
|
|
415
|
+
connection = kitClient.connect(watch.stream);
|
|
416
|
+
epoch += 1;
|
|
417
|
+
const response = await initializeOver(requireConnection(), watch);
|
|
418
|
+
initialized = response;
|
|
419
|
+
advertised = advertisedFinalExtensions(response);
|
|
420
|
+
return response;
|
|
421
|
+
};
|
|
422
|
+
const disconnect = async () => {
|
|
423
|
+
const current = connection;
|
|
424
|
+
const dialed = stream;
|
|
425
|
+
connection = null;
|
|
426
|
+
stream = null;
|
|
427
|
+
for (const release of releaseHeld.splice(0)) release();
|
|
428
|
+
if (current != null) {
|
|
429
|
+
current.close();
|
|
430
|
+
await withTimeout(current.closed, timeoutMs, "connection close").catch(() => void 0);
|
|
431
|
+
}
|
|
432
|
+
dialed?.close?.();
|
|
433
|
+
};
|
|
434
|
+
const reconnect = async (next) => {
|
|
435
|
+
endpoint = next.resumed;
|
|
436
|
+
await connect(next.resumed);
|
|
437
|
+
const keeps = next.resumed.ref.generation === next.previous.ref.generation && described()?.supports.resumeReplay === true;
|
|
438
|
+
if (keeps) {
|
|
439
|
+
await requireConnection().agent.request(methods.agent.session.resume, {
|
|
440
|
+
sessionId: requireSession(),
|
|
441
|
+
cwd,
|
|
442
|
+
mcpServers,
|
|
443
|
+
// Null: resume without replay.
|
|
444
|
+
replayFrom: next.replayFrom
|
|
445
|
+
});
|
|
446
|
+
return "resumed";
|
|
447
|
+
}
|
|
448
|
+
const created = await requireConnection().agent.request(methods.agent.session.new, {
|
|
449
|
+
cwd,
|
|
450
|
+
mcpServers
|
|
451
|
+
});
|
|
452
|
+
sessionId = created.sessionId;
|
|
453
|
+
knownSessions.add(created.sessionId);
|
|
454
|
+
return "recreated";
|
|
455
|
+
};
|
|
456
|
+
const dropAndResume = async (replayFrom) => {
|
|
457
|
+
if (endpoint == null) throw failure("no endpoint");
|
|
458
|
+
const previous = endpoint;
|
|
459
|
+
const before = requireSession();
|
|
460
|
+
await disconnect();
|
|
461
|
+
const resumed = await args.harness.resume({
|
|
462
|
+
...args.openArgs,
|
|
463
|
+
ref: previous.ref,
|
|
464
|
+
lastSeq: logFor(before).items.length
|
|
465
|
+
});
|
|
466
|
+
const from = logFor(before).items.length;
|
|
467
|
+
const how = await reconnect({ resumed, previous, replayFrom });
|
|
468
|
+
return { how, from: how === "resumed" ? from : 0 };
|
|
469
|
+
};
|
|
470
|
+
const waitForText = (id, from, needle, what) => waitFor(id, (items) => textOfUpdates(items.slice(from)).includes(needle), what);
|
|
471
|
+
const afterEcho = (id, from, messageId) => {
|
|
472
|
+
const items = logFor(id).items.slice(from);
|
|
473
|
+
const index = items.findIndex((update) => echoOf(update)?.messageId === messageId);
|
|
474
|
+
return index < 0 ? null : items.slice(index + 1);
|
|
475
|
+
};
|
|
476
|
+
const ownTurn = async (sent) => {
|
|
477
|
+
const id = requireSession();
|
|
478
|
+
await waitFor(
|
|
479
|
+
id,
|
|
480
|
+
() => afterEcho(id, sent.from, sent.messageId)?.some(isIdle) === true,
|
|
481
|
+
"the prompt to be answered"
|
|
482
|
+
);
|
|
483
|
+
const after = afterEcho(id, sent.from, sent.messageId) ?? [];
|
|
484
|
+
const end = after.findIndex(isIdle);
|
|
485
|
+
const updates = after.slice(0, end + 1);
|
|
486
|
+
const idle = updates.at(-1);
|
|
487
|
+
if (idle == null) throw failure("turn did not end");
|
|
488
|
+
return {
|
|
489
|
+
messageId: sent.messageId,
|
|
490
|
+
updates,
|
|
491
|
+
idle,
|
|
492
|
+
stopReason: stopReasonOf(idle),
|
|
493
|
+
text: textOfUpdates(updates)
|
|
494
|
+
};
|
|
495
|
+
};
|
|
496
|
+
const refused = (cause) => failure(
|
|
497
|
+
`a session/prompt sent while the session was running was refused (${cause instanceof Error ? cause.message : String(cause)}); it must be accepted`
|
|
498
|
+
);
|
|
499
|
+
try {
|
|
500
|
+
await step("describe" /* Describe */, {}, async () => {
|
|
501
|
+
description = harnessDescriptionSchema.parse(
|
|
502
|
+
await args.harness.describe({
|
|
503
|
+
config: args.openArgs.config,
|
|
504
|
+
secrets: args.openArgs.secrets
|
|
505
|
+
})
|
|
506
|
+
);
|
|
507
|
+
return `transports ${description.acp.transports.join(", ")}; mcp ${description.acp.mcp.join(", ") || "none"}`;
|
|
508
|
+
});
|
|
509
|
+
await step(
|
|
510
|
+
"open" /* Open */,
|
|
511
|
+
{ requires: ["describe" /* Describe */] },
|
|
512
|
+
async () => {
|
|
513
|
+
endpoint = await args.harness.open(args.openArgs);
|
|
514
|
+
check(endpoint.ref.harnessSessionRef.length > 0, "endpoint has no harnessSessionRef");
|
|
515
|
+
return `transport ${endpoint.transport}`;
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
await step(
|
|
519
|
+
"initialize" /* Initialize */,
|
|
520
|
+
{ requires: ["open" /* Open */] },
|
|
521
|
+
async () => {
|
|
522
|
+
if (endpoint == null) throw failure("no endpoint");
|
|
523
|
+
const response = await connect(endpoint);
|
|
524
|
+
check(
|
|
525
|
+
response.protocolVersion === PROTOCOL_VERSION,
|
|
526
|
+
`protocolVersion ${response.protocolVersion}, expected ${PROTOCOL_VERSION}`
|
|
527
|
+
);
|
|
528
|
+
return `${response.info.name} ${response.info.version}`;
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
const httpFixture = args.httpMcp == null ? null : await (async () => {
|
|
532
|
+
const fixture = createConformanceMcpHttpHandler(nonce);
|
|
533
|
+
const served = await args.httpMcp?.serve(fixture.fetch);
|
|
534
|
+
cleanups.push(async () => {
|
|
535
|
+
await served?.close();
|
|
536
|
+
await fixture.close();
|
|
537
|
+
});
|
|
538
|
+
return served ?? null;
|
|
539
|
+
})();
|
|
540
|
+
await step(
|
|
541
|
+
"session/new" /* NewSession */,
|
|
542
|
+
{ requires: ["initialize" /* Initialize */] },
|
|
543
|
+
async () => {
|
|
544
|
+
const mcp = described()?.acp.mcp ?? [];
|
|
545
|
+
mcpServers = [...args.openArgs.mcpServers];
|
|
546
|
+
if (mcp.includes(McpTransportKind.Acp)) {
|
|
547
|
+
mcpServers.push({
|
|
548
|
+
type: "acp",
|
|
549
|
+
name: CONFORMANCE_ACP_SERVER_NAME,
|
|
550
|
+
serverId: CONFORMANCE_ACP_SERVER_ID
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
if (mcp.includes(McpTransportKind.Http) && httpFixture != null) {
|
|
554
|
+
mcpServers.push({
|
|
555
|
+
type: "http",
|
|
556
|
+
name: CONFORMANCE_HTTP_SERVER_NAME,
|
|
557
|
+
url: httpFixture.url,
|
|
558
|
+
headers: Object.entries(httpFixture.headers).map(([headerName, value]) => ({
|
|
559
|
+
name: headerName,
|
|
560
|
+
value
|
|
561
|
+
}))
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
const response = await requireConnection().agent.request(methods.agent.session.new, {
|
|
565
|
+
cwd,
|
|
566
|
+
mcpServers
|
|
567
|
+
});
|
|
568
|
+
check(response.sessionId.length > 0, "empty sessionId");
|
|
569
|
+
sessionId = response.sessionId;
|
|
570
|
+
knownSessions.add(response.sessionId);
|
|
571
|
+
return `session ${response.sessionId}`;
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
const sessionCases = ["session/new" /* NewSession */];
|
|
575
|
+
await step("prompt" /* Prompt */, { requires: sessionCases }, async () => {
|
|
576
|
+
echoText = `hello conformance ${nonce}`;
|
|
577
|
+
firstPrompt = sendPrompt(directive(ConformanceDirective.Echo, echoText));
|
|
578
|
+
const sent = await firstPrompt;
|
|
579
|
+
check(sent.messageId.length > 0, "session/prompt returned no messageId");
|
|
580
|
+
return `messageId ${sent.messageId}`;
|
|
581
|
+
});
|
|
582
|
+
await step(
|
|
583
|
+
"stream" /* Stream */,
|
|
584
|
+
{ requires: ["prompt" /* Prompt */] },
|
|
585
|
+
async () => {
|
|
586
|
+
if (firstPrompt == null) throw failure("no prompt");
|
|
587
|
+
const result = await awaitTurn(await firstPrompt);
|
|
588
|
+
check(
|
|
589
|
+
result.updates.some((update) => update.sessionUpdate === "agent_message_chunk"),
|
|
590
|
+
"no agent_message_chunk before idle"
|
|
591
|
+
);
|
|
592
|
+
check(
|
|
593
|
+
result.text === echoText,
|
|
594
|
+
`streamed text ${JSON.stringify(result.text)}, expected ${JSON.stringify(echoText)}`
|
|
595
|
+
);
|
|
596
|
+
check(
|
|
597
|
+
result.stopReason === "end_turn",
|
|
598
|
+
`stopReason ${result.stopReason}, expected end_turn`
|
|
599
|
+
);
|
|
600
|
+
return `${result.updates.length} updates`;
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
await step("permission" /* Permission */, { requires: sessionCases }, async () => {
|
|
604
|
+
const before = permissionRequests.length;
|
|
605
|
+
const result = await turn(directive(ConformanceDirective.Permission));
|
|
606
|
+
check(permissionRequests.length > before, "no session/request_permission was sent");
|
|
607
|
+
check(
|
|
608
|
+
permissionRequests[permissionRequests.length - 1]?.optionKinds.includes("allow_once") === true,
|
|
609
|
+
"the permission request had no allow_once option"
|
|
610
|
+
);
|
|
611
|
+
check(result.text === "permission:allow_once", `reply ${JSON.stringify(result.text)}`);
|
|
612
|
+
check(result.stopReason === "end_turn", `stopReason ${result.stopReason}`);
|
|
613
|
+
});
|
|
614
|
+
await step(
|
|
615
|
+
"elicitation" /* Elicitation */,
|
|
616
|
+
{
|
|
617
|
+
requires: sessionCases,
|
|
618
|
+
skip: mcpServers.length === 0 ? "no client MCP fixture can be attached" : null
|
|
619
|
+
},
|
|
620
|
+
async () => {
|
|
621
|
+
const before = elicitations.length;
|
|
622
|
+
const result = await turn(directive(ConformanceDirective.Elicit));
|
|
623
|
+
check(elicitations.length > before, "no elicitation/create was sent");
|
|
624
|
+
check(
|
|
625
|
+
result.text === `elicitation:accept:${CONFORMANCE_ELICITATION_ANSWER}`,
|
|
626
|
+
`reply ${JSON.stringify(result.text)}`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
await step(
|
|
631
|
+
"mcp over acp" /* McpAcp */,
|
|
632
|
+
{
|
|
633
|
+
requires: sessionCases,
|
|
634
|
+
skip: described()?.acp.mcp.includes(McpTransportKind.Acp) === true ? null : "harness does not accept acp MCP servers"
|
|
635
|
+
},
|
|
636
|
+
async () => {
|
|
637
|
+
const before = mcpMessages.length;
|
|
638
|
+
const result = await turn(
|
|
639
|
+
directive(
|
|
640
|
+
ConformanceDirective.Mcp,
|
|
641
|
+
`${CONFORMANCE_ACP_SERVER_NAME} ${CONFORMANCE_MCP_TOOL} {"text":"via-acp"}`
|
|
642
|
+
)
|
|
643
|
+
);
|
|
644
|
+
check(
|
|
645
|
+
mcpMessages.slice(before).some((message) => message.serverId === CONFORMANCE_ACP_SERVER_ID),
|
|
646
|
+
"no mcp/message reached the client"
|
|
647
|
+
);
|
|
648
|
+
const expected = `mcp:${conformanceEchoText(nonce, "via-acp")}`;
|
|
649
|
+
check(
|
|
650
|
+
result.text === expected,
|
|
651
|
+
`reply ${JSON.stringify(result.text)}, expected ${JSON.stringify(expected)}`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
await step(
|
|
656
|
+
"mcp over http" /* McpHttp */,
|
|
657
|
+
{
|
|
658
|
+
requires: sessionCases,
|
|
659
|
+
skip: described()?.acp.mcp.includes(McpTransportKind.Http) !== true ? "harness does not accept http MCP servers" : httpFixture == null ? "no httpMcp host given" : null
|
|
660
|
+
},
|
|
661
|
+
async () => {
|
|
662
|
+
const result = await turn(
|
|
663
|
+
directive(
|
|
664
|
+
ConformanceDirective.Mcp,
|
|
665
|
+
`${CONFORMANCE_HTTP_SERVER_NAME} ${CONFORMANCE_MCP_TOOL} {"text":"via-http"}`
|
|
666
|
+
)
|
|
667
|
+
);
|
|
668
|
+
const expected = `mcp:${conformanceEchoText(nonce, "via-http")}`;
|
|
669
|
+
check(
|
|
670
|
+
result.text === expected,
|
|
671
|
+
`reply ${JSON.stringify(result.text)}, expected ${JSON.stringify(expected)}`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
await step(
|
|
676
|
+
"observe" /* Observe */,
|
|
677
|
+
{
|
|
678
|
+
requires: sessionCases,
|
|
679
|
+
skip: !final ? "not part of the plain ACP v2 profile" : described()?.supports.observe !== true ? "harness does not declare supports.observe" : null
|
|
680
|
+
},
|
|
681
|
+
async () => {
|
|
682
|
+
const id = requireSession();
|
|
683
|
+
const observed = `observed-${nonce}`;
|
|
684
|
+
const before = logFor(id).items.length;
|
|
685
|
+
await requireConnection().agent.notify(OBSERVE_METHOD, {
|
|
686
|
+
sessionId: id,
|
|
687
|
+
content: [{ type: "text", text: observed }]
|
|
688
|
+
});
|
|
689
|
+
await sleep(200);
|
|
690
|
+
check(
|
|
691
|
+
!logFor(id).items.slice(before).some((update) => update.sessionUpdate === "state_update"),
|
|
692
|
+
"_final/session/observe started a turn"
|
|
693
|
+
);
|
|
694
|
+
const result = await turn(directive(ConformanceDirective.History));
|
|
695
|
+
check(
|
|
696
|
+
result.text.split("\n").includes(observed),
|
|
697
|
+
"observed content is missing from the history"
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
await step("error stop" /* ErrorStop */, { requires: sessionCases }, async () => {
|
|
702
|
+
const result = await turn(directive(ConformanceDirective.Error));
|
|
703
|
+
check(result.stopReason === "error", `stopReason ${result.stopReason}, expected error`);
|
|
704
|
+
if (final) {
|
|
705
|
+
const meta = providerErrorSchema.safeParse(
|
|
706
|
+
"_meta" in result.idle ? result.idle._meta : null
|
|
707
|
+
);
|
|
708
|
+
check(
|
|
709
|
+
meta.success,
|
|
710
|
+
'_meta["final.ai"].providerError {code, message, retryable} is missing on the error stop'
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
await step("cancel" /* Cancel */, { requires: sessionCases }, async () => {
|
|
715
|
+
const id = requireSession();
|
|
716
|
+
const sent = await sendPrompt(directive(ConformanceDirective.Slow));
|
|
717
|
+
await waitFor(
|
|
718
|
+
id,
|
|
719
|
+
(items) => items.slice(sent.from).some((update) => update.sessionUpdate === "agent_message_chunk"),
|
|
720
|
+
"slow turn start"
|
|
721
|
+
);
|
|
722
|
+
await requireConnection().agent.notify(methods.agent.session.cancel, { sessionId: id });
|
|
723
|
+
const result = await awaitTurn(sent);
|
|
724
|
+
check(
|
|
725
|
+
result.stopReason === "cancelled",
|
|
726
|
+
`stopReason ${result.stopReason}, expected cancelled`
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
await step(
|
|
730
|
+
"resume with replay" /* ResumeReplay */,
|
|
731
|
+
{
|
|
732
|
+
requires: ["stream" /* Stream */],
|
|
733
|
+
skip: described()?.supports.resumeReplay === true ? null : "harness does not declare supports.resumeReplay (resume re-seeds with session/new)"
|
|
734
|
+
},
|
|
735
|
+
async () => {
|
|
736
|
+
const id = requireSession();
|
|
737
|
+
if (endpoint == null) throw failure("no endpoint");
|
|
738
|
+
const previous = endpoint;
|
|
739
|
+
const original = [...logFor(id).items];
|
|
740
|
+
await disconnect();
|
|
741
|
+
const resumed = await args.harness.resume({
|
|
742
|
+
...args.openArgs,
|
|
743
|
+
ref: previous.ref,
|
|
744
|
+
lastSeq: original.length
|
|
745
|
+
});
|
|
746
|
+
check(
|
|
747
|
+
resumed.ref.generation === previous.ref.generation,
|
|
748
|
+
`resume() moved the generation (${previous.ref.generation} to ${resumed.ref.generation}) although the harness declares supports.resumeReplay`
|
|
749
|
+
);
|
|
750
|
+
const from = logFor(id).items.length;
|
|
751
|
+
await reconnect({ resumed, previous, replayFrom: { type: "start" } });
|
|
752
|
+
await sleep(replayGraceMs);
|
|
753
|
+
const replayed = logFor(id).items.slice(from);
|
|
754
|
+
check(
|
|
755
|
+
textOfUpdates(replayed).includes(echoText),
|
|
756
|
+
"replay from start did not include the first turn"
|
|
757
|
+
);
|
|
758
|
+
if (!final) return `${replayed.length} updates replayed`;
|
|
759
|
+
if (!advertised.afterSeqReplay) {
|
|
760
|
+
return `${replayed.length} updates replayed; _final/after_seq not advertised`;
|
|
761
|
+
}
|
|
762
|
+
const tail = 2;
|
|
763
|
+
const fromSeq = logFor(id).items.length;
|
|
764
|
+
const cursor = resumeReplayFor({ advertised, held: original.length - tail });
|
|
765
|
+
await requireConnection().agent.request(methods.agent.session.resume, {
|
|
766
|
+
sessionId: id,
|
|
767
|
+
cwd,
|
|
768
|
+
mcpServers,
|
|
769
|
+
replayFrom: cursor.replayFrom
|
|
770
|
+
});
|
|
771
|
+
await sleep(replayGraceMs);
|
|
772
|
+
const afterSeq = logFor(id).items.slice(fromSeq);
|
|
773
|
+
check(
|
|
774
|
+
afterSeq.length === tail && afterSeq.every(
|
|
775
|
+
(update, index) => update.sessionUpdate === original[original.length - tail + index]?.sessionUpdate
|
|
776
|
+
),
|
|
777
|
+
`replay after seq returned ${afterSeq.length} updates, expected the last ${tail}`
|
|
778
|
+
);
|
|
779
|
+
return `${replayed.length} updates replayed; after_seq ok`;
|
|
780
|
+
}
|
|
781
|
+
);
|
|
782
|
+
await step("park/resume" /* ParkResume */, { requires: sessionCases }, async () => {
|
|
783
|
+
const id = requireSession();
|
|
784
|
+
if (endpoint == null) throw failure("no endpoint");
|
|
785
|
+
const previous = endpoint;
|
|
786
|
+
await args.harness.park(previous.ref);
|
|
787
|
+
await disconnect();
|
|
788
|
+
const resumed = await args.harness.resume({
|
|
789
|
+
...args.openArgs,
|
|
790
|
+
ref: previous.ref,
|
|
791
|
+
lastSeq: logFor(id).items.length
|
|
792
|
+
});
|
|
793
|
+
const how = await reconnect({ resumed, previous, replayFrom: null });
|
|
794
|
+
const text = `after park ${nonce}`;
|
|
795
|
+
const result = await turn(directive(ConformanceDirective.Echo, text));
|
|
796
|
+
check(result.text === text, `reply after resume ${JSON.stringify(result.text)}`);
|
|
797
|
+
check(result.stopReason === "end_turn", `stopReason ${result.stopReason}`);
|
|
798
|
+
return how === "resumed" ? "session resumed" : "session recreated (state did not survive)";
|
|
799
|
+
});
|
|
800
|
+
const declaredSteer = () => described()?.promptWhileRunning ?? PromptWhileRunning.Queue;
|
|
801
|
+
const survives = () => described()?.sessionSurvivesDisconnect === true;
|
|
802
|
+
await step("prompt while running" /* PromptWhileRunning */, { requires: sessionCases }, async () => {
|
|
803
|
+
const id = requireSession();
|
|
804
|
+
const declared = declaredSteer();
|
|
805
|
+
const first = await sendPrompt(directive(ConformanceDirective.Wait, "800"));
|
|
806
|
+
await waitForText(id, first.from, "wait:started", "the running turn");
|
|
807
|
+
const steer = `steer-${nonce}`;
|
|
808
|
+
const second = await sendPrompt(directive(ConformanceDirective.Echo, steer)).catch(
|
|
809
|
+
(cause) => {
|
|
810
|
+
throw refused(cause);
|
|
811
|
+
}
|
|
812
|
+
);
|
|
813
|
+
await waitFor(
|
|
814
|
+
id,
|
|
815
|
+
(items2) => {
|
|
816
|
+
const after = afterEcho(id, first.from, second.messageId);
|
|
817
|
+
return after != null && textOfUpdates(items2.slice(first.from)).includes(steer) && textOfUpdates(items2.slice(first.from)).includes("wait:done") && items2.slice(first.from).filter(isIdle).length >= (declared === PromptWhileRunning.Queue ? 2 : 1) && after.some(isIdle);
|
|
818
|
+
},
|
|
819
|
+
"both prompts to settle"
|
|
820
|
+
);
|
|
821
|
+
const items = logFor(id).items.slice(first.from);
|
|
822
|
+
const secondEcho = items.findIndex(
|
|
823
|
+
(update) => echoOf(update)?.messageId === second.messageId
|
|
824
|
+
);
|
|
825
|
+
const firstIdle = items.findIndex(isIdle);
|
|
826
|
+
const observed = secondEcho < firstIdle ? PromptWhileRunning.Inject : PromptWhileRunning.Queue;
|
|
827
|
+
check(
|
|
828
|
+
observed === declared,
|
|
829
|
+
`declares promptWhileRunning: ${declared}, but the second prompt's echo came ${observed === PromptWhileRunning.Inject ? "before" : "after"} the running turn's idle (${observed})`
|
|
830
|
+
);
|
|
831
|
+
for (const idle of items.filter(isIdle)) {
|
|
832
|
+
check(
|
|
833
|
+
stopReasonOf(idle) === "end_turn",
|
|
834
|
+
`a turn ended ${stopReasonOf(idle) ?? "without a stopReason"}`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
if (observed === PromptWhileRunning.Inject) {
|
|
838
|
+
check(
|
|
839
|
+
textOfUpdates(items.slice(0, firstIdle)).includes(steer),
|
|
840
|
+
"the injected prompt was not handled before the running turn ended"
|
|
841
|
+
);
|
|
842
|
+
return "inject: one idle ended both turns";
|
|
843
|
+
}
|
|
844
|
+
const nextIdle = items.slice(secondEcho).findIndex(isIdle);
|
|
845
|
+
check(
|
|
846
|
+
textOfUpdates(items.slice(secondEcho, secondEcho + nextIdle)).includes(steer),
|
|
847
|
+
"the queued prompt was not answered in its own turn"
|
|
848
|
+
);
|
|
849
|
+
return "queue: running, idle, running, idle";
|
|
850
|
+
});
|
|
851
|
+
await step("cancel reaching a prompt sent while running" /* CancelWhileRunning */, { requires: sessionCases }, async () => {
|
|
852
|
+
const id = requireSession();
|
|
853
|
+
const declared = declaredSteer();
|
|
854
|
+
const first = await sendPrompt(directive(ConformanceDirective.Slow));
|
|
855
|
+
await waitForText(id, first.from, "slow:started", "the slow turn");
|
|
856
|
+
const dropped = `cancelled-${nonce}`;
|
|
857
|
+
await sendPrompt(directive(ConformanceDirective.Echo, dropped)).catch((cause) => {
|
|
858
|
+
throw refused(cause);
|
|
859
|
+
});
|
|
860
|
+
await sleep(100);
|
|
861
|
+
await requireConnection().agent.notify(methods.agent.session.cancel, { sessionId: id });
|
|
862
|
+
await waitFor(id, (items2) => items2.slice(first.from).some(isIdle), "the cancelled turn");
|
|
863
|
+
await sleep(replayGraceMs);
|
|
864
|
+
const items = logFor(id).items.slice(first.from);
|
|
865
|
+
const idleAt = items.findIndex(isIdle);
|
|
866
|
+
check(
|
|
867
|
+
stopReasonOf(items[idleAt] ?? items[0] ?? { sessionUpdate: "state_update" }) === "cancelled",
|
|
868
|
+
`the turn ended ${stopReasonOf(items[idleAt] ?? { sessionUpdate: "state_update" }) ?? "without a stopReason"}, expected cancelled`
|
|
869
|
+
);
|
|
870
|
+
const after = items.slice(idleAt + 1);
|
|
871
|
+
check(
|
|
872
|
+
!after.some(isRunning) && textOfUpdates(after) === "",
|
|
873
|
+
"work went on after session/cancel: it must reach every accepted prompt"
|
|
874
|
+
);
|
|
875
|
+
if (declared === PromptWhileRunning.Queue) {
|
|
876
|
+
check(
|
|
877
|
+
!textOfUpdates(items).includes(dropped),
|
|
878
|
+
"a queued prompt ran although session/cancel arrived before it started"
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return `${declared}: one cancelled idle, nothing after it`;
|
|
882
|
+
});
|
|
883
|
+
await step("unanswered prompt across a dropped connection" /* NoResend */, { requires: sessionCases }, async () => {
|
|
884
|
+
const token = `once-${nonce}`;
|
|
885
|
+
promptDrop.armed = true;
|
|
886
|
+
promptDrop.dropped = false;
|
|
887
|
+
const outcome = sendPrompt(directive(ConformanceDirective.Echo, token)).then(
|
|
888
|
+
() => "answered",
|
|
889
|
+
() => "unanswered"
|
|
890
|
+
);
|
|
891
|
+
const deadline = Date.now() + timeoutMs;
|
|
892
|
+
while (!promptDrop.dropped && Date.now() < deadline) await sleep(5);
|
|
893
|
+
promptDrop.armed = false;
|
|
894
|
+
check(promptDrop.dropped, "the harness never answered the session/prompt");
|
|
895
|
+
const { how, from } = await dropAndResume({ type: "start" });
|
|
896
|
+
check(await outcome === "unanswered", "the prompt response reached the client");
|
|
897
|
+
const id = requireSession();
|
|
898
|
+
await sleep(replayGraceMs);
|
|
899
|
+
const echoes = (items) => accumulateAgentMessages(
|
|
900
|
+
items.flatMap((update) => {
|
|
901
|
+
if (update.sessionUpdate === "user_message")
|
|
902
|
+
return [{ ...update, sessionUpdate: "agent_message" }];
|
|
903
|
+
if (update.sessionUpdate === "user_message_chunk")
|
|
904
|
+
return [{ ...update, sessionUpdate: "agent_message_chunk" }];
|
|
905
|
+
return [];
|
|
906
|
+
})
|
|
907
|
+
).filter((message) => message.text.includes(token)).length;
|
|
908
|
+
const inserted = echoes(logFor(id).items.slice(from)) > 0;
|
|
909
|
+
if (!inserted) await turn(directive(ConformanceDirective.Echo, token));
|
|
910
|
+
else if (survives()) await waitForText(id, from, token, "the inserted prompt to be answered");
|
|
911
|
+
await sleep(replayGraceMs);
|
|
912
|
+
const count = echoes(logFor(id).items.slice(from));
|
|
913
|
+
check(count === 1, `the prompt was echoed ${count} times after the reconnect, expected once`);
|
|
914
|
+
return inserted ? `inserted before the drop (${how}); the replay showed it, so it was not resent` : `not inserted (${how}); sent once more`;
|
|
915
|
+
});
|
|
916
|
+
await step("disconnect mid-turn" /* DisconnectMidTurn */, { requires: sessionCases }, async () => {
|
|
917
|
+
const sent = await sendPrompt(directive(ConformanceDirective.Wait, "600"));
|
|
918
|
+
await waitForText(requireSession(), sent.from, "wait:started", "the running turn");
|
|
919
|
+
const { how, from } = await dropAndResume({ type: "start" });
|
|
920
|
+
const id = requireSession();
|
|
921
|
+
if (survives()) {
|
|
922
|
+
check(
|
|
923
|
+
how === "resumed",
|
|
924
|
+
"declares sessionSurvivesDisconnect but the session could not be resumed with replay"
|
|
925
|
+
);
|
|
926
|
+
await waitFor(
|
|
927
|
+
id,
|
|
928
|
+
() => {
|
|
929
|
+
const after2 = afterEcho(id, from, sent.messageId);
|
|
930
|
+
return after2 != null && textOfUpdates(after2).includes("wait:done") && after2.some(isIdle);
|
|
931
|
+
},
|
|
932
|
+
"the running turn to finish across the dropped connection"
|
|
933
|
+
);
|
|
934
|
+
const after = afterEcho(id, from, sent.messageId) ?? [];
|
|
935
|
+
const idle = after.find(isIdle);
|
|
936
|
+
check(
|
|
937
|
+
idle != null && stopReasonOf(idle) === "end_turn",
|
|
938
|
+
`the turn ended ${idle == null ? "never" : stopReasonOf(idle)} after the reconnect`
|
|
939
|
+
);
|
|
940
|
+
return "the turn kept running and was replayed";
|
|
941
|
+
}
|
|
942
|
+
const text = `after-drop-${nonce}`;
|
|
943
|
+
const result = await ownTurn(await sendPrompt(directive(ConformanceDirective.Echo, text)));
|
|
944
|
+
check(result.text.includes(text), `reply after the reconnect ${JSON.stringify(result.text)}`);
|
|
945
|
+
return `state not kept (${how}): the client ends the turn harness_state_lost`;
|
|
946
|
+
});
|
|
947
|
+
await step(
|
|
948
|
+
"disconnect mid-permission" /* DisconnectMidPermission */,
|
|
949
|
+
{ requires: ["permission" /* Permission */] },
|
|
950
|
+
async () => {
|
|
951
|
+
holdPermissions = true;
|
|
952
|
+
const before = permissionIds.length;
|
|
953
|
+
const sent = await sendPrompt(directive(ConformanceDirective.Permission));
|
|
954
|
+
const deadline = Date.now() + timeoutMs;
|
|
955
|
+
while (permissionIds.length === before && Date.now() < deadline) await sleep(5);
|
|
956
|
+
holdPermissions = false;
|
|
957
|
+
const held = permissionIds[before];
|
|
958
|
+
check(held != null, "no session/request_permission was sent");
|
|
959
|
+
const { how, from } = await dropAndResume({ type: "start" });
|
|
960
|
+
const id = requireSession();
|
|
961
|
+
if (survives()) {
|
|
962
|
+
check(
|
|
963
|
+
how === "resumed",
|
|
964
|
+
"declares sessionSurvivesDisconnect but the session could not be resumed with replay"
|
|
965
|
+
);
|
|
966
|
+
const reraised = Date.now() + timeoutMs;
|
|
967
|
+
while (!permissionIds.some(
|
|
968
|
+
(entry) => entry.epoch === epoch && entry.requestId === held?.requestId
|
|
969
|
+
) && Date.now() < reraised) {
|
|
970
|
+
await sleep(5);
|
|
971
|
+
}
|
|
972
|
+
check(
|
|
973
|
+
permissionIds.some(
|
|
974
|
+
(entry) => entry.epoch === epoch && entry.requestId === held?.requestId
|
|
975
|
+
),
|
|
976
|
+
`the open permission request (id ${JSON.stringify(held?.requestId)}) was not re-raised with the same id after session/resume`
|
|
977
|
+
);
|
|
978
|
+
await waitFor(
|
|
979
|
+
id,
|
|
980
|
+
() => {
|
|
981
|
+
const after = afterEcho(id, from, sent.messageId);
|
|
982
|
+
return after != null && after.some(isIdle);
|
|
983
|
+
},
|
|
984
|
+
"the turn to finish after the re-raised request was answered"
|
|
985
|
+
);
|
|
986
|
+
check(
|
|
987
|
+
textOfUpdates(afterEcho(id, from, sent.messageId) ?? []).includes(
|
|
988
|
+
"permission:allow_once"
|
|
989
|
+
),
|
|
990
|
+
"the answer to the re-raised request did not reach the turn"
|
|
991
|
+
);
|
|
992
|
+
return "re-raised with the same id and answered";
|
|
993
|
+
}
|
|
994
|
+
const text = `after-permission-drop-${nonce}`;
|
|
995
|
+
const result = await ownTurn(await sendPrompt(directive(ConformanceDirective.Echo, text)));
|
|
996
|
+
check(
|
|
997
|
+
result.text.includes(text),
|
|
998
|
+
`reply after the reconnect ${JSON.stringify(result.text)}`
|
|
999
|
+
);
|
|
1000
|
+
return `state not kept (${how}): the request resolves cancelled, the turn harness_state_lost`;
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
await step(
|
|
1004
|
+
"native session identity across resume" /* NativeIdPreservation */,
|
|
1005
|
+
{
|
|
1006
|
+
requires: sessionCases,
|
|
1007
|
+
skip: args.nativeSessionId == null ? "driver has no independent native session identity probe" : described()?.supports.resumeReplay !== true ? "harness does not declare supports.resumeReplay" : null
|
|
1008
|
+
},
|
|
1009
|
+
async () => {
|
|
1010
|
+
const id = requireSession();
|
|
1011
|
+
const before = await args.nativeSessionId?.(id);
|
|
1012
|
+
check(
|
|
1013
|
+
before != null && before.length > 0,
|
|
1014
|
+
"native session identity was unavailable before resume"
|
|
1015
|
+
);
|
|
1016
|
+
const marker = `native-resume-${nonce}`;
|
|
1017
|
+
await turn(directive(ConformanceDirective.Echo, marker));
|
|
1018
|
+
const { how } = await dropAndResume(null);
|
|
1019
|
+
check(
|
|
1020
|
+
how === "resumed" && requireSession() === id,
|
|
1021
|
+
"ACP session identity changed across resume"
|
|
1022
|
+
);
|
|
1023
|
+
const history = await turn(directive(ConformanceDirective.History));
|
|
1024
|
+
check(history.text.includes(marker), "native history was lost across resume");
|
|
1025
|
+
check(
|
|
1026
|
+
await args.nativeSessionId?.(id) === before,
|
|
1027
|
+
"native session identity changed across resume"
|
|
1028
|
+
);
|
|
1029
|
+
return "independent native identity and conversation history retained across resume";
|
|
1030
|
+
}
|
|
1031
|
+
);
|
|
1032
|
+
await step(
|
|
1033
|
+
"native session id change" /* NativeIdChange */,
|
|
1034
|
+
{
|
|
1035
|
+
requires: sessionCases,
|
|
1036
|
+
skip: args.nativeSessionId == null ? "driver has no independent native session identity probe" : args.nativeClearPrompt === null ? "harness has no public native session reset action" : null
|
|
1037
|
+
},
|
|
1038
|
+
async () => {
|
|
1039
|
+
const id = requireSession();
|
|
1040
|
+
const before = await args.nativeSessionId?.(id);
|
|
1041
|
+
check(
|
|
1042
|
+
before != null && before.length > 0,
|
|
1043
|
+
"native session identity was unavailable before clear"
|
|
1044
|
+
);
|
|
1045
|
+
const foreign = foreignUpdates.length;
|
|
1046
|
+
const cleared = await turn(args.nativeClearPrompt ?? directive(ConformanceDirective.Clear));
|
|
1047
|
+
check(cleared.stopReason === "end_turn", `clear ended ${cleared.stopReason}`);
|
|
1048
|
+
const after = await args.nativeSessionId?.(id);
|
|
1049
|
+
check(
|
|
1050
|
+
after != null && after.length > 0,
|
|
1051
|
+
"native session identity was unavailable after clear"
|
|
1052
|
+
);
|
|
1053
|
+
check(after !== before, "clear did not change the native session identity");
|
|
1054
|
+
const text = `after-clear-${nonce}`;
|
|
1055
|
+
const result = await turn(directive(ConformanceDirective.Echo, text));
|
|
1056
|
+
check(result.text === text, `reply after clear ${JSON.stringify(result.text)}`);
|
|
1057
|
+
let detail = "one ACP session id across the native change";
|
|
1058
|
+
if (described()?.supports.resumeReplay === true) {
|
|
1059
|
+
const { how } = await dropAndResume(null);
|
|
1060
|
+
check(
|
|
1061
|
+
how === "resumed" && requireSession() === id,
|
|
1062
|
+
"session/resume with the ACP session id failed after the native id changed"
|
|
1063
|
+
);
|
|
1064
|
+
const resumedText = `after-clear-resume-${nonce}`;
|
|
1065
|
+
const resumed = await turn(directive(ConformanceDirective.Echo, resumedText));
|
|
1066
|
+
check(resumed.text === resumedText, `reply after resume ${JSON.stringify(resumed.text)}`);
|
|
1067
|
+
check(
|
|
1068
|
+
await args.nativeSessionId?.(id) === after,
|
|
1069
|
+
"resume did not retain the changed native session identity"
|
|
1070
|
+
);
|
|
1071
|
+
detail += ", and session/resume by it";
|
|
1072
|
+
}
|
|
1073
|
+
check(
|
|
1074
|
+
foreignUpdates.length === foreign,
|
|
1075
|
+
`updates arrived under ACP session ids the client never opened: ${foreignUpdates.slice(foreign).join(", ")}`
|
|
1076
|
+
);
|
|
1077
|
+
return detail;
|
|
1078
|
+
}
|
|
1079
|
+
);
|
|
1080
|
+
await step(
|
|
1081
|
+
"multi-megabyte tool result and image" /* OversizedFrame */,
|
|
1082
|
+
{
|
|
1083
|
+
requires: sessionCases,
|
|
1084
|
+
skip: mcpServers.length === 0 ? "no client MCP fixture can be attached" : null
|
|
1085
|
+
},
|
|
1086
|
+
async () => {
|
|
1087
|
+
const frames = inboundFrameBytes.length;
|
|
1088
|
+
const result = await turn(directive(ConformanceDirective.Big, String(BIG_RESULT_BYTES)));
|
|
1089
|
+
check(result.stopReason === "end_turn", `stopReason ${result.stopReason}`);
|
|
1090
|
+
check(
|
|
1091
|
+
result.text.includes("big:done"),
|
|
1092
|
+
`reply ${JSON.stringify(result.text.slice(0, 200))}`
|
|
1093
|
+
);
|
|
1094
|
+
const largest = Math.max(0, ...inboundFrameBytes.slice(frames));
|
|
1095
|
+
const blocks = toolBlocksOf(result.updates);
|
|
1096
|
+
const links = blocks.flatMap((block) => {
|
|
1097
|
+
const link = resourceLinkSchema.safeParse(block);
|
|
1098
|
+
return link.success ? [link.data] : [];
|
|
1099
|
+
});
|
|
1100
|
+
const textLink = links.find((link) => link.size === BIG_RESULT_BYTES);
|
|
1101
|
+
const imageLink = links.find((link) => link.mimeType?.startsWith("image/") === true);
|
|
1102
|
+
if (!final) {
|
|
1103
|
+
const inline = blocks.some(
|
|
1104
|
+
(block) => inlineTextSchema.safeParse(block).data?.text.length === BIG_RESULT_BYTES
|
|
1105
|
+
);
|
|
1106
|
+
check(textLink != null || inline, "the tool result did not arrive whole");
|
|
1107
|
+
return `largest frame ${largest} bytes${largest > MAX_ACP_FRAME_BYTES ? " (the session service backstop rewrites it)" : ""}`;
|
|
1108
|
+
}
|
|
1109
|
+
check(
|
|
1110
|
+
largest <= MAX_ACP_FRAME_BYTES,
|
|
1111
|
+
`a ${largest}-byte frame exceeded the ${MAX_ACP_FRAME_BYTES}-byte frame rule`
|
|
1112
|
+
);
|
|
1113
|
+
check(
|
|
1114
|
+
textLink != null,
|
|
1115
|
+
"the multi-megabyte tool result was not moved to a resource_link carrying its full size"
|
|
1116
|
+
);
|
|
1117
|
+
const description2 = textLink?.description ?? "";
|
|
1118
|
+
check(
|
|
1119
|
+
new TextEncoder().encode(description2).length <= RESOURCE_LINK_DESCRIPTION_BYTES && description2.length > 0,
|
|
1120
|
+
"the resource_link description must be the first 4 KiB of the text"
|
|
1121
|
+
);
|
|
1122
|
+
check(
|
|
1123
|
+
imageLink != null && (imageLink.size ?? 0) > MAX_INLINE_MEDIA_BYTES,
|
|
1124
|
+
"the image above 256 KiB was not moved to a resource_link"
|
|
1125
|
+
);
|
|
1126
|
+
return `largest frame ${largest} bytes; text and image linked`;
|
|
1127
|
+
}
|
|
1128
|
+
);
|
|
1129
|
+
await step(
|
|
1130
|
+
"graceful close" /* GracefulClose */,
|
|
1131
|
+
{
|
|
1132
|
+
requires: ["initialize" /* Initialize */],
|
|
1133
|
+
skip: initializeResponse()?.capabilities?.session == null ? "the agent advertises no session capabilities (no session/close)" : null
|
|
1134
|
+
},
|
|
1135
|
+
async () => {
|
|
1136
|
+
const opened = await args.harness.open({
|
|
1137
|
+
...args.openArgs,
|
|
1138
|
+
harnessSessionId: crypto.randomUUID()
|
|
1139
|
+
});
|
|
1140
|
+
const dialed = await args.dial(opened);
|
|
1141
|
+
const closeIds = /* @__PURE__ */ new Map();
|
|
1142
|
+
const idleSeen = /* @__PURE__ */ new Set();
|
|
1143
|
+
const settledBeforeReply = /* @__PURE__ */ new Set();
|
|
1144
|
+
const outgoing = dialed.writable.getWriter();
|
|
1145
|
+
const watch = watchInitializeVersion({
|
|
1146
|
+
writable: new WritableStream({
|
|
1147
|
+
write: async (wire) => {
|
|
1148
|
+
for (const message of isBatch(wire) ? wire : [wire]) {
|
|
1149
|
+
const close = z.object({
|
|
1150
|
+
id: z.union([z.string(), z.number()]),
|
|
1151
|
+
method: z.literal("session/close"),
|
|
1152
|
+
params: z.object({ sessionId: z.string() })
|
|
1153
|
+
}).safeParse(message);
|
|
1154
|
+
if (close.success)
|
|
1155
|
+
closeIds.set(JSON.stringify(close.data.id), close.data.params.sessionId);
|
|
1156
|
+
}
|
|
1157
|
+
await outgoing.write(wire);
|
|
1158
|
+
},
|
|
1159
|
+
close: () => outgoing.close(),
|
|
1160
|
+
abort: (reason) => outgoing.abort(reason)
|
|
1161
|
+
}),
|
|
1162
|
+
readable: dialed.readable.pipeThrough(
|
|
1163
|
+
new TransformStream({
|
|
1164
|
+
transform: (wire, controller) => {
|
|
1165
|
+
for (const message of isBatch(wire) ? wire : [wire]) {
|
|
1166
|
+
const state = z.object({
|
|
1167
|
+
method: z.literal("session/update"),
|
|
1168
|
+
params: z.object({
|
|
1169
|
+
sessionId: z.string(),
|
|
1170
|
+
update: z.object({
|
|
1171
|
+
sessionUpdate: z.literal("state_update"),
|
|
1172
|
+
state: z.string()
|
|
1173
|
+
})
|
|
1174
|
+
})
|
|
1175
|
+
}).safeParse(message);
|
|
1176
|
+
if (state.success) {
|
|
1177
|
+
if (state.data.params.update.state === "idle")
|
|
1178
|
+
idleSeen.add(state.data.params.sessionId);
|
|
1179
|
+
else idleSeen.delete(state.data.params.sessionId);
|
|
1180
|
+
}
|
|
1181
|
+
const reply = z.object({ id: z.union([z.string(), z.number()]), result: z.unknown() }).safeParse(message);
|
|
1182
|
+
const sessionId2 = reply.success ? closeIds.get(JSON.stringify(reply.data.id)) : null;
|
|
1183
|
+
if (sessionId2 != null && idleSeen.has(sessionId2))
|
|
1184
|
+
settledBeforeReply.add(sessionId2);
|
|
1185
|
+
}
|
|
1186
|
+
controller.enqueue(wire);
|
|
1187
|
+
}
|
|
1188
|
+
})
|
|
1189
|
+
)
|
|
1190
|
+
});
|
|
1191
|
+
const second = kitClient.connect(watch.stream);
|
|
1192
|
+
try {
|
|
1193
|
+
await initializeOver(second, watch);
|
|
1194
|
+
const created = await second.agent.request(methods.agent.session.new, {
|
|
1195
|
+
cwd,
|
|
1196
|
+
mcpServers: []
|
|
1197
|
+
});
|
|
1198
|
+
const id = created.sessionId;
|
|
1199
|
+
knownSessions.add(id);
|
|
1200
|
+
await second.agent.request(methods.agent.session.prompt, {
|
|
1201
|
+
sessionId: id,
|
|
1202
|
+
prompt: [{ type: "text", text: directive(ConformanceDirective.Wait, "2000") }],
|
|
1203
|
+
_meta: promptMeta
|
|
1204
|
+
});
|
|
1205
|
+
await waitForText(id, 0, "wait:started", "the running turn");
|
|
1206
|
+
await second.agent.request(methods.agent.session.close, { sessionId: id });
|
|
1207
|
+
check(
|
|
1208
|
+
settledBeforeReply.has(id),
|
|
1209
|
+
"session/close was answered while the turn was still running: close must settle the program state first"
|
|
1210
|
+
);
|
|
1211
|
+
await args.harness.close(opened.ref);
|
|
1212
|
+
await args.harness.close(opened.ref, { force: true });
|
|
1213
|
+
return "the turn settled before session/close answered; close then force close";
|
|
1214
|
+
} finally {
|
|
1215
|
+
second.close();
|
|
1216
|
+
dialed.close?.();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
);
|
|
1220
|
+
await step("ACP v1 agent rejected" /* V1Rejected */, {}, async () => {
|
|
1221
|
+
const v1 = createLegacyProtocolAgent();
|
|
1222
|
+
const watch = watchInitializeVersion(v1.stream);
|
|
1223
|
+
const probe = kitClient.connect(watch.stream);
|
|
1224
|
+
try {
|
|
1225
|
+
const cause = await initializeOver(probe, watch).then(
|
|
1226
|
+
() => null,
|
|
1227
|
+
(cause2) => cause2
|
|
1228
|
+
);
|
|
1229
|
+
check(
|
|
1230
|
+
isUnsupportedProtocolVersionError(cause),
|
|
1231
|
+
`a v1 initialize reply was not refused with unsupported_protocol_version (${String(cause)})`
|
|
1232
|
+
);
|
|
1233
|
+
check(
|
|
1234
|
+
v1.received.join(",") === "initialize",
|
|
1235
|
+
`the client sent ${v1.received.join(", ")} to a v1 agent`
|
|
1236
|
+
);
|
|
1237
|
+
return "refused with unsupported_protocol_version";
|
|
1238
|
+
} finally {
|
|
1239
|
+
probe.close();
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
await step(
|
|
1243
|
+
"health" /* Health */,
|
|
1244
|
+
{ requires: ["open" /* Open */] },
|
|
1245
|
+
async () => {
|
|
1246
|
+
if (endpoint == null) throw failure("no endpoint");
|
|
1247
|
+
const health = await args.harness.health(endpoint.ref, {
|
|
1248
|
+
config: args.openArgs.config,
|
|
1249
|
+
secrets: args.openArgs.secrets
|
|
1250
|
+
});
|
|
1251
|
+
check(health.ok, `health not ok: ${health.detail ?? ""}`);
|
|
1252
|
+
}
|
|
1253
|
+
);
|
|
1254
|
+
await step(
|
|
1255
|
+
"close" /* Close */,
|
|
1256
|
+
{ requires: ["open" /* Open */] },
|
|
1257
|
+
async () => {
|
|
1258
|
+
if (endpoint == null) throw failure("no endpoint");
|
|
1259
|
+
if (connection != null && sessionId != null) {
|
|
1260
|
+
await requireConnection().agent.request(methods.agent.session.close, { sessionId }).catch(() => void 0);
|
|
1261
|
+
}
|
|
1262
|
+
await disconnect();
|
|
1263
|
+
await args.harness.close(endpoint.ref);
|
|
1264
|
+
}
|
|
1265
|
+
);
|
|
1266
|
+
} finally {
|
|
1267
|
+
await disconnect().catch(() => void 0);
|
|
1268
|
+
for (const cleanup of cleanups) await cleanup().catch(() => void 0);
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
profile,
|
|
1272
|
+
passed: cases.every((entry) => entry.status !== "failed" /* Failed */),
|
|
1273
|
+
description,
|
|
1274
|
+
cases
|
|
1275
|
+
};
|
|
1276
|
+
};
|
|
1277
|
+
const describeHarnessConformance = (api, args) => {
|
|
1278
|
+
api.describe(args.name ?? `harness conformance (${args.profile})`, () => {
|
|
1279
|
+
let report = null;
|
|
1280
|
+
const budget = (args.timeoutMs ?? 15e3) * HARNESS_CONFORMANCE_CASES.length;
|
|
1281
|
+
api.beforeAll(async () => {
|
|
1282
|
+
report = await runHarnessConformance(args);
|
|
1283
|
+
}, budget);
|
|
1284
|
+
for (const name of HARNESS_CONFORMANCE_CASES) {
|
|
1285
|
+
api.it(name, async () => {
|
|
1286
|
+
const result = report?.cases.find((entry) => entry.name === name);
|
|
1287
|
+
if (result == null) throw new Error(`case ${name} did not run`);
|
|
1288
|
+
if (result.status === "failed" /* Failed */)
|
|
1289
|
+
throw new Error(result.error ?? "failed");
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
};
|
|
1294
|
+
export {
|
|
1295
|
+
ConformanceCaseStatus,
|
|
1296
|
+
ConformanceProfile,
|
|
1297
|
+
HARNESS_CONFORMANCE_CASES,
|
|
1298
|
+
HarnessConformanceCase,
|
|
1299
|
+
conformanceOpenArgs,
|
|
1300
|
+
describeHarnessConformance,
|
|
1301
|
+
runHarnessConformance
|
|
1302
|
+
};
|
|
1303
|
+
//# sourceMappingURL=harness-kit.mjs.map
|