@barnum/barnum 0.0.0-main-5c9fe8f2 → 0.0.0-main-5a6a8bee
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/artifacts/linux-arm64/barnum +0 -0
- package/artifacts/linux-x64/barnum +0 -0
- package/artifacts/macos-arm64/barnum +0 -0
- package/artifacts/macos-x64/barnum +0 -0
- package/artifacts/win-x64/barnum.exe +0 -0
- package/barnum-config-schema.json +31 -63
- package/barnum-config-schema.zod.ts +25 -36
- package/package.json +1 -1
- package/run.ts +9 -8
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"title": "
|
|
4
|
-
"description": "Top-level Barnum configuration
|
|
3
|
+
"title": "Config",
|
|
4
|
+
"description": "Top-level Barnum configuration.\n\nDefines a workflow as a directed graph of steps. Each step processes tasks and can spawn follow-up tasks on other steps.",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": [
|
|
7
7
|
"steps"
|
|
8
8
|
],
|
|
9
9
|
"properties": {
|
|
10
|
-
"$schema": {
|
|
11
|
-
"description": "Optional JSON Schema URL for editor validation (e.g., `\"./node_modules/@barnum/barnum/barnum-config-schema.json\"`). Ignored at runtime.",
|
|
12
|
-
"writeOnly": true,
|
|
13
|
-
"type": [
|
|
14
|
-
"string",
|
|
15
|
-
"null"
|
|
16
|
-
]
|
|
17
|
-
},
|
|
18
10
|
"entrypoint": {
|
|
19
11
|
"description": "Name of the step that starts the workflow. When set, the CLI accepts `--entrypoint-value` to provide the initial task value (defaults to `{}`). When omitted, `--initial-state` must provide explicit `[{\"kind\": \"StepName\", \"value\": ...}]` tasks.",
|
|
20
12
|
"default": null,
|
|
@@ -26,10 +18,10 @@
|
|
|
26
18
|
"options": {
|
|
27
19
|
"description": "Global runtime options (timeout, retries, concurrency). Individual steps can override these via their own `options` field.",
|
|
28
20
|
"default": {
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
21
|
+
"maxConcurrency": null,
|
|
22
|
+
"maxRetries": 0,
|
|
23
|
+
"retryOnInvalidResponse": true,
|
|
24
|
+
"retryOnTimeout": true,
|
|
33
25
|
"timeout": null
|
|
34
26
|
},
|
|
35
27
|
"allOf": [
|
|
@@ -42,13 +34,13 @@
|
|
|
42
34
|
"description": "The steps that make up this workflow. Each step defines how to process a task and which steps it can spawn follow-up tasks on.",
|
|
43
35
|
"type": "array",
|
|
44
36
|
"items": {
|
|
45
|
-
"$ref": "#/definitions/
|
|
37
|
+
"$ref": "#/definitions/Step"
|
|
46
38
|
}
|
|
47
39
|
}
|
|
48
40
|
},
|
|
49
41
|
"additionalProperties": false,
|
|
50
42
|
"definitions": {
|
|
51
|
-
"
|
|
43
|
+
"ActionKind": {
|
|
52
44
|
"description": "How a step processes tasks.",
|
|
53
45
|
"oneOf": [
|
|
54
46
|
{
|
|
@@ -56,77 +48,53 @@
|
|
|
56
48
|
"type": "object",
|
|
57
49
|
"required": [
|
|
58
50
|
"kind",
|
|
59
|
-
"
|
|
51
|
+
"script"
|
|
60
52
|
],
|
|
61
53
|
"properties": {
|
|
62
54
|
"kind": {
|
|
63
55
|
"type": "string",
|
|
64
56
|
"enum": [
|
|
65
|
-
"
|
|
57
|
+
"Bash"
|
|
66
58
|
]
|
|
67
59
|
},
|
|
68
|
-
"
|
|
69
|
-
"
|
|
60
|
+
"script": {
|
|
61
|
+
"description": "Shell script to execute.\n\n**Input (stdin):** JSON object: `{\"kind\": \"<step name>\", \"value\": <payload>}`. Use `jq '.value'` to extract the payload, or `jq -r '.value.fieldName'` for a specific field.\n\n**Output (stdout):** JSON array of follow-up tasks to spawn: `[{\"kind\": \"NextStep\", \"value\": {...}}, ...]`. Each `kind` must be a step name listed in this step's `next` array. Return `[]` to spawn no follow-ups.",
|
|
62
|
+
"type": "string"
|
|
70
63
|
}
|
|
71
64
|
}
|
|
72
65
|
}
|
|
73
66
|
]
|
|
74
67
|
},
|
|
75
|
-
"CommandActionFile": {
|
|
76
|
-
"description": "Run a shell command to process tasks.",
|
|
77
|
-
"type": "object",
|
|
78
|
-
"required": [
|
|
79
|
-
"script"
|
|
80
|
-
],
|
|
81
|
-
"properties": {
|
|
82
|
-
"script": {
|
|
83
|
-
"description": "Shell script to execute.\n\n**Input (stdin):** JSON object: `{\"kind\": \"<step name>\", \"value\": <payload>}`. Use `jq '.value'` to extract the payload, or `jq -r '.value.fieldName'` for a specific field.\n\n**Output (stdout):** JSON array of follow-up tasks to spawn: `[{\"kind\": \"NextStep\", \"value\": {...}}, ...]`. Each `kind` must be a step name listed in this step's `next` array. Return `[]` to spawn no follow-ups.",
|
|
84
|
-
"type": "string"
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
68
|
"FinallyHook": {
|
|
89
|
-
"description": "Finally hook. Runs after a task and all its descendants complete.\n\nIn JSON: `{\"kind\": \"
|
|
69
|
+
"description": "Finally hook. Runs after a task and all its descendants complete.\n\nIn JSON: `{\"kind\": \"Bash\", \"script\": \"./finally-hook.sh\"}`\n\n**stdin:** JSON object: `{\"kind\": \"<step name>\", \"value\": <payload>}`. **stdout:** JSON array of follow-up tasks: `[{\"kind\": \"StepName\", \"value\": {...}}, ...]`. Return `[]` for no follow-ups.",
|
|
90
70
|
"oneOf": [
|
|
91
71
|
{
|
|
92
72
|
"description": "Run a shell command as the finally hook.",
|
|
93
73
|
"type": "object",
|
|
94
74
|
"required": [
|
|
95
75
|
"kind",
|
|
96
|
-
"
|
|
76
|
+
"script"
|
|
97
77
|
],
|
|
98
78
|
"properties": {
|
|
99
79
|
"kind": {
|
|
100
80
|
"type": "string",
|
|
101
81
|
"enum": [
|
|
102
|
-
"
|
|
82
|
+
"Bash"
|
|
103
83
|
]
|
|
104
84
|
},
|
|
105
|
-
"
|
|
106
|
-
"
|
|
85
|
+
"script": {
|
|
86
|
+
"description": "Shell script to execute.",
|
|
87
|
+
"type": "string"
|
|
107
88
|
}
|
|
108
89
|
}
|
|
109
90
|
}
|
|
110
91
|
]
|
|
111
92
|
},
|
|
112
|
-
"HookCommand": {
|
|
113
|
-
"description": "A shell command used as a hook.",
|
|
114
|
-
"type": "object",
|
|
115
|
-
"required": [
|
|
116
|
-
"script"
|
|
117
|
-
],
|
|
118
|
-
"properties": {
|
|
119
|
-
"script": {
|
|
120
|
-
"description": "Shell script to execute.",
|
|
121
|
-
"type": "string"
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
93
|
"Options": {
|
|
126
94
|
"description": "Global runtime options for task execution. All fields have sensible defaults.",
|
|
127
95
|
"type": "object",
|
|
128
96
|
"properties": {
|
|
129
|
-
"
|
|
97
|
+
"maxConcurrency": {
|
|
130
98
|
"description": "Maximum concurrent tasks (None = unlimited).",
|
|
131
99
|
"default": null,
|
|
132
100
|
"type": [
|
|
@@ -136,19 +104,19 @@
|
|
|
136
104
|
"format": "uint",
|
|
137
105
|
"minimum": 0.0
|
|
138
106
|
},
|
|
139
|
-
"
|
|
107
|
+
"maxRetries": {
|
|
140
108
|
"description": "Maximum retries per task (default: 0).",
|
|
141
109
|
"default": 0,
|
|
142
110
|
"type": "integer",
|
|
143
111
|
"format": "uint32",
|
|
144
112
|
"minimum": 0.0
|
|
145
113
|
},
|
|
146
|
-
"
|
|
114
|
+
"retryOnInvalidResponse": {
|
|
147
115
|
"description": "Whether to retry when agent returns invalid response (default: true).",
|
|
148
116
|
"default": true,
|
|
149
117
|
"type": "boolean"
|
|
150
118
|
},
|
|
151
|
-
"
|
|
119
|
+
"retryOnTimeout": {
|
|
152
120
|
"description": "Whether to retry when agent times out (default: true).",
|
|
153
121
|
"default": true,
|
|
154
122
|
"type": "boolean"
|
|
@@ -166,7 +134,7 @@
|
|
|
166
134
|
},
|
|
167
135
|
"additionalProperties": false
|
|
168
136
|
},
|
|
169
|
-
"
|
|
137
|
+
"Step": {
|
|
170
138
|
"description": "A named step in the workflow. Steps are the nodes of the task graph.\n\nThe `finally` hook runs after the task **and all of its descendant tasks** complete.",
|
|
171
139
|
"type": "object",
|
|
172
140
|
"required": [
|
|
@@ -178,7 +146,7 @@
|
|
|
178
146
|
"description": "How this step processes tasks.",
|
|
179
147
|
"allOf": [
|
|
180
148
|
{
|
|
181
|
-
"$ref": "#/definitions/
|
|
149
|
+
"$ref": "#/definitions/ActionKind"
|
|
182
150
|
}
|
|
183
151
|
]
|
|
184
152
|
},
|
|
@@ -209,9 +177,9 @@
|
|
|
209
177
|
"options": {
|
|
210
178
|
"description": "Per-step options that override the global `options`. Only the fields you set here take effect; everything else falls through to the global defaults.",
|
|
211
179
|
"default": {
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
180
|
+
"maxRetries": null,
|
|
181
|
+
"retryOnInvalidResponse": null,
|
|
182
|
+
"retryOnTimeout": null,
|
|
215
183
|
"timeout": null
|
|
216
184
|
},
|
|
217
185
|
"allOf": [
|
|
@@ -227,7 +195,7 @@
|
|
|
227
195
|
"description": "Per-step option overrides. Only set the fields you want to override; omitted fields inherit from the global `options`.",
|
|
228
196
|
"type": "object",
|
|
229
197
|
"properties": {
|
|
230
|
-
"
|
|
198
|
+
"maxRetries": {
|
|
231
199
|
"description": "Maximum retries for tasks on this step (overrides global `max_retries`).",
|
|
232
200
|
"default": null,
|
|
233
201
|
"type": [
|
|
@@ -237,7 +205,7 @@
|
|
|
237
205
|
"format": "uint32",
|
|
238
206
|
"minimum": 0.0
|
|
239
207
|
},
|
|
240
|
-
"
|
|
208
|
+
"retryOnInvalidResponse": {
|
|
241
209
|
"description": "Whether to retry when an agent returns an invalid response on this step (overrides global `retry_on_invalid_response`).",
|
|
242
210
|
"default": null,
|
|
243
211
|
"type": [
|
|
@@ -245,7 +213,7 @@
|
|
|
245
213
|
"null"
|
|
246
214
|
]
|
|
247
215
|
},
|
|
248
|
-
"
|
|
216
|
+
"retryOnTimeout": {
|
|
249
217
|
"description": "Whether to retry when an agent times out on this step (overrides global `retry_on_timeout`).",
|
|
250
218
|
"default": null,
|
|
251
219
|
"type": [
|
|
@@ -1,66 +1,55 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
script: z.string().describe("Shell script to execute.\n\n**Input (stdin):** JSON object: `{\"kind\": \"<step name>\", \"value\": <payload>}`. Use `jq '.value'` to extract the payload, or `jq -r '.value.fieldName'` for a specific field.\n\n**Output (stdout):** JSON array of follow-up tasks to spawn: `[{\"kind\": \"NextStep\", \"value\": {...}}, ...]`. Each `kind` must be a step name listed in this step's `next` array. Return `[]` to spawn no follow-ups."),
|
|
5
|
-
}).describe("Run a shell command to process tasks.");
|
|
6
|
-
|
|
7
|
-
const ActionFile = z.discriminatedUnion("kind", [
|
|
3
|
+
const ActionKind = z.discriminatedUnion("kind", [
|
|
8
4
|
z.object({
|
|
9
|
-
kind: z.literal("
|
|
10
|
-
|
|
5
|
+
kind: z.literal("Bash"),
|
|
6
|
+
script: z.string().describe("Shell script to execute.\n\n**Input (stdin):** JSON object: `{\"kind\": \"<step name>\", \"value\": <payload>}`. Use `jq '.value'` to extract the payload, or `jq -r '.value.fieldName'` for a specific field.\n\n**Output (stdout):** JSON array of follow-up tasks to spawn: `[{\"kind\": \"NextStep\", \"value\": {...}}, ...]`. Each `kind` must be a step name listed in this step's `next` array. Return `[]` to spawn no follow-ups."),
|
|
11
7
|
}).describe("Run a shell command."),
|
|
12
8
|
]).describe("How a step processes tasks.");
|
|
13
9
|
|
|
14
|
-
const HookCommand = z.object({
|
|
15
|
-
script: z.string().describe("Shell script to execute."),
|
|
16
|
-
}).describe("A shell command used as a hook.");
|
|
17
|
-
|
|
18
10
|
const FinallyHook = z.discriminatedUnion("kind", [
|
|
19
11
|
z.object({
|
|
20
|
-
kind: z.literal("
|
|
21
|
-
|
|
12
|
+
kind: z.literal("Bash"),
|
|
13
|
+
script: z.string().describe("Shell script to execute."),
|
|
22
14
|
}).describe("Run a shell command as the finally hook."),
|
|
23
|
-
]).describe("Finally hook. Runs after a task and all its descendants complete.\n\nIn JSON: `{\"kind\": \"
|
|
15
|
+
]).describe("Finally hook. Runs after a task and all its descendants complete.\n\nIn JSON: `{\"kind\": \"Bash\", \"script\": \"./finally-hook.sh\"}`\n\n**stdin:** JSON object: `{\"kind\": \"<step name>\", \"value\": <payload>}`. **stdout:** JSON array of follow-up tasks: `[{\"kind\": \"StepName\", \"value\": {...}}, ...]`. Return `[]` for no follow-ups.");
|
|
24
16
|
|
|
25
17
|
const Options = z.object({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
maxConcurrency: z.number().int().nonnegative().nullable().optional().default(null).describe("Maximum concurrent tasks (None = unlimited)."),
|
|
19
|
+
maxRetries: z.number().int().nonnegative().optional().default(0).describe("Maximum retries per task (default: 0)."),
|
|
20
|
+
retryOnInvalidResponse: z.boolean().optional().default(true).describe("Whether to retry when agent returns invalid response (default: true)."),
|
|
21
|
+
retryOnTimeout: z.boolean().optional().default(true).describe("Whether to retry when agent times out (default: true)."),
|
|
30
22
|
timeout: z.number().int().nonnegative().nullable().optional().default(null).describe("Timeout in seconds for each task (None = no timeout)."),
|
|
31
23
|
}).strict().describe("Global runtime options for task execution. All fields have sensible defaults.");
|
|
32
24
|
|
|
33
25
|
const StepOptions = z.object({
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
maxRetries: z.number().int().nonnegative().nullable().optional().default(null).describe("Maximum retries for tasks on this step (overrides global `max_retries`)."),
|
|
27
|
+
retryOnInvalidResponse: z.boolean().nullable().optional().default(null).describe("Whether to retry when an agent returns an invalid response on this step (overrides global `retry_on_invalid_response`)."),
|
|
28
|
+
retryOnTimeout: z.boolean().nullable().optional().default(null).describe("Whether to retry when an agent times out on this step (overrides global `retry_on_timeout`)."),
|
|
37
29
|
timeout: z.number().int().nonnegative().nullable().optional().default(null).describe("Timeout in seconds for tasks on this step (overrides global `timeout`)."),
|
|
38
30
|
}).strict().describe("Per-step option overrides. Only set the fields you want to override; omitted fields inherit from the global `options`.");
|
|
39
31
|
|
|
40
|
-
const
|
|
41
|
-
action:
|
|
32
|
+
const Step = z.object({
|
|
33
|
+
action: ActionKind.describe("How this step processes tasks."),
|
|
42
34
|
finally: FinallyHook.nullable().optional().default(null).describe("Shell script that runs after this task **and all tasks it spawned (recursively)** have completed.\n\n**stdin:** JSON object: `{\"kind\": \"<step name>\", \"value\": <payload>}`. Same envelope format as command action scripts.\n\n**stdout:** A JSON array of follow-up tasks to spawn: `[{\"kind\": \"StepName\", \"value\": {...}}, ...]`. Each `kind` must be a valid step name. Return `[]` to spawn no follow-ups.\n\nUse this for cleanup, aggregation, or spawning a final summarization step after an entire subtree of work completes."),
|
|
43
35
|
name: z.string().describe("Unique name for this step (e.g., `\"Analyze\"`, `\"Implement\"`, `\"Review\"`). This is the string used as `kind` when creating tasks: `{\"kind\": \"ThisStepName\", \"value\": {...}}`."),
|
|
44
36
|
next: z.array(z.string()).optional().default([]).describe("Step names this step is allowed to spawn follow-up tasks on. Each string must match the `name` of another step in this config. An empty array means this is a terminal step (no follow-ups)."),
|
|
45
|
-
options: StepOptions.optional().default({"
|
|
37
|
+
options: StepOptions.optional().default({"maxRetries": null, "retryOnInvalidResponse": null, "retryOnTimeout": null, "timeout": null}).describe("Per-step options that override the global `options`. Only the fields you set here take effect; everything else falls through to the global defaults."),
|
|
46
38
|
}).strict().describe("A named step in the workflow. Steps are the nodes of the task graph.\n\nThe `finally` hook runs after the task **and all of its descendant tasks** complete.");
|
|
47
39
|
|
|
48
|
-
export const
|
|
49
|
-
"$schema": z.string().nullable().optional().describe("Optional JSON Schema URL for editor validation (e.g., `\"./node_modules/@barnum/barnum/barnum-config-schema.json\"`). Ignored at runtime."),
|
|
40
|
+
export const configSchema = z.object({
|
|
50
41
|
entrypoint: z.string().nullable().optional().default(null).describe("Name of the step that starts the workflow. When set, the CLI accepts `--entrypoint-value` to provide the initial task value (defaults to `{}`). When omitted, `--initial-state` must provide explicit `[{\"kind\": \"StepName\", \"value\": ...}]` tasks."),
|
|
51
|
-
options: Options.optional().default({"
|
|
52
|
-
steps: z.array(
|
|
53
|
-
}).strict().describe("Top-level Barnum configuration
|
|
42
|
+
options: Options.optional().default({"maxConcurrency": null, "maxRetries": 0, "retryOnInvalidResponse": true, "retryOnTimeout": true, "timeout": null}).describe("Global runtime options (timeout, retries, concurrency). Individual steps can override these via their own `options` field."),
|
|
43
|
+
steps: z.array(Step).describe("The steps that make up this workflow. Each step defines how to process a task and which steps it can spawn follow-up tasks on."),
|
|
44
|
+
}).strict().describe("Top-level Barnum configuration.\n\nDefines a workflow as a directed graph of steps. Each step processes tasks and can spawn follow-up tasks on other steps.");
|
|
54
45
|
|
|
55
|
-
export type
|
|
56
|
-
export type
|
|
57
|
-
export type ActionFile = z.infer<typeof ActionFile>;
|
|
58
|
-
export type HookCommand = z.infer<typeof HookCommand>;
|
|
46
|
+
export type Config = z.infer<typeof configSchema>;
|
|
47
|
+
export type ActionKind = z.infer<typeof ActionKind>;
|
|
59
48
|
export type FinallyHook = z.infer<typeof FinallyHook>;
|
|
60
49
|
export type Options = z.infer<typeof Options>;
|
|
61
50
|
export type StepOptions = z.infer<typeof StepOptions>;
|
|
62
|
-
export type
|
|
51
|
+
export type Step = z.infer<typeof Step>;
|
|
63
52
|
|
|
64
|
-
export function defineConfig(config: z.input<typeof
|
|
65
|
-
return
|
|
53
|
+
export function defineConfig(config: z.input<typeof configSchema>): Config {
|
|
54
|
+
return configSchema.parse(config);
|
|
66
55
|
}
|
package/package.json
CHANGED
package/run.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import { chmodSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import {
|
|
4
|
+
import { configSchema } from "./barnum-config-schema.zod.js";
|
|
5
5
|
import type { z } from "zod";
|
|
6
6
|
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const binaryPath: string = process.env.BARNUM ?? require("./index.cjs");
|
|
9
9
|
|
|
10
|
-
function spawnBarnum(args: string[]): ChildProcess {
|
|
10
|
+
function spawnBarnum(args: string[], cwd?: string): ChildProcess {
|
|
11
11
|
try {
|
|
12
12
|
chmodSync(binaryPath, 0o755);
|
|
13
13
|
} catch {}
|
|
14
|
-
return spawn(binaryPath, args, { stdio: "inherit" });
|
|
14
|
+
return spawn(binaryPath, args, { stdio: "inherit", cwd });
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface RunOptions {
|
|
@@ -21,17 +21,18 @@ export interface RunOptions {
|
|
|
21
21
|
logFile?: string;
|
|
22
22
|
stateLog?: string;
|
|
23
23
|
wake?: string;
|
|
24
|
+
cwd?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export class BarnumConfig {
|
|
27
|
-
private readonly config: z.output<typeof
|
|
28
|
+
private readonly config: z.output<typeof configSchema>;
|
|
28
29
|
|
|
29
|
-
private constructor(config: z.output<typeof
|
|
30
|
+
private constructor(config: z.output<typeof configSchema>) {
|
|
30
31
|
this.config = config;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
static fromConfig(config: z.input<typeof
|
|
34
|
-
return new BarnumConfig(
|
|
34
|
+
static fromConfig(config: z.input<typeof configSchema>): BarnumConfig {
|
|
35
|
+
return new BarnumConfig(configSchema.parse(config));
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
run(opts?: RunOptions): ChildProcess {
|
|
@@ -43,6 +44,6 @@ export class BarnumConfig {
|
|
|
43
44
|
if (opts?.logFile) args.push("--log-file", opts.logFile);
|
|
44
45
|
if (opts?.stateLog) args.push("--state-log", opts.stateLog);
|
|
45
46
|
if (opts?.wake) args.push("--wake", opts.wake);
|
|
46
|
-
return spawnBarnum(args);
|
|
47
|
+
return spawnBarnum(args, opts?.cwd);
|
|
47
48
|
}
|
|
48
49
|
}
|