@clinebot/agents 0.0.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/README.md +145 -0
- package/dist/agent-input.d.ts +2 -0
- package/dist/agent.d.ts +56 -0
- package/dist/extensions.d.ts +21 -0
- package/dist/hooks/engine.d.ts +42 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/lifecycle.d.ts +5 -0
- package/dist/hooks/node.d.ts +2 -0
- package/dist/hooks/subprocess-runner.d.ts +16 -0
- package/dist/hooks/subprocess.d.ts +268 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +49 -0
- package/dist/index.node.d.ts +5 -0
- package/dist/index.node.js +49 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/policies.d.ts +14 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/types.d.ts +35 -0
- package/dist/message-builder.d.ts +31 -0
- package/dist/prompts/cline.d.ts +1 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/runtime/agent-runtime-bus.d.ts +13 -0
- package/dist/runtime/conversation-store.d.ts +16 -0
- package/dist/runtime/lifecycle-orchestrator.d.ts +28 -0
- package/dist/runtime/tool-orchestrator.d.ts +39 -0
- package/dist/runtime/turn-processor.d.ts +21 -0
- package/dist/teams/index.d.ts +3 -0
- package/dist/teams/multi-agent.d.ts +566 -0
- package/dist/teams/spawn-agent-tool.d.ts +85 -0
- package/dist/teams/team-tools.d.ts +51 -0
- package/dist/tools/ask-question.d.ts +12 -0
- package/dist/tools/create.d.ts +59 -0
- package/dist/tools/execution.d.ts +61 -0
- package/dist/tools/formatting.d.ts +20 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/registry.d.ts +26 -0
- package/dist/tools/validation.d.ts +27 -0
- package/dist/types.d.ts +826 -0
- package/package.json +54 -0
- package/src/agent-input.ts +116 -0
- package/src/agent.test.ts +931 -0
- package/src/agent.ts +1050 -0
- package/src/example.test.ts +564 -0
- package/src/extensions.ts +337 -0
- package/src/hooks/engine.test.ts +163 -0
- package/src/hooks/engine.ts +537 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/lifecycle.ts +239 -0
- package/src/hooks/node.ts +18 -0
- package/src/hooks/subprocess-runner.ts +140 -0
- package/src/hooks/subprocess.test.ts +180 -0
- package/src/hooks/subprocess.ts +620 -0
- package/src/index.browser.ts +1 -0
- package/src/index.node.ts +21 -0
- package/src/index.ts +133 -0
- package/src/mcp/index.ts +17 -0
- package/src/mcp/policies.test.ts +51 -0
- package/src/mcp/policies.ts +53 -0
- package/src/mcp/tools.test.ts +76 -0
- package/src/mcp/tools.ts +60 -0
- package/src/mcp/types.ts +41 -0
- package/src/message-builder.test.ts +175 -0
- package/src/message-builder.ts +429 -0
- package/src/prompts/cline.ts +49 -0
- package/src/prompts/index.ts +1 -0
- package/src/runtime/agent-runtime-bus.ts +53 -0
- package/src/runtime/conversation-store.ts +61 -0
- package/src/runtime/lifecycle-orchestrator.ts +90 -0
- package/src/runtime/tool-orchestrator.ts +177 -0
- package/src/runtime/turn-processor.ts +250 -0
- package/src/streaming.test.ts +197 -0
- package/src/streaming.ts +307 -0
- package/src/teams/index.ts +63 -0
- package/src/teams/multi-agent.lifecycle.test.ts +48 -0
- package/src/teams/multi-agent.ts +1866 -0
- package/src/teams/spawn-agent-tool.test.ts +172 -0
- package/src/teams/spawn-agent-tool.ts +223 -0
- package/src/teams/team-tools.test.ts +448 -0
- package/src/teams/team-tools.ts +929 -0
- package/src/tools/ask-question.ts +78 -0
- package/src/tools/create.ts +104 -0
- package/src/tools/execution.ts +311 -0
- package/src/tools/formatting.ts +73 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/registry.ts +52 -0
- package/src/tools/tools.test.ts +292 -0
- package/src/tools/validation.ts +73 -0
- package/src/types.ts +966 -0
package/src/streaming.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming Support
|
|
3
|
+
*
|
|
4
|
+
* Provides async iterable wrapper for streaming agent events in real-time.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Agent } from "./agent.js";
|
|
8
|
+
import type { AgentEvent, AgentResult } from "./types.js";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// AgentStream
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A stream of agent events that can be iterated asynchronously
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const stream = streamRun(agent, "Hello")
|
|
20
|
+
*
|
|
21
|
+
* for await (const event of stream) {
|
|
22
|
+
* if (event.type === "content_start" && event.contentType === "text") {
|
|
23
|
+
* process.stdout.write(event.text ?? "")
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* const result = await stream.getResult()
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export interface AgentStream extends AsyncIterable<AgentEvent> {
|
|
31
|
+
/**
|
|
32
|
+
* Get the final result after the stream completes
|
|
33
|
+
*
|
|
34
|
+
* This promise resolves when the agent finishes (successfully or with error).
|
|
35
|
+
* Call this after iterating through all events, or if you don't need events
|
|
36
|
+
* and just want the final result.
|
|
37
|
+
*/
|
|
38
|
+
getResult(): Promise<AgentResult>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Abort the stream
|
|
42
|
+
*
|
|
43
|
+
* This stops the agent execution and closes the stream.
|
|
44
|
+
*/
|
|
45
|
+
abort(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Stream Implementation
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Internal implementation of AgentStream
|
|
54
|
+
*/
|
|
55
|
+
class AgentStreamImpl implements AgentStream {
|
|
56
|
+
private eventQueue: AgentEvent[] = [];
|
|
57
|
+
private waitingResolvers: Array<(value: IteratorResult<AgentEvent>) => void> =
|
|
58
|
+
[];
|
|
59
|
+
private isDone = false;
|
|
60
|
+
private resultPromise: Promise<AgentResult>;
|
|
61
|
+
private resolveResult!: (result: AgentResult) => void;
|
|
62
|
+
private rejectResult!: (error: Error) => void;
|
|
63
|
+
private abortController: AbortController;
|
|
64
|
+
private unsubscribeEvents: (() => void) | null = null;
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
private agent: Agent,
|
|
68
|
+
private message: string,
|
|
69
|
+
private isContinue: boolean,
|
|
70
|
+
) {
|
|
71
|
+
this.abortController = new AbortController();
|
|
72
|
+
|
|
73
|
+
// Create result promise
|
|
74
|
+
this.resultPromise = new Promise((resolve, reject) => {
|
|
75
|
+
this.resolveResult = resolve;
|
|
76
|
+
this.rejectResult = reject;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Start the run
|
|
80
|
+
this.startRun();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async startRun(): Promise<void> {
|
|
84
|
+
this.unsubscribeEvents = this.agent.subscribeEvents((event: AgentEvent) => {
|
|
85
|
+
this.enqueueEvent(event);
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
// Run or continue
|
|
89
|
+
const result = this.isContinue
|
|
90
|
+
? await this.agent.continue(this.message)
|
|
91
|
+
: await this.agent.run(this.message);
|
|
92
|
+
|
|
93
|
+
// Mark as done
|
|
94
|
+
this.isDone = true;
|
|
95
|
+
this.flushWaiters();
|
|
96
|
+
this.resolveResult(result);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
99
|
+
this.isDone = true;
|
|
100
|
+
this.flushWaiters();
|
|
101
|
+
this.rejectResult(err);
|
|
102
|
+
} finally {
|
|
103
|
+
this.unsubscribeEvents?.();
|
|
104
|
+
this.unsubscribeEvents = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private enqueueEvent(event: AgentEvent): void {
|
|
109
|
+
if (this.waitingResolvers.length > 0) {
|
|
110
|
+
// Someone is waiting - resolve immediately
|
|
111
|
+
const resolve = this.waitingResolvers.shift()!;
|
|
112
|
+
resolve({ value: event, done: false });
|
|
113
|
+
} else {
|
|
114
|
+
// Queue the event
|
|
115
|
+
this.eventQueue.push(event);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private flushWaiters(): void {
|
|
120
|
+
// Resolve any remaining waiters with done
|
|
121
|
+
while (this.waitingResolvers.length > 0) {
|
|
122
|
+
const resolve = this.waitingResolvers.shift()!;
|
|
123
|
+
resolve({ value: undefined as unknown as AgentEvent, done: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[Symbol.asyncIterator](): AsyncIterator<AgentEvent> {
|
|
128
|
+
return {
|
|
129
|
+
next: async (): Promise<IteratorResult<AgentEvent>> => {
|
|
130
|
+
// Check if we have queued events
|
|
131
|
+
if (this.eventQueue.length > 0) {
|
|
132
|
+
return { value: this.eventQueue.shift()!, done: false };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if we're done
|
|
136
|
+
if (this.isDone) {
|
|
137
|
+
return { value: undefined as unknown as AgentEvent, done: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Wait for next event
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
this.waitingResolvers.push(resolve);
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getResult(): Promise<AgentResult> {
|
|
149
|
+
return this.resultPromise;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
abort(): void {
|
|
153
|
+
this.abortController.abort();
|
|
154
|
+
this.agent.abort();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// =============================================================================
|
|
159
|
+
// Factory Functions
|
|
160
|
+
// =============================================================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a streaming agent run
|
|
164
|
+
*
|
|
165
|
+
* This returns an AgentStream that can be iterated to receive events
|
|
166
|
+
* as they happen, with the final result available via getResult().
|
|
167
|
+
*
|
|
168
|
+
* @param agent - The agent to run
|
|
169
|
+
* @param message - The user message to send
|
|
170
|
+
* @returns An AgentStream for real-time event consumption
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* const stream = streamRun(agent, "Analyze this code")
|
|
175
|
+
*
|
|
176
|
+
* for await (const event of stream) {
|
|
177
|
+
* switch (event.type) {
|
|
178
|
+
* case "content_start":
|
|
179
|
+
* if (event.contentType === "text") {
|
|
180
|
+
* process.stdout.write(event.text ?? "")
|
|
181
|
+
* }
|
|
182
|
+
* break
|
|
183
|
+
* case "content_end":
|
|
184
|
+
* if (event.contentType === "tool") {
|
|
185
|
+
* console.log(`Done (${event.durationMs}ms)`)
|
|
186
|
+
* }
|
|
187
|
+
* break
|
|
188
|
+
* }
|
|
189
|
+
* }
|
|
190
|
+
*
|
|
191
|
+
* const result = await stream.getResult()
|
|
192
|
+
* console.log("\nTotal cost:", result.usage.totalCost)
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export function streamRun(agent: Agent, message: string): AgentStream {
|
|
196
|
+
return new AgentStreamImpl(agent, message, false);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create a streaming agent continuation
|
|
201
|
+
*
|
|
202
|
+
* Like streamRun, but continues an existing conversation.
|
|
203
|
+
*
|
|
204
|
+
* @param agent - The agent to continue
|
|
205
|
+
* @param message - The user message to add
|
|
206
|
+
* @returns An AgentStream for real-time event consumption
|
|
207
|
+
*/
|
|
208
|
+
export function streamContinue(agent: Agent, message: string): AgentStream {
|
|
209
|
+
return new AgentStreamImpl(agent, message, true);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// =============================================================================
|
|
213
|
+
// Utility Functions
|
|
214
|
+
// =============================================================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Collect all events from a stream into an array
|
|
218
|
+
*
|
|
219
|
+
* Useful for testing or when you need all events at once.
|
|
220
|
+
*/
|
|
221
|
+
export async function collectEvents(
|
|
222
|
+
stream: AgentStream,
|
|
223
|
+
): Promise<AgentEvent[]> {
|
|
224
|
+
const events: AgentEvent[] = [];
|
|
225
|
+
for await (const event of stream) {
|
|
226
|
+
events.push(event);
|
|
227
|
+
}
|
|
228
|
+
return events;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Filter events by type
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* for await (const event of filterEvents(stream, "text")) {
|
|
237
|
+
* console.log(event.text)
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
export async function* filterEvents<T extends AgentEvent["type"]>(
|
|
242
|
+
stream: AgentStream,
|
|
243
|
+
type: T,
|
|
244
|
+
): AsyncGenerator<Extract<AgentEvent, { type: T }>> {
|
|
245
|
+
for await (const event of stream) {
|
|
246
|
+
if (event.type === type) {
|
|
247
|
+
yield event as Extract<AgentEvent, { type: T }>;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Map events to a different format
|
|
254
|
+
*/
|
|
255
|
+
export async function* mapEvents<T>(
|
|
256
|
+
stream: AgentStream,
|
|
257
|
+
mapper: (event: AgentEvent) => T,
|
|
258
|
+
): AsyncGenerator<T> {
|
|
259
|
+
for await (const event of stream) {
|
|
260
|
+
yield mapper(event);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Buffer events and yield in batches
|
|
266
|
+
*/
|
|
267
|
+
export async function* batchEvents(
|
|
268
|
+
stream: AgentStream,
|
|
269
|
+
batchSize: number,
|
|
270
|
+
): AsyncGenerator<AgentEvent[]> {
|
|
271
|
+
let batch: AgentEvent[] = [];
|
|
272
|
+
|
|
273
|
+
for await (const event of stream) {
|
|
274
|
+
batch.push(event);
|
|
275
|
+
if (batch.length >= batchSize) {
|
|
276
|
+
yield batch;
|
|
277
|
+
batch = [];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (batch.length > 0) {
|
|
282
|
+
yield batch;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create a simple text accumulator that yields text as it streams
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```typescript
|
|
291
|
+
* for await (const text of streamText(agent, "Hello")) {
|
|
292
|
+
* process.stdout.write(text)
|
|
293
|
+
* }
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
export async function* streamText(
|
|
297
|
+
agent: Agent,
|
|
298
|
+
message: string,
|
|
299
|
+
): AsyncGenerator<string> {
|
|
300
|
+
const stream = streamRun(agent, message);
|
|
301
|
+
|
|
302
|
+
for await (const event of stream) {
|
|
303
|
+
if (event.type === "content_start" && event.contentType === "text") {
|
|
304
|
+
yield event.text ?? "";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Spawn Agent Tool
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createSpawnAgentTool,
|
|
7
|
+
type SpawnAgentInput,
|
|
8
|
+
type SpawnAgentOutput,
|
|
9
|
+
type SpawnAgentToolConfig,
|
|
10
|
+
type SubAgentEndContext,
|
|
11
|
+
type SubAgentStartContext,
|
|
12
|
+
} from "./spawn-agent-tool.js";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Multi-Agent
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
type AgentTask,
|
|
20
|
+
AgentTeam,
|
|
21
|
+
AgentTeamsRuntime,
|
|
22
|
+
type AgentTeamsRuntimeOptions,
|
|
23
|
+
type AppendMissionLogInput,
|
|
24
|
+
type AttachTeamOutcomeFragmentInput,
|
|
25
|
+
type CreateTeamOutcomeInput,
|
|
26
|
+
type CreateTeamTaskInput,
|
|
27
|
+
createAgentTeam,
|
|
28
|
+
createWorkerReviewerTeam,
|
|
29
|
+
type MissionLogEntry,
|
|
30
|
+
type MissionLogKind,
|
|
31
|
+
type ReviewTeamOutcomeFragmentInput,
|
|
32
|
+
type RouteToTeammateOptions,
|
|
33
|
+
type SpawnTeammateOptions,
|
|
34
|
+
type TaskResult,
|
|
35
|
+
type TeamEvent,
|
|
36
|
+
type TeamMailboxMessage,
|
|
37
|
+
type TeamMemberConfig,
|
|
38
|
+
type TeamMemberSnapshot,
|
|
39
|
+
TeamMessageType,
|
|
40
|
+
type TeammateLifecycleSpec,
|
|
41
|
+
type TeamOutcome,
|
|
42
|
+
type TeamOutcomeFragment,
|
|
43
|
+
type TeamOutcomeFragmentStatus,
|
|
44
|
+
type TeamOutcomeStatus,
|
|
45
|
+
type TeamRunRecord,
|
|
46
|
+
type TeamRunStatus,
|
|
47
|
+
type TeamRuntimeSnapshot,
|
|
48
|
+
type TeamRuntimeState,
|
|
49
|
+
type TeamTask,
|
|
50
|
+
type TeamTaskStatus,
|
|
51
|
+
} from "./multi-agent.js";
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
type BootstrapAgentTeamsOptions,
|
|
55
|
+
type BootstrapAgentTeamsResult,
|
|
56
|
+
bootstrapAgentTeams,
|
|
57
|
+
type CreateAgentTeamsToolsOptions,
|
|
58
|
+
createAgentTeamsTools,
|
|
59
|
+
reviveTeamStateDates,
|
|
60
|
+
sanitizeTeamName,
|
|
61
|
+
type TeamTeammateRuntimeConfig,
|
|
62
|
+
type TeamTeammateSpec,
|
|
63
|
+
} from "./team-tools.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
AgentTeamsRuntime,
|
|
4
|
+
type TeamEvent,
|
|
5
|
+
TeamMessageType,
|
|
6
|
+
} from "./multi-agent";
|
|
7
|
+
|
|
8
|
+
vi.mock("../agent.js", () => ({
|
|
9
|
+
createAgent: vi.fn(() => ({
|
|
10
|
+
abort: vi.fn(),
|
|
11
|
+
run: vi.fn(),
|
|
12
|
+
continue: vi.fn(),
|
|
13
|
+
getAgentId: vi.fn(() => "teammate-1"),
|
|
14
|
+
getConversationId: vi.fn(() => "conv-1"),
|
|
15
|
+
})),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe("AgentTeamsRuntime teammate lifecycle events", () => {
|
|
19
|
+
it("emits teammate_spawned with lifecycle payload", () => {
|
|
20
|
+
const events: TeamEvent[] = [];
|
|
21
|
+
const runtime = new AgentTeamsRuntime({
|
|
22
|
+
teamName: "test-team",
|
|
23
|
+
onTeamEvent: (event) => events.push(event),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
runtime.spawnTeammate({
|
|
27
|
+
agentId: "python-poet",
|
|
28
|
+
config: {
|
|
29
|
+
providerId: "anthropic",
|
|
30
|
+
modelId: "claude-sonnet-4-5-20250929",
|
|
31
|
+
systemPrompt: "Write concise Python-focused haiku",
|
|
32
|
+
maxIterations: 7,
|
|
33
|
+
tools: [],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(events).toContainEqual({
|
|
38
|
+
type: TeamMessageType.TeammateSpawned,
|
|
39
|
+
agentId: "python-poet",
|
|
40
|
+
role: undefined,
|
|
41
|
+
teammate: {
|
|
42
|
+
rolePrompt: "Write concise Python-focused haiku",
|
|
43
|
+
modelId: "claude-sonnet-4-5-20250929",
|
|
44
|
+
maxIterations: 7,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|