ocpipe 0.6.9 → 0.6.11
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 +19 -15
- package/example/correction.ts +1 -2
- package/example/index.ts +6 -7
- package/package.json +13 -12
- package/src/agent.ts +184 -209
- package/src/claude-code.ts +19 -24
- package/src/index.ts +3 -3
- package/src/omp.ts +313 -0
- package/src/pipeline.ts +8 -7
- package/src/predict.ts +9 -8
- package/src/testing.ts +7 -5
- package/src/types.ts +75 -42
package/src/omp.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ocpipe Oh My Pi integration.
|
|
3
|
+
*
|
|
4
|
+
* Runs Oh My Pi through its headless JSON print mode.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn, type ChildProcess } from 'child_process'
|
|
8
|
+
import { isAbsolute, join } from 'path'
|
|
9
|
+
import { PROJECT_ROOT } from './paths.js'
|
|
10
|
+
import type { OmpOptions, RunAgentOptions, RunAgentResult } from './types.js'
|
|
11
|
+
|
|
12
|
+
interface OmpProcessRequest {
|
|
13
|
+
command: string
|
|
14
|
+
args: string[]
|
|
15
|
+
cwd: string
|
|
16
|
+
env: NodeJS.ProcessEnv
|
|
17
|
+
signal?: AbortSignal
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface OmpProcessResult {
|
|
21
|
+
stdout: string
|
|
22
|
+
stderr: string
|
|
23
|
+
exitCode: number | null
|
|
24
|
+
signal: NodeJS.Signals | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OmpProcess {
|
|
28
|
+
run(req: OmpProcessRequest): Promise<OmpProcessResult>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface OmpRunSummary {
|
|
32
|
+
sawJsonEvent: boolean
|
|
33
|
+
sessionId: string
|
|
34
|
+
finalMessage: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const defaultOmpCommand = 'omp'
|
|
38
|
+
const defaultOmpThinking = 'high'
|
|
39
|
+
const defaultOmpApprovalMode = 'yolo'
|
|
40
|
+
|
|
41
|
+
/** runOmpAgent executes an Oh My Pi coding-agent turn. */
|
|
42
|
+
export async function runOmpAgent(
|
|
43
|
+
options: RunAgentOptions,
|
|
44
|
+
processRunner: OmpProcess = commandOmpProcess,
|
|
45
|
+
): Promise<RunAgentResult> {
|
|
46
|
+
const {
|
|
47
|
+
prompt,
|
|
48
|
+
model,
|
|
49
|
+
sessionId,
|
|
50
|
+
timeoutSec = 3600,
|
|
51
|
+
workdir,
|
|
52
|
+
omp,
|
|
53
|
+
signal,
|
|
54
|
+
} = options
|
|
55
|
+
|
|
56
|
+
if (signal?.aborted) {
|
|
57
|
+
throw new Error('Request aborted')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const cwd = workdir ?? PROJECT_ROOT
|
|
61
|
+
const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
|
|
62
|
+
const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
|
|
63
|
+
console.error(
|
|
64
|
+
`\n>>> OMP [${model.modelID}] ${sessionInfo}: ${promptPreview}...`,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const abort = new AbortController()
|
|
68
|
+
const abortHandler = () => abort.abort()
|
|
69
|
+
signal?.addEventListener('abort', abortHandler, { once: true })
|
|
70
|
+
let timedOut = false
|
|
71
|
+
const timeout =
|
|
72
|
+
timeoutSec > 0 ?
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
timedOut = true
|
|
75
|
+
abort.abort()
|
|
76
|
+
}, timeoutSec * 1000)
|
|
77
|
+
: null
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await processRunner.run({
|
|
81
|
+
command: omp?.command ?? defaultOmpCommand,
|
|
82
|
+
args: buildOmpArgs(model.modelID, cwd, sessionId, prompt, omp),
|
|
83
|
+
cwd: omp?.processCwd ?? cwd,
|
|
84
|
+
env: buildOmpEnv(omp),
|
|
85
|
+
signal: abort.signal,
|
|
86
|
+
})
|
|
87
|
+
const summary = parseOmpOutput(result.stdout)
|
|
88
|
+
const detail =
|
|
89
|
+
result.signal ? `signal ${result.signal}` : `status ${result.exitCode}`
|
|
90
|
+
if (result.exitCode !== 0 || result.signal) {
|
|
91
|
+
const message = firstNonEmpty(
|
|
92
|
+
summary.finalMessage,
|
|
93
|
+
result.stderr.trim(),
|
|
94
|
+
result.stdout.trim(),
|
|
95
|
+
detail,
|
|
96
|
+
)
|
|
97
|
+
throw new Error(`OMP exited with ${detail}: ${message}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const response = firstNonEmpty(
|
|
101
|
+
summary.finalMessage,
|
|
102
|
+
summary.sawJsonEvent ? '' : result.stdout.trim(),
|
|
103
|
+
)
|
|
104
|
+
if (!response) {
|
|
105
|
+
throw new Error('OMP returned an empty final message')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const nextSessionId = firstNonEmpty(summary.sessionId, sessionId ?? '')
|
|
109
|
+
console.error(
|
|
110
|
+
`<<< OMP done (${response.length} chars)${nextSessionId ? ` [session:${nextSessionId}]` : ''}`,
|
|
111
|
+
)
|
|
112
|
+
return {
|
|
113
|
+
text: response,
|
|
114
|
+
sessionId: nextSessionId,
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (timedOut) {
|
|
118
|
+
throw new Error(`Timeout after ${timeoutSec}s`, { cause: err })
|
|
119
|
+
}
|
|
120
|
+
if (signal?.aborted) {
|
|
121
|
+
throw new Error('Request aborted', { cause: err })
|
|
122
|
+
}
|
|
123
|
+
throw err
|
|
124
|
+
} finally {
|
|
125
|
+
if (timeout) clearTimeout(timeout)
|
|
126
|
+
signal?.removeEventListener('abort', abortHandler)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildOmpArgs(
|
|
131
|
+
modelID: string,
|
|
132
|
+
cwd: string,
|
|
133
|
+
sessionId: string | undefined,
|
|
134
|
+
prompt: string,
|
|
135
|
+
omp: OmpOptions | undefined,
|
|
136
|
+
): string[] {
|
|
137
|
+
const args = ['--print', '--mode', 'json', '--cwd', cwd]
|
|
138
|
+
if (modelID) {
|
|
139
|
+
args.push('--model', modelID)
|
|
140
|
+
}
|
|
141
|
+
if (hasOwn(omp, 'codexHome')) {
|
|
142
|
+
args.push('--codex-home', omp?.codexHome ?? '')
|
|
143
|
+
}
|
|
144
|
+
if (sessionId) {
|
|
145
|
+
args.push('--resume', sessionId)
|
|
146
|
+
}
|
|
147
|
+
if (omp?.goalMode || firstNonEmpty(omp?.goalObjective ?? '')) {
|
|
148
|
+
const objective = firstNonEmpty(omp?.goalObjective ?? '', prompt)
|
|
149
|
+
if (objective) {
|
|
150
|
+
args.push('--goal', objective)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
typeof omp?.contextStopPercent === 'number' &&
|
|
155
|
+
omp.contextStopPercent > 0
|
|
156
|
+
) {
|
|
157
|
+
args.push('--context-stop-percent', String(omp.contextStopPercent))
|
|
158
|
+
}
|
|
159
|
+
if (typeof omp?.contextStopTokens === 'number' && omp.contextStopTokens > 0) {
|
|
160
|
+
args.push('--context-stop-tokens', String(omp.contextStopTokens))
|
|
161
|
+
}
|
|
162
|
+
if (omp?.scratchHandoffFile) {
|
|
163
|
+
args.push(
|
|
164
|
+
'--scratch-handoff-file',
|
|
165
|
+
resolveOmpPath(cwd, omp.scratchHandoffFile),
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
if (omp?.autoApprove ?? true) {
|
|
169
|
+
args.push('--auto-approve')
|
|
170
|
+
}
|
|
171
|
+
const approvalMode = omp?.approvalMode ?? defaultOmpApprovalMode
|
|
172
|
+
if (approvalMode) {
|
|
173
|
+
args.push('--approval-mode', approvalMode)
|
|
174
|
+
}
|
|
175
|
+
const thinking = omp?.thinking ?? defaultOmpThinking
|
|
176
|
+
if (thinking) {
|
|
177
|
+
args.push('--thinking', thinking)
|
|
178
|
+
}
|
|
179
|
+
args.push(...(omp?.extraArgs ?? []))
|
|
180
|
+
if (prompt) {
|
|
181
|
+
args.push('--', prompt)
|
|
182
|
+
}
|
|
183
|
+
return args
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildOmpEnv(omp: OmpOptions | undefined): NodeJS.ProcessEnv {
|
|
187
|
+
const env: NodeJS.ProcessEnv = { ...process.env, ...omp?.env }
|
|
188
|
+
if (hasOwn(omp, 'codexHome')) {
|
|
189
|
+
delete env.CODEX_HOME
|
|
190
|
+
}
|
|
191
|
+
if (omp?.home) {
|
|
192
|
+
env.OMP_HOME = omp.home
|
|
193
|
+
}
|
|
194
|
+
return env
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveOmpPath(cwd: string, path: string): string {
|
|
198
|
+
return isAbsolute(path) ? path : join(cwd, path)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseOmpOutput(stdout: string): OmpRunSummary {
|
|
202
|
+
const summary: OmpRunSummary = {
|
|
203
|
+
sawJsonEvent: false,
|
|
204
|
+
sessionId: '',
|
|
205
|
+
finalMessage: '',
|
|
206
|
+
}
|
|
207
|
+
for (const rawLine of stdout.split('\n')) {
|
|
208
|
+
const line = rawLine.trim()
|
|
209
|
+
if (!line) continue
|
|
210
|
+
let event: unknown
|
|
211
|
+
try {
|
|
212
|
+
event = JSON.parse(line)
|
|
213
|
+
} catch {
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
if (!isRecord(event)) continue
|
|
217
|
+
summary.sawJsonEvent = true
|
|
218
|
+
const type = ompString(event.type)
|
|
219
|
+
if (type === 'session') {
|
|
220
|
+
summary.sessionId = firstNonEmpty(
|
|
221
|
+
ompString(event.id),
|
|
222
|
+
ompString(event.sessionId),
|
|
223
|
+
ompString(event.session_id),
|
|
224
|
+
summary.sessionId,
|
|
225
|
+
)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
if (type === 'message_end') {
|
|
229
|
+
const text = ompAssistantText(event.message)
|
|
230
|
+
if (text) {
|
|
231
|
+
summary.finalMessage = text
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!summary.sawJsonEvent) {
|
|
236
|
+
summary.finalMessage = stdout.trim()
|
|
237
|
+
}
|
|
238
|
+
return summary
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function ompAssistantText(raw: unknown): string {
|
|
242
|
+
if (!isRecord(raw) || ompString(raw.role) !== 'assistant') return ''
|
|
243
|
+
if (Array.isArray(raw.content)) {
|
|
244
|
+
const parts: string[] = []
|
|
245
|
+
for (const rawPart of raw.content) {
|
|
246
|
+
if (!isRecord(rawPart) || ompString(rawPart.type) !== 'text') continue
|
|
247
|
+
const text = ompString(rawPart.text).trim()
|
|
248
|
+
if (text) parts.push(text)
|
|
249
|
+
}
|
|
250
|
+
return parts.join('\n\n')
|
|
251
|
+
}
|
|
252
|
+
return ompString(raw.content).trim()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const commandOmpProcess: OmpProcess = {
|
|
256
|
+
run(req) {
|
|
257
|
+
const { promise, resolve, reject } =
|
|
258
|
+
Promise.withResolvers<OmpProcessResult>()
|
|
259
|
+
let child: ChildProcess
|
|
260
|
+
try {
|
|
261
|
+
child = spawn(req.command, req.args, {
|
|
262
|
+
cwd: req.cwd,
|
|
263
|
+
env: req.env,
|
|
264
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
265
|
+
})
|
|
266
|
+
} catch (err) {
|
|
267
|
+
reject(err)
|
|
268
|
+
return promise
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const stdoutChunks: Buffer[] = []
|
|
272
|
+
const stderrChunks: Buffer[] = []
|
|
273
|
+
const abortHandler = () => child.kill()
|
|
274
|
+
req.signal?.addEventListener('abort', abortHandler, { once: true })
|
|
275
|
+
|
|
276
|
+
child.stdout?.on('data', (chunk: Buffer) => stdoutChunks.push(chunk))
|
|
277
|
+
child.stderr?.on('data', (chunk: Buffer) => stderrChunks.push(chunk))
|
|
278
|
+
child.on('error', reject)
|
|
279
|
+
child.on('close', (exitCode, signal) => {
|
|
280
|
+
req.signal?.removeEventListener('abort', abortHandler)
|
|
281
|
+
resolve({
|
|
282
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
283
|
+
stderr: Buffer.concat(stderrChunks).toString('utf8'),
|
|
284
|
+
exitCode,
|
|
285
|
+
signal,
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
return promise
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function hasOwn<T extends object, K extends PropertyKey>(
|
|
293
|
+
value: T | undefined,
|
|
294
|
+
key: K,
|
|
295
|
+
): value is T & Record<K, unknown> {
|
|
296
|
+
return !!value && Object.prototype.hasOwnProperty.call(value, key)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
300
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function ompString(value: unknown): string {
|
|
304
|
+
return typeof value === 'string' ? value : ''
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function firstNonEmpty(...values: string[]): string {
|
|
308
|
+
for (const value of values) {
|
|
309
|
+
const trimmed = value.trim()
|
|
310
|
+
if (trimmed) return trimmed
|
|
311
|
+
}
|
|
312
|
+
return ''
|
|
313
|
+
}
|
package/src/pipeline.ts
CHANGED
|
@@ -30,12 +30,13 @@ export class Pipeline<S extends BaseState> {
|
|
|
30
30
|
this.ctx = {
|
|
31
31
|
sessionId: undefined,
|
|
32
32
|
defaultModel: config.defaultModel,
|
|
33
|
-
defaultAgent: config.defaultAgent,
|
|
34
33
|
timeoutSec: config.timeoutSec ?? 3600,
|
|
35
34
|
workdir: config.workdir,
|
|
35
|
+
opencode: config.opencode,
|
|
36
36
|
claudeCode: config.claudeCode,
|
|
37
37
|
codex: config.codex,
|
|
38
38
|
pi: config.pi,
|
|
39
|
+
omp: config.omp,
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -88,8 +89,8 @@ export class Pipeline<S extends BaseState> {
|
|
|
88
89
|
result: result as StepResult<unknown>,
|
|
89
90
|
})
|
|
90
91
|
|
|
91
|
-
// Update
|
|
92
|
-
this.state.
|
|
92
|
+
// Update backend session in state.
|
|
93
|
+
this.state.agentSessionId = this.ctx.sessionId
|
|
93
94
|
|
|
94
95
|
await this.saveCheckpoint()
|
|
95
96
|
return result
|
|
@@ -158,7 +159,7 @@ export class Pipeline<S extends BaseState> {
|
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
/** getSessionId returns the current
|
|
162
|
+
/** getSessionId returns the current backend session ID. */
|
|
162
163
|
getSessionId(): string | undefined {
|
|
163
164
|
return this.ctx.sessionId
|
|
164
165
|
}
|
|
@@ -188,9 +189,9 @@ export class Pipeline<S extends BaseState> {
|
|
|
188
189
|
const pipeline = new Pipeline<S>(config, () => state)
|
|
189
190
|
pipeline.state = state
|
|
190
191
|
|
|
191
|
-
// Restore context from state
|
|
192
|
-
if (state.
|
|
193
|
-
pipeline.ctx.sessionId = state.
|
|
192
|
+
// Restore context from state.
|
|
193
|
+
if (state.agentSessionId) {
|
|
194
|
+
pipeline.ctx.sessionId = state.agentSessionId
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
// Restore step number
|
package/src/predict.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ocpipe Predict class.
|
|
3
3
|
*
|
|
4
|
-
* Executes a signature by generating a prompt, calling
|
|
4
|
+
* Executes a signature by generating a prompt, calling a backend, and parsing
|
|
5
|
+
* the response.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { z } from 'zod/v4'
|
|
@@ -37,8 +38,6 @@ import {
|
|
|
37
38
|
|
|
38
39
|
/** Configuration for a Predict instance. */
|
|
39
40
|
export interface PredictConfig {
|
|
40
|
-
/** Override the pipeline's default agent. */
|
|
41
|
-
agent?: string
|
|
42
41
|
/** Override the pipeline's default model. */
|
|
43
42
|
model?: ModelConfig
|
|
44
43
|
/** Start a fresh session (default: false, reuses context). */
|
|
@@ -71,14 +70,15 @@ export class Predict<S extends AnySignature> {
|
|
|
71
70
|
|
|
72
71
|
const agentResult = await runAgent({
|
|
73
72
|
prompt,
|
|
74
|
-
agent: this.config.agent ?? ctx.defaultAgent,
|
|
75
73
|
model: this.config.model ?? ctx.defaultModel,
|
|
76
74
|
sessionId: this.config.newSession ? undefined : ctx.sessionId,
|
|
77
75
|
timeoutSec: ctx.timeoutSec,
|
|
78
76
|
workdir: ctx.workdir,
|
|
79
77
|
claudeCode: ctx.claudeCode,
|
|
78
|
+
opencode: ctx.opencode,
|
|
80
79
|
codex: ctx.codex,
|
|
81
80
|
pi: ctx.pi,
|
|
81
|
+
omp: ctx.omp,
|
|
82
82
|
signal: ctx.signal,
|
|
83
83
|
})
|
|
84
84
|
|
|
@@ -205,12 +205,13 @@ export class Predict<S extends AnySignature> {
|
|
|
205
205
|
prompt: repairPrompt,
|
|
206
206
|
model: correctionModel ?? ctx.defaultModel,
|
|
207
207
|
sessionId: correctionModel ? undefined : sessionId,
|
|
208
|
-
agent: ctx.defaultAgent,
|
|
209
208
|
timeoutSec: ctx.timeoutSec,
|
|
210
209
|
workdir: ctx.workdir,
|
|
211
210
|
claudeCode: ctx.claudeCode,
|
|
211
|
+
opencode: ctx.opencode,
|
|
212
212
|
codex: ctx.codex,
|
|
213
213
|
pi: ctx.pi,
|
|
214
|
+
omp: ctx.omp,
|
|
214
215
|
signal: ctx.signal,
|
|
215
216
|
})
|
|
216
217
|
|
|
@@ -284,12 +285,13 @@ export class Predict<S extends AnySignature> {
|
|
|
284
285
|
prompt: patchPrompt,
|
|
285
286
|
model: correctionModel ?? ctx.defaultModel,
|
|
286
287
|
sessionId: correctionModel ? undefined : sessionId,
|
|
287
|
-
agent: ctx.defaultAgent,
|
|
288
288
|
timeoutSec: ctx.timeoutSec,
|
|
289
289
|
workdir: ctx.workdir,
|
|
290
290
|
claudeCode: ctx.claudeCode,
|
|
291
|
+
opencode: ctx.opencode,
|
|
291
292
|
codex: ctx.codex,
|
|
292
293
|
pi: ctx.pi,
|
|
294
|
+
omp: ctx.omp,
|
|
293
295
|
signal: ctx.signal,
|
|
294
296
|
})
|
|
295
297
|
|
|
@@ -396,8 +398,7 @@ export class Predict<S extends AnySignature> {
|
|
|
396
398
|
// Add field descriptions from our config (toJSONSchema uses .describe() metadata)
|
|
397
399
|
// Since our FieldConfig has a separate desc field, merge it in
|
|
398
400
|
const props = jsonSchema.properties as
|
|
399
|
-
|
|
400
|
-
| undefined
|
|
401
|
+
Record<string, Record<string, unknown>> | undefined
|
|
401
402
|
if (props) {
|
|
402
403
|
for (const [name, config] of Object.entries(this.sig.outputs) as [
|
|
403
404
|
string,
|
package/src/testing.ts
CHANGED
|
@@ -134,18 +134,20 @@ export class MockAgentBackend {
|
|
|
134
134
|
export function createMockContext(
|
|
135
135
|
overrides?: Partial<{
|
|
136
136
|
sessionId: string
|
|
137
|
-
defaultModel: {
|
|
138
|
-
|
|
137
|
+
defaultModel: {
|
|
138
|
+
backend?: 'claude-code' | 'codex' | 'pi' | 'omp'
|
|
139
|
+
providerID?: string
|
|
140
|
+
modelID: string
|
|
141
|
+
}
|
|
139
142
|
timeoutSec: number
|
|
140
143
|
}>,
|
|
141
144
|
) {
|
|
142
145
|
return {
|
|
143
146
|
sessionId: overrides?.sessionId,
|
|
144
147
|
defaultModel: overrides?.defaultModel ?? {
|
|
145
|
-
|
|
146
|
-
modelID: '
|
|
148
|
+
backend: 'omp',
|
|
149
|
+
modelID: 'gpt-5.5',
|
|
147
150
|
},
|
|
148
|
-
defaultAgent: overrides?.defaultAgent ?? 'general',
|
|
149
151
|
timeoutSec: overrides?.timeoutSec ?? 60,
|
|
150
152
|
}
|
|
151
153
|
}
|