@statelyai/agent 0.1.4 → 1.0.0-beta.1
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/index.d.mts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +12 -9
- package/dist/index.mjs +12 -9
- package/examples/ticTacToe.ts +0 -1
- package/examples/word.ts +5 -2
- package/package.json +13 -7
- package/src/agent.ts +3 -3
- package/src/planners/simplePlanner.ts +1 -1
- package/src/text.ts +4 -4
- package/src/types.ts +20 -3
- package/src/utils.ts +6 -0
package/dist/index.d.mts
CHANGED
|
@@ -4,14 +4,28 @@ import { generateText, streamText, LanguageModel, CoreMessage, GenerateTextResul
|
|
|
4
4
|
|
|
5
5
|
type GenerateTextOptions = Parameters<typeof generateText>[0];
|
|
6
6
|
type StreamTextOptions = Parameters<typeof streamText>[0];
|
|
7
|
-
type AgentPlanInput<TEvent extends EventObject> = {
|
|
8
|
-
|
|
7
|
+
type AgentPlanInput<TEvent extends EventObject> = Omit<GenerateTextOptions, 'prompt' | 'messages' | 'tools'> & {
|
|
8
|
+
/**
|
|
9
|
+
* The currently observed state.
|
|
10
|
+
*/
|
|
9
11
|
state: ObservedState;
|
|
12
|
+
/**
|
|
13
|
+
* The goal for the agent to accomplish.
|
|
14
|
+
* The agent will create a plan based on this goal.
|
|
15
|
+
*/
|
|
10
16
|
goal: string;
|
|
17
|
+
/**
|
|
18
|
+
* The events that the agent can trigger. This is a mapping of
|
|
19
|
+
* event types to Zod event schemas.
|
|
20
|
+
*/
|
|
11
21
|
events: ZodEventMapping;
|
|
22
|
+
/**
|
|
23
|
+
* The state machine that represents the environment the agent
|
|
24
|
+
* is interacting with.
|
|
25
|
+
*/
|
|
12
26
|
machine?: AnyStateMachine;
|
|
13
27
|
/**
|
|
14
|
-
* The previous plan
|
|
28
|
+
* The previous plan.
|
|
15
29
|
*/
|
|
16
30
|
previousPlan?: AgentPlan<TEvent>;
|
|
17
31
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,14 +4,28 @@ import { generateText, streamText, LanguageModel, CoreMessage, GenerateTextResul
|
|
|
4
4
|
|
|
5
5
|
type GenerateTextOptions = Parameters<typeof generateText>[0];
|
|
6
6
|
type StreamTextOptions = Parameters<typeof streamText>[0];
|
|
7
|
-
type AgentPlanInput<TEvent extends EventObject> = {
|
|
8
|
-
|
|
7
|
+
type AgentPlanInput<TEvent extends EventObject> = Omit<GenerateTextOptions, 'prompt' | 'messages' | 'tools'> & {
|
|
8
|
+
/**
|
|
9
|
+
* The currently observed state.
|
|
10
|
+
*/
|
|
9
11
|
state: ObservedState;
|
|
12
|
+
/**
|
|
13
|
+
* The goal for the agent to accomplish.
|
|
14
|
+
* The agent will create a plan based on this goal.
|
|
15
|
+
*/
|
|
10
16
|
goal: string;
|
|
17
|
+
/**
|
|
18
|
+
* The events that the agent can trigger. This is a mapping of
|
|
19
|
+
* event types to Zod event schemas.
|
|
20
|
+
*/
|
|
11
21
|
events: ZodEventMapping;
|
|
22
|
+
/**
|
|
23
|
+
* The state machine that represents the environment the agent
|
|
24
|
+
* is interacting with.
|
|
25
|
+
*/
|
|
12
26
|
machine?: AnyStateMachine;
|
|
13
27
|
/**
|
|
14
|
-
* The previous plan
|
|
28
|
+
* The previous plan.
|
|
15
29
|
*/
|
|
16
30
|
previousPlan?: AgentPlan<TEvent>;
|
|
17
31
|
};
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,11 @@ function getAllTransitions(state) {
|
|
|
48
48
|
function wrapInXml(tagName, content) {
|
|
49
49
|
return `<${tagName}>${content}</${tagName}>`;
|
|
50
50
|
}
|
|
51
|
+
function randomId() {
|
|
52
|
+
const timestamp = Date.now().toString(36);
|
|
53
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
54
|
+
return timestamp + random;
|
|
55
|
+
}
|
|
51
56
|
|
|
52
57
|
// src/templates/defaultText.ts
|
|
53
58
|
var defaultTextTemplate = (data) => {
|
|
@@ -118,10 +123,10 @@ async function simplePlanner(agent, input) {
|
|
|
118
123
|
goal: input.goal
|
|
119
124
|
});
|
|
120
125
|
const result = await agent.generateText({
|
|
126
|
+
...input,
|
|
121
127
|
prompt,
|
|
122
128
|
tools: toolMap,
|
|
123
|
-
toolChoice: "required"
|
|
124
|
-
...input
|
|
129
|
+
toolChoice: "required"
|
|
125
130
|
});
|
|
126
131
|
const singleResult = result.toolResults[0];
|
|
127
132
|
if (!singleResult) {
|
|
@@ -144,7 +149,6 @@ async function simplePlanner(agent, input) {
|
|
|
144
149
|
|
|
145
150
|
// src/text.ts
|
|
146
151
|
var import_xstate = require("xstate");
|
|
147
|
-
var import_nanoid = require("nanoid");
|
|
148
152
|
async function getMessages(agent, prompt, options) {
|
|
149
153
|
let messages = [];
|
|
150
154
|
if (options.messages === true) {
|
|
@@ -166,7 +170,7 @@ async function agentGenerateText(agent, options) {
|
|
|
166
170
|
...options
|
|
167
171
|
};
|
|
168
172
|
const template = resolvedOptions.template ?? defaultTextTemplate;
|
|
169
|
-
const id = (
|
|
173
|
+
const id = randomId();
|
|
170
174
|
const goal = typeof resolvedOptions.prompt === "string" ? resolvedOptions.prompt : await resolvedOptions.prompt(agent);
|
|
171
175
|
const promptWithContext = template({
|
|
172
176
|
goal,
|
|
@@ -200,7 +204,7 @@ async function agentStreamText(agent, options) {
|
|
|
200
204
|
...options
|
|
201
205
|
};
|
|
202
206
|
const template = resolvedOptions.template ?? defaultTextTemplate;
|
|
203
|
-
const id = (
|
|
207
|
+
const id = randomId();
|
|
204
208
|
const goal = typeof resolvedOptions.prompt === "string" ? resolvedOptions.prompt : await resolvedOptions.prompt(agent);
|
|
205
209
|
const promptWithContext = template({
|
|
206
210
|
goal,
|
|
@@ -232,7 +236,7 @@ async function agentStreamText(agent, options) {
|
|
|
232
236
|
rawResponse: res.rawResponse
|
|
233
237
|
},
|
|
234
238
|
content: res.text,
|
|
235
|
-
id: (
|
|
239
|
+
id: randomId(),
|
|
236
240
|
timestamp: Date.now(),
|
|
237
241
|
responseId: id
|
|
238
242
|
});
|
|
@@ -352,7 +356,6 @@ var vercelAdapter = {
|
|
|
352
356
|
};
|
|
353
357
|
|
|
354
358
|
// src/agent.ts
|
|
355
|
-
var import_nanoid2 = require("nanoid");
|
|
356
359
|
var agentLogic = (0, import_xstate3.fromTransition)(
|
|
357
360
|
(state, event, { emit }) => {
|
|
358
361
|
switch (event.type) {
|
|
@@ -437,7 +440,7 @@ function createAgent({
|
|
|
437
440
|
agent.addMessage = (messageInput) => {
|
|
438
441
|
const message = {
|
|
439
442
|
...messageInput,
|
|
440
|
-
id: messageInput.id ?? (
|
|
443
|
+
id: messageInput.id ?? randomId(),
|
|
441
444
|
timestamp: messageInput.timestamp ?? Date.now(),
|
|
442
445
|
sessionId: agent.sessionId
|
|
443
446
|
};
|
|
@@ -464,7 +467,7 @@ function createAgent({
|
|
|
464
467
|
agent.addObservation = (observationInput) => {
|
|
465
468
|
const observation = {
|
|
466
469
|
...observationInput,
|
|
467
|
-
id: observationInput.id ?? (
|
|
470
|
+
id: observationInput.id ?? randomId(),
|
|
468
471
|
sessionId: agent.sessionId,
|
|
469
472
|
timestamp: observationInput.timestamp ?? Date.now()
|
|
470
473
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -21,6 +21,11 @@ function getAllTransitions(state) {
|
|
|
21
21
|
function wrapInXml(tagName, content) {
|
|
22
22
|
return `<${tagName}>${content}</${tagName}>`;
|
|
23
23
|
}
|
|
24
|
+
function randomId() {
|
|
25
|
+
const timestamp = Date.now().toString(36);
|
|
26
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
27
|
+
return timestamp + random;
|
|
28
|
+
}
|
|
24
29
|
|
|
25
30
|
// src/templates/defaultText.ts
|
|
26
31
|
var defaultTextTemplate = (data) => {
|
|
@@ -91,10 +96,10 @@ async function simplePlanner(agent, input) {
|
|
|
91
96
|
goal: input.goal
|
|
92
97
|
});
|
|
93
98
|
const result = await agent.generateText({
|
|
99
|
+
...input,
|
|
94
100
|
prompt,
|
|
95
101
|
tools: toolMap,
|
|
96
|
-
toolChoice: "required"
|
|
97
|
-
...input
|
|
102
|
+
toolChoice: "required"
|
|
98
103
|
});
|
|
99
104
|
const singleResult = result.toolResults[0];
|
|
100
105
|
if (!singleResult) {
|
|
@@ -121,7 +126,6 @@ import {
|
|
|
121
126
|
fromPromise,
|
|
122
127
|
toObserver
|
|
123
128
|
} from "xstate";
|
|
124
|
-
import { nanoid } from "nanoid";
|
|
125
129
|
async function getMessages(agent, prompt, options) {
|
|
126
130
|
let messages = [];
|
|
127
131
|
if (options.messages === true) {
|
|
@@ -143,7 +147,7 @@ async function agentGenerateText(agent, options) {
|
|
|
143
147
|
...options
|
|
144
148
|
};
|
|
145
149
|
const template = resolvedOptions.template ?? defaultTextTemplate;
|
|
146
|
-
const id =
|
|
150
|
+
const id = randomId();
|
|
147
151
|
const goal = typeof resolvedOptions.prompt === "string" ? resolvedOptions.prompt : await resolvedOptions.prompt(agent);
|
|
148
152
|
const promptWithContext = template({
|
|
149
153
|
goal,
|
|
@@ -177,7 +181,7 @@ async function agentStreamText(agent, options) {
|
|
|
177
181
|
...options
|
|
178
182
|
};
|
|
179
183
|
const template = resolvedOptions.template ?? defaultTextTemplate;
|
|
180
|
-
const id =
|
|
184
|
+
const id = randomId();
|
|
181
185
|
const goal = typeof resolvedOptions.prompt === "string" ? resolvedOptions.prompt : await resolvedOptions.prompt(agent);
|
|
182
186
|
const promptWithContext = template({
|
|
183
187
|
goal,
|
|
@@ -209,7 +213,7 @@ async function agentStreamText(agent, options) {
|
|
|
209
213
|
rawResponse: res.rawResponse
|
|
210
214
|
},
|
|
211
215
|
content: res.text,
|
|
212
|
-
id:
|
|
216
|
+
id: randomId(),
|
|
213
217
|
timestamp: Date.now(),
|
|
214
218
|
responseId: id
|
|
215
219
|
});
|
|
@@ -329,7 +333,6 @@ var vercelAdapter = {
|
|
|
329
333
|
};
|
|
330
334
|
|
|
331
335
|
// src/agent.ts
|
|
332
|
-
import { nanoid as nanoid2 } from "nanoid";
|
|
333
336
|
var agentLogic = fromTransition(
|
|
334
337
|
(state, event, { emit }) => {
|
|
335
338
|
switch (event.type) {
|
|
@@ -414,7 +417,7 @@ function createAgent({
|
|
|
414
417
|
agent.addMessage = (messageInput) => {
|
|
415
418
|
const message = {
|
|
416
419
|
...messageInput,
|
|
417
|
-
id: messageInput.id ??
|
|
420
|
+
id: messageInput.id ?? randomId(),
|
|
418
421
|
timestamp: messageInput.timestamp ?? Date.now(),
|
|
419
422
|
sessionId: agent.sessionId
|
|
420
423
|
};
|
|
@@ -441,7 +444,7 @@ function createAgent({
|
|
|
441
444
|
agent.addObservation = (observationInput) => {
|
|
442
445
|
const observation = {
|
|
443
446
|
...observationInput,
|
|
444
|
-
id: observationInput.id ??
|
|
447
|
+
id: observationInput.id ?? randomId(),
|
|
445
448
|
sessionId: agent.sessionId,
|
|
446
449
|
timestamp: observationInput.timestamp ?? Date.now()
|
|
447
450
|
};
|
package/examples/ticTacToe.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { assign, setup, assertEvent, createActor } from 'xstate';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { createAgent, fromDecision, fromTextStream } from '../src';
|
|
4
4
|
import { openai } from '@ai-sdk/openai';
|
|
5
|
-
import { defaultToolCallTemplate } from '../src/templates/defaultToolCall';
|
|
6
5
|
|
|
7
6
|
const agent = createAgent({
|
|
8
7
|
name: 'tic-tac-toe-bot',
|
package/examples/word.ts
CHANGED
|
@@ -12,7 +12,7 @@ const context = {
|
|
|
12
12
|
|
|
13
13
|
const agent = createAgent({
|
|
14
14
|
name: 'word',
|
|
15
|
-
model: openai('gpt-
|
|
15
|
+
model: openai('gpt-4o'),
|
|
16
16
|
events: {
|
|
17
17
|
'agent.guessLetter': z.object({
|
|
18
18
|
letter: z.string().min(1).max(1).describe('The letter guessed'),
|
|
@@ -42,12 +42,15 @@ const wordGuesserMachine = setup({
|
|
|
42
42
|
agent: fromDecision(agent),
|
|
43
43
|
getFromTerminal,
|
|
44
44
|
},
|
|
45
|
+
actions: {
|
|
46
|
+
resetContext: assign(context),
|
|
47
|
+
},
|
|
45
48
|
}).createMachine({
|
|
46
49
|
initial: 'providingWord',
|
|
47
50
|
context,
|
|
48
51
|
states: {
|
|
49
52
|
providingWord: {
|
|
50
|
-
entry:
|
|
53
|
+
entry: 'resetContext',
|
|
51
54
|
invoke: {
|
|
52
55
|
src: 'getFromTerminal',
|
|
53
56
|
input: 'Enter a word, and an agent will try to guess it.',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statelyai/agent",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "Stateful agents that make decisions based on finite-state machine models",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -9,13 +9,20 @@
|
|
|
9
9
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
10
10
|
"lint": "tsc --noEmit",
|
|
11
11
|
"test": "vitest",
|
|
12
|
+
"test:ci": "vitest --run",
|
|
12
13
|
"example": "ts-node examples/helpers/runner.ts",
|
|
13
14
|
"prepublishOnly": "tsup src/index.ts --format cjs,esm --dts",
|
|
14
15
|
"changeset": "changeset",
|
|
15
16
|
"release": "changeset publish",
|
|
16
17
|
"version": "changeset version"
|
|
17
18
|
},
|
|
18
|
-
"keywords": [
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"state machine",
|
|
22
|
+
"agent",
|
|
23
|
+
"rl",
|
|
24
|
+
"reinforcement learning"
|
|
25
|
+
],
|
|
19
26
|
"author": "",
|
|
20
27
|
"license": "MIT",
|
|
21
28
|
"devDependencies": {
|
|
@@ -24,7 +31,7 @@
|
|
|
24
31
|
"@langchain/community": "^0.0.53",
|
|
25
32
|
"@langchain/core": "^0.1.63",
|
|
26
33
|
"@langchain/openai": "^0.0.28",
|
|
27
|
-
"@types/node": "^20.14.
|
|
34
|
+
"@types/node": "^20.14.6",
|
|
28
35
|
"dotenv": "^16.4.5",
|
|
29
36
|
"json-schema-to-ts": "^3.1.0",
|
|
30
37
|
"ts-node": "^10.9.2",
|
|
@@ -38,10 +45,9 @@
|
|
|
38
45
|
"access": "public"
|
|
39
46
|
},
|
|
40
47
|
"dependencies": {
|
|
41
|
-
"@ai-sdk/openai": "^0.0.
|
|
48
|
+
"@ai-sdk/openai": "^0.0.31",
|
|
42
49
|
"@xstate/graph": "^2.0.0",
|
|
43
|
-
"ai": "^3.1
|
|
44
|
-
"nanoid": "^5.0.7",
|
|
50
|
+
"ai": "^3.2.1",
|
|
45
51
|
"xstate": "^5.13.2"
|
|
46
52
|
},
|
|
47
53
|
"packageManager": "pnpm@8.11.0"
|
package/src/agent.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { simplePlanner } from './planners/simplePlanner';
|
|
|
25
25
|
import { agentGenerateText, agentStreamText } from './text';
|
|
26
26
|
import { agentDecide } from './decision';
|
|
27
27
|
import { vercelAdapter } from './adapters/vercel';
|
|
28
|
-
import {
|
|
28
|
+
import { randomId } from './utils';
|
|
29
29
|
|
|
30
30
|
export const agentLogic: AgentLogic<AnyEventObject> = fromTransition(
|
|
31
31
|
(state, event, { emit }) => {
|
|
@@ -144,7 +144,7 @@ export function createAgent<
|
|
|
144
144
|
agent.addMessage = (messageInput) => {
|
|
145
145
|
const message = {
|
|
146
146
|
...messageInput,
|
|
147
|
-
id: messageInput.id ??
|
|
147
|
+
id: messageInput.id ?? randomId(),
|
|
148
148
|
timestamp: messageInput.timestamp ?? Date.now(),
|
|
149
149
|
sessionId: agent.sessionId,
|
|
150
150
|
};
|
|
@@ -176,7 +176,7 @@ export function createAgent<
|
|
|
176
176
|
agent.addObservation = (observationInput) => {
|
|
177
177
|
const observation = {
|
|
178
178
|
...observationInput,
|
|
179
|
-
id: observationInput.id ??
|
|
179
|
+
id: observationInput.id ?? randomId(),
|
|
180
180
|
sessionId: agent.sessionId,
|
|
181
181
|
timestamp: observationInput.timestamp ?? Date.now(),
|
|
182
182
|
};
|
|
@@ -100,10 +100,10 @@ export async function simplePlanner<T extends Agent<any>>(
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
const result = await agent.generateText({
|
|
103
|
+
...input,
|
|
103
104
|
prompt,
|
|
104
105
|
tools: toolMap,
|
|
105
106
|
toolChoice: 'required',
|
|
106
|
-
...input,
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
const singleResult = result.toolResults[0];
|
package/src/text.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
fromPromise,
|
|
20
20
|
toObserver,
|
|
21
21
|
} from 'xstate';
|
|
22
|
-
import {
|
|
22
|
+
import { randomId } from './utils';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Gets an array of messages from the given prompt, based on the agent and options.
|
|
@@ -61,7 +61,7 @@ export async function agentGenerateText<T extends Agent<any>>(
|
|
|
61
61
|
};
|
|
62
62
|
const template = resolvedOptions.template ?? defaultTextTemplate;
|
|
63
63
|
// TODO: check if messages was provided instead
|
|
64
|
-
const id =
|
|
64
|
+
const id = randomId();
|
|
65
65
|
const goal =
|
|
66
66
|
typeof resolvedOptions.prompt === 'string'
|
|
67
67
|
? resolvedOptions.prompt
|
|
@@ -109,7 +109,7 @@ export async function agentStreamText(
|
|
|
109
109
|
};
|
|
110
110
|
const template = resolvedOptions.template ?? defaultTextTemplate;
|
|
111
111
|
|
|
112
|
-
const id =
|
|
112
|
+
const id = randomId();
|
|
113
113
|
const goal =
|
|
114
114
|
typeof resolvedOptions.prompt === 'string'
|
|
115
115
|
? resolvedOptions.prompt
|
|
@@ -148,7 +148,7 @@ export async function agentStreamText(
|
|
|
148
148
|
rawResponse: res.rawResponse,
|
|
149
149
|
},
|
|
150
150
|
content: res.text,
|
|
151
|
-
id:
|
|
151
|
+
id: randomId(),
|
|
152
152
|
timestamp: Date.now(),
|
|
153
153
|
responseId: id,
|
|
154
154
|
});
|
package/src/types.ts
CHANGED
|
@@ -29,14 +29,31 @@ export type GenerateTextOptions = Parameters<typeof generateText>[0];
|
|
|
29
29
|
|
|
30
30
|
export type StreamTextOptions = Parameters<typeof streamText>[0];
|
|
31
31
|
|
|
32
|
-
export type AgentPlanInput<TEvent extends EventObject> =
|
|
33
|
-
|
|
32
|
+
export type AgentPlanInput<TEvent extends EventObject> = Omit<
|
|
33
|
+
GenerateTextOptions,
|
|
34
|
+
'prompt' | 'messages' | 'tools'
|
|
35
|
+
> & {
|
|
36
|
+
/**
|
|
37
|
+
* The currently observed state.
|
|
38
|
+
*/
|
|
34
39
|
state: ObservedState;
|
|
40
|
+
/**
|
|
41
|
+
* The goal for the agent to accomplish.
|
|
42
|
+
* The agent will create a plan based on this goal.
|
|
43
|
+
*/
|
|
35
44
|
goal: string;
|
|
45
|
+
/**
|
|
46
|
+
* The events that the agent can trigger. This is a mapping of
|
|
47
|
+
* event types to Zod event schemas.
|
|
48
|
+
*/
|
|
36
49
|
events: ZodEventMapping;
|
|
50
|
+
/**
|
|
51
|
+
* The state machine that represents the environment the agent
|
|
52
|
+
* is interacting with.
|
|
53
|
+
*/
|
|
37
54
|
machine?: AnyStateMachine;
|
|
38
55
|
/**
|
|
39
|
-
* The previous plan
|
|
56
|
+
* The previous plan.
|
|
40
57
|
*/
|
|
41
58
|
previousPlan?: AgentPlan<TEvent>;
|
|
42
59
|
};
|
package/src/utils.ts
CHANGED
|
@@ -20,3 +20,9 @@ export function getAllTransitions(state: AnyMachineSnapshot): TransitionData[] {
|
|
|
20
20
|
export function wrapInXml(tagName: string, content: string): string {
|
|
21
21
|
return `<${tagName}>${content}</${tagName}>`;
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
export function randomId() {
|
|
25
|
+
const timestamp = Date.now().toString(36);
|
|
26
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
27
|
+
return timestamp + random;
|
|
28
|
+
}
|