@plaited/acp-harness 0.3.2 → 0.4.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.
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Zod schemas for headless ACP adapter configuration.
3
+ *
4
+ * @remarks
5
+ * These schemas define how to interact with ANY headless CLI agent via a
6
+ * schema-driven approach. No hardcoded agent-specific logic - the schema
7
+ * defines everything: command, flags, output parsing rules.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ import { z } from 'zod'
13
+
14
+ // ============================================================================
15
+ // Output Event Mapping Schema
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Schema for matching CLI output to ACP update types.
20
+ *
21
+ * @remarks
22
+ * Uses JSONPath-like patterns to match events in CLI JSON output
23
+ * and map them to ACP session update types.
24
+ */
25
+ export const OutputEventMatchSchema = z.object({
26
+ /** JSONPath to match event type in CLI output (e.g., "$.type") */
27
+ path: z.string(),
28
+ /** Value to match at the path (e.g., "tool_use") */
29
+ value: z.string(),
30
+ })
31
+
32
+ /** Output event match type */
33
+ export type OutputEventMatch = z.infer<typeof OutputEventMatchSchema>
34
+
35
+ /**
36
+ * Schema for extracting content from matched events.
37
+ *
38
+ * @remarks
39
+ * Paths can be:
40
+ * - JSONPath expressions (e.g., "$.message.text")
41
+ * - Literal strings in single quotes (e.g., "'pending'")
42
+ */
43
+ export const OutputEventExtractSchema = z.object({
44
+ /** JSONPath to extract main content */
45
+ content: z.string().optional(),
46
+ /** JSONPath to extract title (for tool calls) */
47
+ title: z.string().optional(),
48
+ /** JSONPath to extract status (or literal like "'pending'") */
49
+ status: z.string().optional(),
50
+ })
51
+
52
+ /** Output event extract type */
53
+ export type OutputEventExtract = z.infer<typeof OutputEventExtractSchema>
54
+
55
+ /**
56
+ * Schema for mapping CLI output events to ACP update types.
57
+ *
58
+ * @remarks
59
+ * Each mapping specifies:
60
+ * 1. How to match events (match.path + match.value)
61
+ * 2. What ACP update type to emit (emitAs)
62
+ * 3. What content to extract (extract)
63
+ */
64
+ export const OutputEventMappingSchema = z.object({
65
+ /** Matching criteria for CLI output */
66
+ match: OutputEventMatchSchema,
67
+ /** ACP session update type to emit */
68
+ emitAs: z.enum(['thought', 'tool_call', 'message', 'plan']),
69
+ /** Content extraction configuration */
70
+ extract: OutputEventExtractSchema.optional(),
71
+ })
72
+
73
+ /** Output event mapping type */
74
+ export type OutputEventMapping = z.infer<typeof OutputEventMappingSchema>
75
+
76
+ // ============================================================================
77
+ // Prompt Configuration Schema
78
+ // ============================================================================
79
+
80
+ /**
81
+ * Schema for how to pass prompts to the CLI.
82
+ */
83
+ export const PromptConfigSchema = z.object({
84
+ /** Flag to pass prompt (e.g., "-p", "--prompt"). Omit for stdin. */
85
+ flag: z.string().optional(),
86
+ /** Format for stdin input in stream mode */
87
+ stdinFormat: z.enum(['text', 'json']).optional(),
88
+ })
89
+
90
+ /** Prompt configuration type */
91
+ export type PromptConfig = z.infer<typeof PromptConfigSchema>
92
+
93
+ // ============================================================================
94
+ // Output Configuration Schema
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Schema for output format configuration.
99
+ */
100
+ export const OutputConfigSchema = z.object({
101
+ /** Flag for output format (e.g., "--output-format") */
102
+ flag: z.string(),
103
+ /** Value for output format (e.g., "stream-json") */
104
+ value: z.string(),
105
+ })
106
+
107
+ /** Output configuration type */
108
+ export type OutputConfig = z.infer<typeof OutputConfigSchema>
109
+
110
+ // ============================================================================
111
+ // Resume Configuration Schema
112
+ // ============================================================================
113
+
114
+ /**
115
+ * Schema for session resume support (stream mode).
116
+ */
117
+ export const ResumeConfigSchema = z.object({
118
+ /** Flag to resume session (e.g., "--resume") */
119
+ flag: z.string(),
120
+ /** JSONPath to extract session ID from output */
121
+ sessionIdPath: z.string(),
122
+ })
123
+
124
+ /** Resume configuration type */
125
+ export type ResumeConfig = z.infer<typeof ResumeConfigSchema>
126
+
127
+ // ============================================================================
128
+ // Result Configuration Schema
129
+ // ============================================================================
130
+
131
+ /**
132
+ * Schema for final result extraction.
133
+ */
134
+ export const ResultConfigSchema = z.object({
135
+ /** JSONPath to match result type (e.g., "$.type") */
136
+ matchPath: z.string(),
137
+ /** Value indicating final result (e.g., "result") */
138
+ matchValue: z.string(),
139
+ /** JSONPath to extract result content */
140
+ contentPath: z.string(),
141
+ })
142
+
143
+ /** Result configuration type */
144
+ export type ResultConfig = z.infer<typeof ResultConfigSchema>
145
+
146
+ // ============================================================================
147
+ // Main Adapter Schema
148
+ // ============================================================================
149
+
150
+ /**
151
+ * Schema for headless ACP adapter configuration.
152
+ *
153
+ * @remarks
154
+ * This schema defines everything needed to interact with a headless CLI agent:
155
+ * - Command and flags to spawn
156
+ * - How to pass prompts
157
+ * - How to parse output
158
+ * - Session handling mode
159
+ *
160
+ * Example (Claude):
161
+ * ```json
162
+ * {
163
+ * "version": 1,
164
+ * "name": "claude-headless",
165
+ * "command": ["claude"],
166
+ * "sessionMode": "stream",
167
+ * "prompt": { "flag": "-p" },
168
+ * "output": { "flag": "--output-format", "value": "stream-json" },
169
+ * "outputEvents": [...]
170
+ * }
171
+ * ```
172
+ */
173
+ export const HeadlessAdapterSchema = z.object({
174
+ /** Schema version for forward compatibility */
175
+ version: z.literal(1),
176
+
177
+ /** Human-readable adapter name */
178
+ name: z.string(),
179
+
180
+ /** Base command to spawn (e.g., ["claude"], ["gemini"]) */
181
+ command: z.array(z.string()),
182
+
183
+ /**
184
+ * Session mode determines how multi-turn conversations work:
185
+ * - 'stream': Keep process alive, multi-turn via stdin
186
+ * - 'iterative': New process per turn, accumulate context in prompt
187
+ */
188
+ sessionMode: z.enum(['stream', 'iterative']),
189
+
190
+ /** How to pass the prompt */
191
+ prompt: PromptConfigSchema,
192
+
193
+ /** Output format configuration */
194
+ output: OutputConfigSchema,
195
+
196
+ /** Flags for auto-approval in headless mode (e.g., ["--allowedTools", "*"]) */
197
+ autoApprove: z.array(z.string()).optional(),
198
+
199
+ /** Session resume support (stream mode only) */
200
+ resume: ResumeConfigSchema.optional(),
201
+
202
+ /** Working directory flag (if CLI needs explicit --cwd) */
203
+ cwdFlag: z.string().optional(),
204
+
205
+ /** Output event mappings - how to parse CLI output into ACP updates */
206
+ outputEvents: z.array(OutputEventMappingSchema),
207
+
208
+ /** Final result extraction configuration */
209
+ result: ResultConfigSchema,
210
+
211
+ /** Template for formatting conversation history (iterative mode only) */
212
+ historyTemplate: z.string().optional(),
213
+ })
214
+
215
+ /** Headless adapter configuration type */
216
+ export type HeadlessAdapterConfig = z.infer<typeof HeadlessAdapterSchema>
217
+
218
+ // ============================================================================
219
+ // Validation Helpers
220
+ // ============================================================================
221
+
222
+ /**
223
+ * Validates and parses a headless adapter configuration.
224
+ *
225
+ * @param config - Raw configuration object (e.g., from JSON file)
226
+ * @returns Validated HeadlessAdapterConfig
227
+ * @throws ZodError if validation fails
228
+ */
229
+ export const parseHeadlessConfig = (config: unknown): HeadlessAdapterConfig => {
230
+ return HeadlessAdapterSchema.parse(config)
231
+ }
232
+
233
+ /**
234
+ * Safely validates a headless adapter configuration.
235
+ *
236
+ * @param config - Raw configuration object
237
+ * @returns Result with success/failure and data or error
238
+ */
239
+ export const safeParseHeadlessConfig = (config: unknown) => {
240
+ return HeadlessAdapterSchema.safeParse(config)
241
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Headless ACP adapter factory - schema-driven adapter for any CLI agent.
3
+ *
4
+ * @remarks
5
+ * Re-exports public API from the headless module. The headless adapter enables
6
+ * capturing trajectories from ANY headless CLI agent by defining a schema
7
+ * that describes how to interact with the CLI.
8
+ *
9
+ * **CLI Usage:**
10
+ * ```bash
11
+ * acp-harness headless --schema ./my-agent.json
12
+ * ```
13
+ *
14
+ * **Programmatic Usage:**
15
+ * ```typescript
16
+ * import { parseHeadlessConfig, createSessionManager } from '@plaited/acp-harness/headless'
17
+ *
18
+ * const schema = parseHeadlessConfig(jsonConfig)
19
+ * const sessions = createSessionManager({ schema })
20
+ * ```
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+
25
+ // Schema definitions and parsing
26
+ export {
27
+ HeadlessAdapterSchema,
28
+ OutputConfigSchema,
29
+ OutputEventExtractSchema,
30
+ OutputEventMappingSchema,
31
+ OutputEventMatchSchema,
32
+ PromptConfigSchema,
33
+ parseHeadlessConfig,
34
+ ResultConfigSchema,
35
+ ResumeConfigSchema,
36
+ safeParseHeadlessConfig,
37
+ } from './headless.schemas.ts'
38
+ // Types
39
+ export type {
40
+ HeadlessAdapterConfig,
41
+ OutputConfig,
42
+ OutputEventExtract,
43
+ OutputEventMapping,
44
+ OutputEventMatch,
45
+ PromptConfig,
46
+ ResultConfig,
47
+ ResumeConfig,
48
+ } from './headless.types.ts'
49
+ // CLI entry point
50
+ export { headless } from './headless-cli.ts'
51
+ export type { HistoryBuilder, HistoryBuilderConfig, HistoryTurn } from './headless-history-builder.ts'
52
+ // History builder
53
+ export { createHistoryBuilder } from './headless-history-builder.ts'
54
+ export type {
55
+ OutputParser,
56
+ ParsedResult,
57
+ ParsedUpdate,
58
+ ResultParseResult,
59
+ SessionUpdateType,
60
+ } from './headless-output-parser.ts'
61
+ // Output parser
62
+ export { createOutputParser, jsonPath, jsonPathString } from './headless-output-parser.ts'
63
+ export type {
64
+ PromptResult,
65
+ Session,
66
+ SessionManager,
67
+ SessionManagerConfig,
68
+ UpdateCallback,
69
+ } from './headless-session-manager.ts'
70
+ // Session manager
71
+ export { createSessionManager } from './headless-session-manager.ts'
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Type exports for headless ACP adapter.
3
+ *
4
+ * @remarks
5
+ * Re-exports all types from the schemas module for external consumers.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ export type {
11
+ HeadlessAdapterConfig,
12
+ OutputConfig,
13
+ OutputEventExtract,
14
+ OutputEventMapping,
15
+ OutputEventMatch,
16
+ PromptConfig,
17
+ ResultConfig,
18
+ ResumeConfig,
19
+ } from './headless.schemas.ts'
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Headless Adapter integration Tests - Claude Code
3
+ *
4
+ * @remarks
5
+ * These tests verify the headless ACP adapter works correctly with Claude Code
6
+ * using the schema-driven approach from `.claude/skills/acp-adapters/schemas/`.
7
+ *
8
+ * Run locally with API key:
9
+ * ```bash
10
+ * ANTHROPIC_API_KEY=sk-... bun test ./src/tests/acp-claude.spec.ts
11
+ * ```
12
+ *
13
+ * Prerequisites:
14
+ * 1. Claude CLI installed (`bunx @anthropic-ai/claude-code`)
15
+ * 2. API key: `ANTHROPIC_API_KEY` environment variable
16
+ *
17
+ * These tests make real API calls and consume credits.
18
+ *
19
+ * MCP servers are auto-discovered from project root via:
20
+ * - `.mcp.json` - MCP server configuration
21
+ */
22
+
23
+ import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from 'bun:test'
24
+ import { join } from 'node:path'
25
+ import { type ACPClient, createACPClient } from '../acp-client.ts'
26
+ import { createPrompt, summarizeResponse } from '../acp-helpers.ts'
27
+
28
+ // Long timeout for real agent interactions (2 minutes)
29
+ setDefaultTimeout(120000)
30
+
31
+ // Use project root as cwd - agents discover MCP servers from config files
32
+ const PROJECT_ROOT = process.cwd()
33
+
34
+ // Schema path for Claude headless adapter
35
+ const SCHEMA_PATH = join(PROJECT_ROOT, '.claude/skills/acp-adapters/schemas/claude-headless.json')
36
+
37
+ // Get API key from environment
38
+ const API_KEY = process.env.ANTHROPIC_API_KEY ?? ''
39
+
40
+ // Skip all tests if no API key is available
41
+ const describeWithApiKey = API_KEY ? describe : describe.skip
42
+
43
+ describeWithApiKey('Headless Adapter Integration - Claude', () => {
44
+ let client: ACPClient
45
+
46
+ beforeAll(async () => {
47
+ // Use headless adapter with Claude schema
48
+ client = createACPClient({
49
+ command: ['bun', 'src/headless-cli.ts', '--', '--schema', SCHEMA_PATH],
50
+ timeout: 120000, // 2 min timeout for initialization
51
+ env: {
52
+ ANTHROPIC_API_KEY: API_KEY,
53
+ },
54
+ })
55
+
56
+ await client.connect()
57
+ })
58
+
59
+ afterAll(async () => {
60
+ await client?.disconnect()
61
+ })
62
+
63
+ test('connects and initializes via headless adapter', () => {
64
+ expect(client.isConnected()).toBe(true)
65
+
66
+ const initResult = client.getInitializeResult()
67
+ expect(initResult).toBeDefined()
68
+ expect(initResult?.protocolVersion).toBeDefined()
69
+ })
70
+
71
+ test('reports agent capabilities', () => {
72
+ const capabilities = client.getCapabilities()
73
+ expect(capabilities).toBeDefined()
74
+ })
75
+
76
+ test('creates session with project cwd', async () => {
77
+ // Session uses project root - agent discovers MCP servers from .mcp.json
78
+ const session = await client.createSession({
79
+ cwd: PROJECT_ROOT,
80
+ })
81
+
82
+ expect(session).toBeDefined()
83
+ expect(session.id).toBeDefined()
84
+ expect(typeof session.id).toBe('string')
85
+ })
86
+
87
+ test('sends prompt and receives response', async () => {
88
+ const session = await client.createSession({
89
+ cwd: PROJECT_ROOT,
90
+ })
91
+
92
+ // Simple prompt that doesn't require tools
93
+ const { result, updates } = await client.promptSync(
94
+ session.id,
95
+ createPrompt('What is 2 + 2? Reply with just the number.'),
96
+ )
97
+
98
+ expect(result).toBeDefined()
99
+ expect(updates).toBeInstanceOf(Array)
100
+
101
+ // Summarize and verify response structure
102
+ const summary = summarizeResponse(updates)
103
+ expect(summary.text).toBeDefined()
104
+ expect(summary.text.length).toBeGreaterThan(0)
105
+ })
106
+
107
+ test('streaming prompt yields updates', async () => {
108
+ const session = await client.createSession({
109
+ cwd: PROJECT_ROOT,
110
+ })
111
+
112
+ const events: string[] = []
113
+
114
+ for await (const event of client.prompt(session.id, createPrompt('Say "hello" and nothing else.'))) {
115
+ events.push(event.type)
116
+ if (event.type === 'complete') {
117
+ expect(event.result).toBeDefined()
118
+ }
119
+ }
120
+
121
+ expect(events).toContain('complete')
122
+ })
123
+
124
+ test('uses MCP server from project config', async () => {
125
+ // This test verifies that Claude discovers MCP servers from .mcp.json
126
+ // The bun-docs MCP server is configured at project root
127
+ const session = await client.createSession({
128
+ cwd: PROJECT_ROOT,
129
+ })
130
+
131
+ // Query the bun-docs MCP server (configured in .mcp.json)
132
+ const { updates } = await client.promptSync(
133
+ session.id,
134
+ createPrompt(
135
+ 'Use the bun-docs MCP server to search for information about Bun.serve(). ' +
136
+ 'What are the key options for creating an HTTP server with Bun?',
137
+ ),
138
+ )
139
+
140
+ const summary = summarizeResponse(updates)
141
+
142
+ // Response should contain Bun server-related information
143
+ expect(summary.text.length).toBeGreaterThan(0)
144
+ // Should mention server/HTTP-related concepts from Bun docs
145
+ expect(summary.text.toLowerCase()).toMatch(/serve|server|http|port|fetch|handler/)
146
+ })
147
+
148
+ test('multi-turn conversation maintains context', async () => {
149
+ // Multi-turn: multiple prompts to same session via headless adapter
150
+ const session = await client.createSession({
151
+ cwd: PROJECT_ROOT,
152
+ })
153
+
154
+ // Turn 1: Establish context
155
+ const { updates: turn1Updates } = await client.promptSync(
156
+ session.id,
157
+ createPrompt('Remember this number: 42. Just confirm you have it.'),
158
+ )
159
+ const turn1Summary = summarizeResponse(turn1Updates)
160
+ expect(turn1Summary.text).toMatch(/42|forty.?two|remember/i)
161
+
162
+ // Turn 2: Reference previous context
163
+ const { updates: turn2Updates } = await client.promptSync(
164
+ session.id,
165
+ createPrompt('What number did I ask you to remember? Reply with just the number.'),
166
+ )
167
+ const turn2Summary = summarizeResponse(turn2Updates)
168
+ expect(turn2Summary.text).toMatch(/42/)
169
+ })
170
+ })
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Headless Adapter integration Tests - Gemini CLI
3
+ *
4
+ * @remarks
5
+ * These tests verify the headless ACP adapter works correctly with Gemini CLI
6
+ * using the schema-driven approach from `.claude/skills/acp-adapters/schemas/`.
7
+ *
8
+ * Run locally with API key:
9
+ * ```bash
10
+ * GEMINI_API_KEY=... bun test ./src/tests/acp-gemini.spec.ts
11
+ * ```
12
+ *
13
+ * Prerequisites:
14
+ * 1. Gemini CLI installed (`npm install -g @anthropic-ai/gemini-cli`)
15
+ * 2. API key: `GEMINI_API_KEY` environment variable
16
+ *
17
+ * These tests make real API calls and consume credits.
18
+ *
19
+ * MCP servers are auto-discovered from project root via:
20
+ * - `.gemini/settings.json` - Gemini MCP server configuration
21
+ */
22
+
23
+ import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from 'bun:test'
24
+ import { join } from 'node:path'
25
+ import { type ACPClient, createACPClient } from '../acp-client.ts'
26
+ import { createPrompt, summarizeResponse } from '../acp-helpers.ts'
27
+
28
+ // Long timeout for real agent interactions (2 minutes)
29
+ setDefaultTimeout(120000)
30
+
31
+ // Use project root as cwd - agents discover MCP servers from config files
32
+ const PROJECT_ROOT = process.cwd()
33
+
34
+ // Schema path for Gemini headless adapter
35
+ const SCHEMA_PATH = join(PROJECT_ROOT, '.claude/skills/acp-adapters/schemas/gemini-headless.json')
36
+
37
+ // Gemini CLI accepts GEMINI_API_KEY
38
+ // Use either one if available
39
+ const GEMINI_API_KEY = process.env.GEMINI_API_KEY ?? ''
40
+
41
+ // Skip all tests if no API key is available
42
+ const describeWithApiKey = GEMINI_API_KEY ? describe : describe.skip
43
+
44
+ describeWithApiKey('Headless Adapter Integration - Gemini', () => {
45
+ let client: ACPClient
46
+
47
+ beforeAll(async () => {
48
+ // Use headless adapter with Gemini schema
49
+ // Pass both API key variants - Gemini CLI should pick up whichever it prefers
50
+ client = createACPClient({
51
+ command: ['bun', 'src/headless-cli.ts', '--', '--schema', SCHEMA_PATH],
52
+ timeout: 120000, // 2 min timeout for initialization
53
+ env: {
54
+ GEMINI_API_KEY,
55
+ },
56
+ })
57
+
58
+ await client.connect()
59
+ })
60
+
61
+ afterAll(async () => {
62
+ await client?.disconnect()
63
+ })
64
+
65
+ test('connects and initializes via headless adapter', () => {
66
+ expect(client.isConnected()).toBe(true)
67
+
68
+ const initResult = client.getInitializeResult()
69
+ expect(initResult).toBeDefined()
70
+ expect(initResult?.protocolVersion).toBeDefined()
71
+ })
72
+
73
+ test('reports agent capabilities', () => {
74
+ const capabilities = client.getCapabilities()
75
+ expect(capabilities).toBeDefined()
76
+ })
77
+
78
+ test('creates session with project cwd', async () => {
79
+ // Session uses project root - agent discovers MCP servers from .gemini/settings.json
80
+ const session = await client.createSession({
81
+ cwd: PROJECT_ROOT,
82
+ })
83
+
84
+ expect(session).toBeDefined()
85
+ expect(session.id).toBeDefined()
86
+ expect(typeof session.id).toBe('string')
87
+ })
88
+
89
+ test('sends prompt and receives response', async () => {
90
+ const session = await client.createSession({
91
+ cwd: PROJECT_ROOT,
92
+ })
93
+
94
+ // Simple prompt that doesn't require tools
95
+ const { result, updates } = await client.promptSync(
96
+ session.id,
97
+ createPrompt('What is 2 + 2? Reply with just the number.'),
98
+ )
99
+
100
+ expect(result).toBeDefined()
101
+ expect(updates).toBeInstanceOf(Array)
102
+
103
+ // Summarize and verify response structure
104
+ const summary = summarizeResponse(updates)
105
+ expect(summary.text).toBeDefined()
106
+ expect(summary.text.length).toBeGreaterThan(0)
107
+ // Should contain "4" somewhere in the response
108
+ expect(summary.text).toMatch(/4/)
109
+ })
110
+
111
+ test('streaming prompt yields updates', async () => {
112
+ const session = await client.createSession({
113
+ cwd: PROJECT_ROOT,
114
+ })
115
+
116
+ const events: string[] = []
117
+
118
+ for await (const event of client.prompt(session.id, createPrompt('Say "hello" and nothing else.'))) {
119
+ events.push(event.type)
120
+ if (event.type === 'complete') {
121
+ expect(event.result).toBeDefined()
122
+ }
123
+ }
124
+
125
+ expect(events).toContain('complete')
126
+ })
127
+
128
+ test('uses MCP server from project config', async () => {
129
+ // This test verifies that Gemini discovers MCP servers from .gemini/settings.json
130
+ // The agent-client-protocol MCP server is configured at project root
131
+ const session = await client.createSession({
132
+ cwd: PROJECT_ROOT,
133
+ })
134
+
135
+ // Query the agent-client-protocol MCP server (configured in .gemini/settings.json)
136
+ const { updates } = await client.promptSync(
137
+ session.id,
138
+ createPrompt(
139
+ 'Use the agent-client-protocol MCP server to search for information about ACP. ' +
140
+ 'What is the Agent Client Protocol and what problem does it solve?',
141
+ ),
142
+ )
143
+
144
+ const summary = summarizeResponse(updates)
145
+
146
+ // Response should contain ACP-related information
147
+ expect(summary.text.length).toBeGreaterThan(0)
148
+ // Should mention protocol/agent-related concepts
149
+ expect(summary.text.toLowerCase()).toMatch(/agent|protocol|client|json-rpc|stdio/)
150
+ })
151
+
152
+ test('multi-turn conversation maintains context (iterative mode)', async () => {
153
+ // Multi-turn via headless adapter in iterative mode (history accumulation)
154
+ const session = await client.createSession({
155
+ cwd: PROJECT_ROOT,
156
+ })
157
+
158
+ // Turn 1: Establish context
159
+ const { updates: turn1Updates } = await client.promptSync(
160
+ session.id,
161
+ createPrompt('Remember this number: 42. Just confirm you have it.'),
162
+ )
163
+ const turn1Summary = summarizeResponse(turn1Updates)
164
+ expect(turn1Summary.text).toMatch(/42|forty.?two|remember/i)
165
+
166
+ // Turn 2: Reference previous context
167
+ const { updates: turn2Updates } = await client.promptSync(
168
+ session.id,
169
+ createPrompt('What number did I ask you to remember? Reply with just the number.'),
170
+ )
171
+ const turn2Summary = summarizeResponse(turn2Updates)
172
+ expect(turn2Summary.text).toMatch(/42/)
173
+ })
174
+ })