@agent-relay/spawner 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.
package/dist/types.js ADDED
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Spawner Types
3
+ *
4
+ * Zod schemas for agent spawning and lifecycle management types.
5
+ * These types are used across the spawner, daemon, and dashboard.
6
+ */
7
+ import { z } from 'zod';
8
+ // =============================================================================
9
+ // Enums and Basic Types
10
+ // =============================================================================
11
+ /**
12
+ * When shadow agents should activate
13
+ */
14
+ export const SpeakOnTriggerSchema = z.enum([
15
+ 'SESSION_END',
16
+ 'CODE_WRITTEN',
17
+ 'REVIEW_REQUEST',
18
+ 'EXPLICIT_ASK',
19
+ 'ALL_MESSAGES',
20
+ ]);
21
+ /**
22
+ * Shadow role preset names
23
+ */
24
+ export const ShadowRolePresetSchema = z.enum(['reviewer', 'auditor', 'active']);
25
+ /**
26
+ * Shadow execution mode
27
+ */
28
+ export const ShadowModeSchema = z.enum(['subagent', 'process']);
29
+ /**
30
+ * Policy source types
31
+ */
32
+ export const PolicySourceSchema = z.enum(['repo', 'local', 'workspace', 'default']);
33
+ // =============================================================================
34
+ // Policy Types
35
+ // =============================================================================
36
+ /**
37
+ * Policy decision result
38
+ */
39
+ export const PolicyDecisionSchema = z.object({
40
+ allowed: z.boolean(),
41
+ reason: z.string(),
42
+ policySource: PolicySourceSchema,
43
+ });
44
+ // =============================================================================
45
+ // Spawn Request/Result Types
46
+ // =============================================================================
47
+ /**
48
+ * Request to spawn a new agent
49
+ */
50
+ export const SpawnRequestSchema = z.object({
51
+ /** Worker agent name (must be unique) */
52
+ name: z.string(),
53
+ /** CLI tool (e.g., 'claude', 'claude:opus', 'codex', 'gemini') */
54
+ cli: z.string(),
55
+ /** Initial task to inject after spawn */
56
+ task: z.string(),
57
+ /** Optional team name for organization */
58
+ team: z.string().optional(),
59
+ /** Working directory (defaults to project root) */
60
+ cwd: z.string().optional(),
61
+ /** Name of requesting agent (for policy enforcement) */
62
+ spawnerName: z.string().optional(),
63
+ /** Interactive mode - disables auto-accept of permission prompts */
64
+ interactive: z.boolean().optional(),
65
+ /** Shadow execution mode (subagent = no extra process) */
66
+ shadowMode: ShadowModeSchema.optional(),
67
+ /** Primary agent to shadow (if this agent is a shadow) */
68
+ shadowOf: z.string().optional(),
69
+ /** Shadow agent profile to use (for subagent mode) */
70
+ shadowAgent: z.string().optional(),
71
+ /** When to trigger the shadow (for subagent mode) */
72
+ shadowTriggers: z.array(SpeakOnTriggerSchema).optional(),
73
+ /** When the shadow should speak (default: ['EXPLICIT_ASK']) */
74
+ shadowSpeakOn: z.array(SpeakOnTriggerSchema).optional(),
75
+ /** User ID for per-user credential storage in shared workspaces */
76
+ userId: z.string().optional(),
77
+ });
78
+ /**
79
+ * Result of a spawn operation
80
+ */
81
+ export const SpawnResultSchema = z.object({
82
+ success: z.boolean(),
83
+ name: z.string(),
84
+ /** PID of the spawned process (for pty-based workers) */
85
+ pid: z.number().optional(),
86
+ error: z.string().optional(),
87
+ /** Policy decision details if spawn was blocked by policy */
88
+ policyDecision: PolicyDecisionSchema.optional(),
89
+ });
90
+ /**
91
+ * Information about an active worker
92
+ */
93
+ export const WorkerInfoSchema = z.object({
94
+ name: z.string(),
95
+ cli: z.string(),
96
+ task: z.string(),
97
+ /** Optional team name this agent belongs to */
98
+ team: z.string().optional(),
99
+ spawnedAt: z.number(),
100
+ /** PID of the pty process */
101
+ pid: z.number().optional(),
102
+ });
103
+ // =============================================================================
104
+ // Shadow Agent Types
105
+ // =============================================================================
106
+ /**
107
+ * Primary agent configuration for spawnWithShadow
108
+ */
109
+ export const PrimaryAgentConfigSchema = z.object({
110
+ /** Agent name */
111
+ name: z.string(),
112
+ /** CLI command (default: 'claude') */
113
+ command: z.string().optional(),
114
+ /** Initial task to send to the agent */
115
+ task: z.string().optional(),
116
+ /** Team name to organize under */
117
+ team: z.string().optional(),
118
+ });
119
+ /**
120
+ * Shadow agent configuration for spawnWithShadow
121
+ */
122
+ export const ShadowAgentConfigSchema = z.object({
123
+ /** Shadow agent name */
124
+ name: z.string(),
125
+ /** CLI command (default: same as primary) */
126
+ command: z.string().optional(),
127
+ /** Role preset (reviewer, auditor, active) or custom prompt */
128
+ role: z.string().optional(),
129
+ /** Custom speakOn triggers (overrides role preset) */
130
+ speakOn: z.array(SpeakOnTriggerSchema).optional(),
131
+ /** Custom prompt for the shadow agent */
132
+ prompt: z.string().optional(),
133
+ });
134
+ /**
135
+ * Request for spawning a primary agent with its shadow
136
+ */
137
+ export const SpawnWithShadowRequestSchema = z.object({
138
+ /** Primary agent configuration */
139
+ primary: PrimaryAgentConfigSchema,
140
+ /** Shadow agent configuration */
141
+ shadow: ShadowAgentConfigSchema,
142
+ });
143
+ /**
144
+ * Result from spawnWithShadow
145
+ */
146
+ export const SpawnWithShadowResultSchema = z.object({
147
+ success: z.boolean(),
148
+ /** Primary agent spawn result */
149
+ primary: SpawnResultSchema.optional(),
150
+ /** Shadow agent spawn result */
151
+ shadow: SpawnResultSchema.optional(),
152
+ /** Error message if overall operation failed */
153
+ error: z.string().optional(),
154
+ });
155
+ // =============================================================================
156
+ // Bridge/Multi-Project Types
157
+ // =============================================================================
158
+ /**
159
+ * Project configuration for multi-project orchestration
160
+ */
161
+ export const ProjectConfigSchema = z.object({
162
+ /** Absolute path to project root */
163
+ path: z.string(),
164
+ /** Project identifier (derived from path hash) */
165
+ id: z.string(),
166
+ /** Socket path for this project's daemon */
167
+ socketPath: z.string(),
168
+ /** Lead agent name (auto-generated from dirname if not specified) */
169
+ leadName: z.string(),
170
+ /** CLI tool to use (default: claude) */
171
+ cli: z.string(),
172
+ });
173
+ /**
174
+ * Bridge configuration for multi-project coordination
175
+ */
176
+ export const BridgeConfigSchema = z.object({
177
+ /** Projects to bridge */
178
+ projects: z.array(ProjectConfigSchema),
179
+ /** CLI override for all projects */
180
+ cliOverride: z.string().optional(),
181
+ });
182
+ /**
183
+ * Lead agent information
184
+ */
185
+ export const LeadInfoSchema = z.object({
186
+ /** Lead agent name */
187
+ name: z.string(),
188
+ /** Project this lead manages */
189
+ projectId: z.string(),
190
+ /** Whether lead is currently connected */
191
+ connected: z.boolean(),
192
+ });
193
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@agent-relay/spawner",
3
+ "version": "0.1.0",
4
+ "description": "Agent spawning types and utilities for Agent Relay",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./types": {
14
+ "types": "./dist/types.d.ts",
15
+ "import": "./dist/types.js",
16
+ "default": "./dist/types.js"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "clean": "rm -rf dist"
24
+ },
25
+ "dependencies": {
26
+ "zod": "^3.23.8"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.9.3",
30
+ "vitest": "^3.0.0"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src"
35
+ ],
36
+ "keywords": [
37
+ "agent-relay",
38
+ "spawner",
39
+ "agent-lifecycle"
40
+ ],
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/AgentWorkforce/relay.git",
44
+ "directory": "packages/spawner"
45
+ },
46
+ "license": "MIT"
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @agent-relay/spawner
3
+ *
4
+ * Agent spawning types and utilities for Agent Relay.
5
+ * Phase 2A extraction - pure types with Zod validation.
6
+ */
7
+
8
+ export * from './types.js';
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Spawner Types Tests
3
+ *
4
+ * TDD approach - tests written first to define expected behavior.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ SpeakOnTriggerSchema,
10
+ ShadowRolePresetSchema,
11
+ PolicyDecisionSchema,
12
+ SpawnRequestSchema,
13
+ SpawnResultSchema,
14
+ WorkerInfoSchema,
15
+ PrimaryAgentConfigSchema,
16
+ ShadowAgentConfigSchema,
17
+ SpawnWithShadowRequestSchema,
18
+ SpawnWithShadowResultSchema,
19
+ ProjectConfigSchema,
20
+ BridgeConfigSchema,
21
+ LeadInfoSchema,
22
+ } from './types.js';
23
+
24
+ describe('Spawner Types', () => {
25
+ describe('SpeakOnTriggerSchema', () => {
26
+ it('should validate all trigger types', () => {
27
+ expect(SpeakOnTriggerSchema.parse('SESSION_END')).toBe('SESSION_END');
28
+ expect(SpeakOnTriggerSchema.parse('CODE_WRITTEN')).toBe('CODE_WRITTEN');
29
+ expect(SpeakOnTriggerSchema.parse('REVIEW_REQUEST')).toBe('REVIEW_REQUEST');
30
+ expect(SpeakOnTriggerSchema.parse('EXPLICIT_ASK')).toBe('EXPLICIT_ASK');
31
+ expect(SpeakOnTriggerSchema.parse('ALL_MESSAGES')).toBe('ALL_MESSAGES');
32
+ });
33
+
34
+ it('should reject invalid triggers', () => {
35
+ expect(() => SpeakOnTriggerSchema.parse('INVALID')).toThrow();
36
+ expect(() => SpeakOnTriggerSchema.parse('session_end')).toThrow(); // lowercase
37
+ });
38
+ });
39
+
40
+ describe('ShadowRolePresetSchema', () => {
41
+ it('should validate role presets', () => {
42
+ expect(ShadowRolePresetSchema.parse('reviewer')).toBe('reviewer');
43
+ expect(ShadowRolePresetSchema.parse('auditor')).toBe('auditor');
44
+ expect(ShadowRolePresetSchema.parse('active')).toBe('active');
45
+ });
46
+
47
+ it('should reject invalid roles', () => {
48
+ expect(() => ShadowRolePresetSchema.parse('observer')).toThrow();
49
+ });
50
+ });
51
+
52
+ describe('PolicyDecisionSchema', () => {
53
+ it('should validate allowed decision', () => {
54
+ const decision = {
55
+ allowed: true,
56
+ reason: 'Policy permits spawn',
57
+ policySource: 'repo',
58
+ };
59
+ const result = PolicyDecisionSchema.parse(decision);
60
+ expect(result.allowed).toBe(true);
61
+ expect(result.policySource).toBe('repo');
62
+ });
63
+
64
+ it('should validate denied decision', () => {
65
+ const decision = {
66
+ allowed: false,
67
+ reason: 'Agent limit exceeded',
68
+ policySource: 'workspace',
69
+ };
70
+ const result = PolicyDecisionSchema.parse(decision);
71
+ expect(result.allowed).toBe(false);
72
+ expect(result.reason).toBe('Agent limit exceeded');
73
+ });
74
+
75
+ it('should validate all policy sources', () => {
76
+ const sources = ['repo', 'local', 'workspace', 'default'];
77
+ for (const source of sources) {
78
+ const decision = { allowed: true, reason: 'test', policySource: source };
79
+ const result = PolicyDecisionSchema.parse(decision);
80
+ expect(result.policySource).toBe(source);
81
+ }
82
+ });
83
+ });
84
+
85
+ describe('SpawnRequestSchema', () => {
86
+ it('should validate minimal spawn request', () => {
87
+ const request = {
88
+ name: 'Worker1',
89
+ cli: 'claude',
90
+ task: 'Implement feature X',
91
+ };
92
+ const result = SpawnRequestSchema.parse(request);
93
+ expect(result.name).toBe('Worker1');
94
+ expect(result.cli).toBe('claude');
95
+ expect(result.task).toBe('Implement feature X');
96
+ });
97
+
98
+ it('should validate full spawn request', () => {
99
+ const request = {
100
+ name: 'ShadowAgent',
101
+ cli: 'claude:opus',
102
+ task: 'Review code changes',
103
+ team: 'backend',
104
+ cwd: '/workspace/project',
105
+ spawnerName: 'Lead',
106
+ interactive: false,
107
+ shadowMode: 'process',
108
+ shadowOf: 'Primary',
109
+ shadowAgent: 'reviewer',
110
+ shadowTriggers: ['CODE_WRITTEN', 'REVIEW_REQUEST'],
111
+ shadowSpeakOn: ['EXPLICIT_ASK'],
112
+ userId: 'user-123',
113
+ };
114
+ const result = SpawnRequestSchema.parse(request);
115
+ expect(result.shadowMode).toBe('process');
116
+ expect(result.shadowOf).toBe('Primary');
117
+ expect(result.shadowTriggers).toHaveLength(2);
118
+ });
119
+
120
+ it('should validate shadow modes', () => {
121
+ const subagent = { name: 'A', cli: 'claude', task: 't', shadowMode: 'subagent' };
122
+ const process = { name: 'B', cli: 'claude', task: 't', shadowMode: 'process' };
123
+
124
+ expect(SpawnRequestSchema.parse(subagent).shadowMode).toBe('subagent');
125
+ expect(SpawnRequestSchema.parse(process).shadowMode).toBe('process');
126
+ });
127
+
128
+ it('should reject invalid shadow mode', () => {
129
+ const request = { name: 'A', cli: 'claude', task: 't', shadowMode: 'invalid' };
130
+ expect(() => SpawnRequestSchema.parse(request)).toThrow();
131
+ });
132
+
133
+ it('should allow various CLI formats', () => {
134
+ const clis = ['claude', 'claude:opus', 'codex', 'gemini', 'cursor', 'agent'];
135
+ for (const cli of clis) {
136
+ const request = { name: 'Agent', cli, task: 'task' };
137
+ expect(SpawnRequestSchema.parse(request).cli).toBe(cli);
138
+ }
139
+ });
140
+ });
141
+
142
+ describe('SpawnResultSchema', () => {
143
+ it('should validate success result', () => {
144
+ const result = {
145
+ success: true,
146
+ name: 'Worker1',
147
+ pid: 12345,
148
+ };
149
+ const parsed = SpawnResultSchema.parse(result);
150
+ expect(parsed.success).toBe(true);
151
+ expect(parsed.pid).toBe(12345);
152
+ });
153
+
154
+ it('should validate failure result', () => {
155
+ const result = {
156
+ success: false,
157
+ name: 'Worker1',
158
+ error: 'Agent already exists',
159
+ };
160
+ const parsed = SpawnResultSchema.parse(result);
161
+ expect(parsed.success).toBe(false);
162
+ expect(parsed.error).toBe('Agent already exists');
163
+ });
164
+
165
+ it('should validate policy blocked result', () => {
166
+ const result = {
167
+ success: false,
168
+ name: 'Worker1',
169
+ error: 'Policy denied',
170
+ policyDecision: {
171
+ allowed: false,
172
+ reason: 'Spawner not authorized',
173
+ policySource: 'repo',
174
+ },
175
+ };
176
+ const parsed = SpawnResultSchema.parse(result);
177
+ expect(parsed.policyDecision?.allowed).toBe(false);
178
+ expect(parsed.policyDecision?.policySource).toBe('repo');
179
+ });
180
+ });
181
+
182
+ describe('WorkerInfoSchema', () => {
183
+ it('should validate complete worker info', () => {
184
+ const info = {
185
+ name: 'Worker1',
186
+ cli: 'claude',
187
+ task: 'Build API endpoints',
188
+ team: 'backend',
189
+ spawnedAt: 1705920600000,
190
+ pid: 12345,
191
+ };
192
+ const result = WorkerInfoSchema.parse(info);
193
+ expect(result.name).toBe('Worker1');
194
+ expect(result.team).toBe('backend');
195
+ expect(result.pid).toBe(12345);
196
+ });
197
+
198
+ it('should allow worker without optional fields', () => {
199
+ const info = {
200
+ name: 'Worker2',
201
+ cli: 'codex',
202
+ task: 'Fix bug',
203
+ spawnedAt: Date.now(),
204
+ };
205
+ const result = WorkerInfoSchema.parse(info);
206
+ expect(result.team).toBeUndefined();
207
+ expect(result.pid).toBeUndefined();
208
+ });
209
+ });
210
+
211
+ describe('PrimaryAgentConfigSchema', () => {
212
+ it('should validate minimal config', () => {
213
+ const config = { name: 'Lead' };
214
+ const result = PrimaryAgentConfigSchema.parse(config);
215
+ expect(result.name).toBe('Lead');
216
+ });
217
+
218
+ it('should validate full config', () => {
219
+ const config = {
220
+ name: 'Lead',
221
+ command: 'claude:opus',
222
+ task: 'Coordinate team',
223
+ team: 'core',
224
+ };
225
+ const result = PrimaryAgentConfigSchema.parse(config);
226
+ expect(result.command).toBe('claude:opus');
227
+ expect(result.team).toBe('core');
228
+ });
229
+ });
230
+
231
+ describe('ShadowAgentConfigSchema', () => {
232
+ it('should validate minimal shadow config', () => {
233
+ const config = { name: 'ShadowReviewer' };
234
+ const result = ShadowAgentConfigSchema.parse(config);
235
+ expect(result.name).toBe('ShadowReviewer');
236
+ });
237
+
238
+ it('should validate full shadow config', () => {
239
+ const config = {
240
+ name: 'Auditor',
241
+ command: 'codex',
242
+ role: 'auditor',
243
+ speakOn: ['SESSION_END', 'EXPLICIT_ASK'],
244
+ prompt: 'Review all code changes for security issues',
245
+ };
246
+ const result = ShadowAgentConfigSchema.parse(config);
247
+ expect(result.role).toBe('auditor');
248
+ expect(result.speakOn).toHaveLength(2);
249
+ expect(result.prompt).toContain('security');
250
+ });
251
+
252
+ it('should allow custom role string', () => {
253
+ const config = { name: 'Custom', role: 'custom-observer' };
254
+ const result = ShadowAgentConfigSchema.parse(config);
255
+ expect(result.role).toBe('custom-observer');
256
+ });
257
+ });
258
+
259
+ describe('SpawnWithShadowRequestSchema', () => {
260
+ it('should validate spawn with shadow request', () => {
261
+ const request = {
262
+ primary: {
263
+ name: 'Lead',
264
+ command: 'claude',
265
+ task: 'Implement feature',
266
+ team: 'core',
267
+ },
268
+ shadow: {
269
+ name: 'Reviewer',
270
+ role: 'reviewer',
271
+ speakOn: ['CODE_WRITTEN'],
272
+ },
273
+ };
274
+ const result = SpawnWithShadowRequestSchema.parse(request);
275
+ expect(result.primary.name).toBe('Lead');
276
+ expect(result.shadow.name).toBe('Reviewer');
277
+ });
278
+ });
279
+
280
+ describe('SpawnWithShadowResultSchema', () => {
281
+ it('should validate full success result', () => {
282
+ const result = {
283
+ success: true,
284
+ primary: { success: true, name: 'Lead', pid: 1000 },
285
+ shadow: { success: true, name: 'Reviewer', pid: 1001 },
286
+ };
287
+ const parsed = SpawnWithShadowResultSchema.parse(result);
288
+ expect(parsed.primary?.pid).toBe(1000);
289
+ expect(parsed.shadow?.pid).toBe(1001);
290
+ });
291
+
292
+ it('should validate partial success (shadow failed)', () => {
293
+ const result = {
294
+ success: true,
295
+ primary: { success: true, name: 'Lead', pid: 1000 },
296
+ shadow: { success: false, name: 'Reviewer', error: 'No authenticated CLI' },
297
+ error: 'Shadow spawn failed',
298
+ };
299
+ const parsed = SpawnWithShadowResultSchema.parse(result);
300
+ expect(parsed.success).toBe(true); // Overall success because primary worked
301
+ expect(parsed.shadow?.success).toBe(false);
302
+ });
303
+
304
+ it('should validate failure result', () => {
305
+ const result = {
306
+ success: false,
307
+ error: 'Primary agent spawn failed',
308
+ };
309
+ const parsed = SpawnWithShadowResultSchema.parse(result);
310
+ expect(parsed.success).toBe(false);
311
+ expect(parsed.primary).toBeUndefined();
312
+ });
313
+ });
314
+
315
+ describe('ProjectConfigSchema', () => {
316
+ it('should validate project config', () => {
317
+ const config = {
318
+ path: '/workspace/myproject',
319
+ id: 'proj-abc123',
320
+ socketPath: '/tmp/relay-myproject.sock',
321
+ leadName: 'ProjectLead',
322
+ cli: 'claude',
323
+ };
324
+ const result = ProjectConfigSchema.parse(config);
325
+ expect(result.path).toBe('/workspace/myproject');
326
+ expect(result.leadName).toBe('ProjectLead');
327
+ });
328
+ });
329
+
330
+ describe('BridgeConfigSchema', () => {
331
+ it('should validate bridge config', () => {
332
+ const config = {
333
+ projects: [
334
+ {
335
+ path: '/workspace/project1',
336
+ id: 'proj-1',
337
+ socketPath: '/tmp/relay-1.sock',
338
+ leadName: 'Lead1',
339
+ cli: 'claude',
340
+ },
341
+ {
342
+ path: '/workspace/project2',
343
+ id: 'proj-2',
344
+ socketPath: '/tmp/relay-2.sock',
345
+ leadName: 'Lead2',
346
+ cli: 'codex',
347
+ },
348
+ ],
349
+ cliOverride: 'claude:opus',
350
+ };
351
+ const result = BridgeConfigSchema.parse(config);
352
+ expect(result.projects).toHaveLength(2);
353
+ expect(result.cliOverride).toBe('claude:opus');
354
+ });
355
+
356
+ it('should allow bridge config without override', () => {
357
+ const config = { projects: [] };
358
+ const result = BridgeConfigSchema.parse(config);
359
+ expect(result.cliOverride).toBeUndefined();
360
+ });
361
+ });
362
+
363
+ describe('LeadInfoSchema', () => {
364
+ it('should validate lead info', () => {
365
+ const info = {
366
+ name: 'ProjectLead',
367
+ projectId: 'proj-123',
368
+ connected: true,
369
+ };
370
+ const result = LeadInfoSchema.parse(info);
371
+ expect(result.name).toBe('ProjectLead');
372
+ expect(result.connected).toBe(true);
373
+ });
374
+
375
+ it('should validate disconnected lead', () => {
376
+ const info = {
377
+ name: 'OldLead',
378
+ projectId: 'proj-456',
379
+ connected: false,
380
+ };
381
+ const result = LeadInfoSchema.parse(info);
382
+ expect(result.connected).toBe(false);
383
+ });
384
+ });
385
+ });