@nylorun/harness 0.5.0-beta.1 → 0.8.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/CHANGELOG.md +63 -0
- package/README.md +27 -108
- package/dist/build/agent.d.ts +6 -7
- package/dist/build/agent.js +16 -7
- package/dist/build/assemble.d.ts +5 -6
- package/dist/build/assemble.js +10 -43
- package/dist/build/bind-tool.d.ts +2 -3
- package/dist/build/bind-tool.js +6 -4
- package/dist/build/builder.d.ts +30 -15
- package/dist/build/builder.js +89 -38
- package/dist/build/helpers.d.ts +2 -3
- package/dist/build/helpers.js +0 -1
- package/dist/build/manifest.d.ts +2 -4
- package/dist/build/manifest.js +2 -8
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +2 -2
- package/dist/session/capability-state.d.ts +14 -0
- package/dist/session/capability-state.js +67 -0
- package/dist/session/input-queue.d.ts +11 -2
- package/dist/session/input-queue.js +5 -1
- package/dist/session/record.d.ts +11 -0
- package/dist/session/record.js +26 -0
- package/dist/session/scheduler.d.ts +17 -3
- package/dist/session/scheduler.js +181 -48
- package/dist/session/seed.d.ts +10 -0
- package/dist/session/seed.js +219 -0
- package/dist/session/session.d.ts +3 -2
- package/dist/session/session.js +15 -3
- package/dist/session/state.d.ts +8 -3
- package/dist/session/state.js +16 -4
- package/dist/session/submission-stream.d.ts +2 -0
- package/dist/session/submission-stream.js +11 -1
- package/dist/step/model-configuration.d.ts +1 -3
- package/dist/step/model-configuration.js +2 -4
- package/dist/step/project.js +4 -2
- package/dist/step/run.d.ts +6 -1
- package/dist/step/run.js +35 -13
- package/dist/step/seal.d.ts +6 -2
- package/dist/step/seal.js +4 -3
- package/dist/step/step-context.d.ts +5 -2
- package/dist/step/step-context.js +19 -11
- package/dist/turn/plan-runner.d.ts +28 -13
- package/dist/turn/plan-runner.js +165 -182
- package/dist/turn/runner.d.ts +18 -4
- package/dist/turn/runner.js +73 -45
- package/dist/types/manifest.d.ts +2 -6
- package/dist/types/middleware.d.ts +25 -4
- package/dist/types/model.d.ts +3 -2
- package/dist/types/session.d.ts +86 -2
- package/dist/types/shared.d.ts +65 -9
- package/dist/types/tool.d.ts +37 -39
- package/dist/utils/immutable.js +16 -2
- package/package.json +1 -2
- package/dist/build/adapters.d.ts +0 -11
- package/dist/build/adapters.js +0 -91
- package/docs/loop.md +0 -47
- package/docs/model-call-projection.md +0 -112
- package/docs/reference.md +0 -122
package/dist/build/helpers.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ModelAdapter } from "../types/model.js";
|
|
2
2
|
import type { StepMiddleware } from "../types/middleware.js";
|
|
3
|
-
import type {
|
|
4
|
-
export declare const tool: <
|
|
5
|
-
export declare const adapter: <T extends ToolAdapter>(value: T) => T;
|
|
3
|
+
import type { ToolDefinition, ToolObjectSchema } from "../types/tool.js";
|
|
4
|
+
export declare const tool: <Parameters extends ToolObjectSchema, State = never>(value: ToolDefinition<Parameters, State>) => ToolDefinition<Parameters, State>;
|
|
6
5
|
export declare const model: <T extends ModelAdapter>(value: T) => T;
|
|
7
6
|
export declare const middleware: <T extends StepMiddleware>(value: T) => T;
|
package/dist/build/helpers.js
CHANGED
package/dist/build/manifest.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { AgentManifest } from "../types/manifest.js";
|
|
2
2
|
import type { BoundMiddleware } from "../types/middleware.js";
|
|
3
|
-
import type { ModelDirective } from "../types/model.js";
|
|
4
|
-
import type { AdapterRegistry } from "./adapters.js";
|
|
5
3
|
export declare function createManifest(input: {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
6
|
middleware: readonly BoundMiddleware[];
|
|
7
|
-
directive?: ModelDirective;
|
|
8
|
-
adapters: AdapterRegistry;
|
|
9
7
|
}): AgentManifest;
|
package/dist/build/manifest.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { deepFreeze } from "../utils/immutable.js";
|
|
2
2
|
export function createManifest(input) {
|
|
3
3
|
const middleware = input.middleware.map(({ id }) => ({ id }));
|
|
4
|
-
const adapters = [...input.adapters.entries].map(([, entry]) => ({
|
|
5
|
-
id: entry.adapter.id,
|
|
6
|
-
...(entry.options.maxConcurrentCalls === undefined
|
|
7
|
-
? {}
|
|
8
|
-
: { maxConcurrentCalls: entry.options.maxConcurrentCalls }),
|
|
9
|
-
}));
|
|
10
4
|
return deepFreeze({
|
|
5
|
+
id: input.id,
|
|
6
|
+
name: input.name,
|
|
11
7
|
middleware,
|
|
12
|
-
...(input.directive === undefined ? {} : { model: input.directive }),
|
|
13
|
-
adapters,
|
|
14
8
|
});
|
|
15
9
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Machine-readable error raised by Harness-owned code. */
|
|
2
|
-
export type HarnessErrorCode = "
|
|
2
|
+
export type HarnessErrorCode = "agent.build-failed" | "agent.lifecycle-sealed" | "capability.state.create-failed" | "capability.state.undeclared" | "context.invalid-item" | "context.invalid-item-type" | "context.invalid-order" | "context.invalid-reason" | "context.invalid-slot" | "interaction.invalid" | "interaction.missing-resume" | "interaction.uncorrelated-resume" | "json.invalid-data" | "json.invalid-object" | "middleware.next-after-return" | "middleware.next-called-twice" | "middleware.request-mutators-revoked" | "model.candidate-missing" | "model.invalid-candidate" | "model.invalid-directive" | "configuration.duplicate-tool-name" | "configuration.invalid" | "configuration.invalid-instructions" | "configuration.invalid-order" | "configuration.invalid-reason" | "configuration.invalid-slot" | "configuration.invalid-tools" | "configuration.model-selection-conflict" | "response.invalid-replacement" | "session.invalid-seed" | "session.record-failed" | "session.stale-result" | "tool.invalid" | "tool.invalid-arguments" | "tool.invalid-name" | "tool.invalid-schema" | "tool.invalid-tool-result";
|
|
3
3
|
export type HarnessErrorDetails = Readonly<Record<string, string | number | boolean>>;
|
|
4
4
|
export interface HarnessErrorOptions {
|
|
5
5
|
readonly cause?: unknown;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError } from "./build/builder.js";
|
|
1
|
+
export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError, BoundAgentBuilder, } from "./build/builder.js";
|
|
2
|
+
export type { AgentOptions } from "./build/builder.js";
|
|
2
3
|
export { HarnessError, isHarnessError } from "./errors.js";
|
|
3
4
|
export type { HarnessErrorCode, HarnessErrorDetails, HarnessErrorOptions } from "./errors.js";
|
|
4
5
|
export { BuiltAgent } from "./build/agent.js";
|
|
5
|
-
export {
|
|
6
|
+
export { middleware, model, tool } from "./build/helpers.js";
|
|
6
7
|
export type { AgentManifest } from "./types/manifest.js";
|
|
7
8
|
export type { ModelCandidate, ModelControls, ModelDirective, ModelEvidence, ModelFinishReason, ModelAdapter, ModelAdapterContext, ContextContributor, ContextMutationOptions, ContextSnapshot, ModelCall, ModelCallTool, ModelOutputBlock, PromptContentPart, PromptItem, ModelConfigurationContributor, ModelConfigurationInstruction, ModelConfigurationMutationOptions, ModelConfigurationSnapshot, ModelConfigurationTool, ModelRequest, ModelToolCall, ModelUsage, } from "./types/model.js";
|
|
8
|
-
export type { BoundMiddleware, StepInput, StepMiddleware, StepRequest, StepResponse, } from "./types/middleware.js";
|
|
9
|
-
export type { BuildDiagnostic, ContextItem, JsonObject, JsonPrimitive, JsonValue, ObserveEvent, ObserveModelConfigurationSnapshot, ObserveModelRequested, ObserveSealedCall, ObserveToolSnapshot, Observer, Tripwire, } from "./types/shared.js";
|
|
10
|
-
export type { InteractionReply, InputCompletion, InputEvent, InputHandle, InputOptions, MessageInput, Session, SessionInput, SessionEvent, SessionOptions, SessionSnapshot, TranscriptEntry, } from "./types/session.js";
|
|
11
|
-
export type { BoundToolSchema, BoundToolDefinition, Interaction,
|
|
9
|
+
export type { BoundMiddleware, CapabilityDeclaration, CapabilityItems, CapabilityState, StepInput, StepMiddleware, StepRequest, StepResponse, } from "./types/middleware.js";
|
|
10
|
+
export type { BuildDiagnostic, ContextItem, DeferredOutcome, JsonObject, JsonPrimitive, JsonValue, ObserveEvent, ObserveModelConfigurationSnapshot, ObserveModelRequested, ObserveSealedCall, ObserveToolSnapshot, Observer, Tripwire, } from "./types/shared.js";
|
|
11
|
+
export type { ActiveExecutionRecord, ActiveInteractionExecutionRecord, ActiveModelExecutionRecord, ActiveToolCallRecord, ActiveToolsExecutionRecord, InteractionReply, InputCompletion, InputEvent, InputHandle, InputOptions, MessageInput, Session, SessionInput, SessionIdentity, SessionEvent, SessionOptions, SessionRecord, SessionRecorder, SessionRunOptions, SessionSeed, SeededSessionOptions, SessionSnapshot, TranscriptEntry, } from "./types/session.js";
|
|
12
|
+
export type { BoundToolSchema, BoundToolDefinition, Interaction, RequiredInteraction, SealedToolCall, ToolContent, ToolDefinition, ToolExecutionContext, ToolExecutionResume, ToolObjectSchema, ToolOwner, ToolOutcome, ToolResult, } from "./types/tool.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError } from "./build/builder.js";
|
|
1
|
+
export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError, BoundAgentBuilder, } from "./build/builder.js";
|
|
2
2
|
export { HarnessError, isHarnessError } from "./errors.js";
|
|
3
3
|
export { BuiltAgent } from "./build/agent.js";
|
|
4
|
-
export {
|
|
4
|
+
export { middleware, model, tool } from "./build/helpers.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BoundMiddleware } from "../types/middleware.js";
|
|
2
|
+
import type { SessionIdentity } from "../types/session.js";
|
|
3
|
+
import type { ObserveEmit } from "../utils/observe.js";
|
|
4
|
+
/** Owns declared, process-local state for exactly one Session. */
|
|
5
|
+
export declare class CapabilityStateRegistry {
|
|
6
|
+
#private;
|
|
7
|
+
private readonly session;
|
|
8
|
+
private readonly observe;
|
|
9
|
+
constructor(middleware: readonly BoundMiddleware[], session: SessionIdentity, observe: ObserveEmit);
|
|
10
|
+
has(id: string): boolean;
|
|
11
|
+
get(id: string): Promise<unknown>;
|
|
12
|
+
abort(reason: unknown): void;
|
|
13
|
+
shutdown(reason: unknown): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { HarnessError } from "../errors.js";
|
|
2
|
+
/** Owns declared, process-local state for exactly one Session. */
|
|
3
|
+
export class CapabilityStateRegistry {
|
|
4
|
+
session;
|
|
5
|
+
observe;
|
|
6
|
+
#entries = new Map();
|
|
7
|
+
#controller = new AbortController();
|
|
8
|
+
#shutdown;
|
|
9
|
+
constructor(middleware, session, observe) {
|
|
10
|
+
this.session = session;
|
|
11
|
+
this.observe = observe;
|
|
12
|
+
for (const middlewareEntry of middleware) {
|
|
13
|
+
if (middlewareEntry.state)
|
|
14
|
+
this.#entries.set(middlewareEntry.id, {
|
|
15
|
+
id: middlewareEntry.id,
|
|
16
|
+
state: middlewareEntry.state,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
has(id) {
|
|
21
|
+
return this.#entries.has(id);
|
|
22
|
+
}
|
|
23
|
+
get(id) {
|
|
24
|
+
const entry = this.#entries.get(id);
|
|
25
|
+
if (!entry)
|
|
26
|
+
return Promise.reject(new HarnessError("capability.state.undeclared", `Capability '${id}' did not declare session state`));
|
|
27
|
+
if (!entry.promise) {
|
|
28
|
+
entry.promise = Promise.resolve()
|
|
29
|
+
.then(() => entry.state.create(this.session, this.#controller.signal))
|
|
30
|
+
.then((value) => {
|
|
31
|
+
entry.value = value;
|
|
32
|
+
entry.created = true;
|
|
33
|
+
return value;
|
|
34
|
+
}, (cause) => {
|
|
35
|
+
throw new HarnessError("capability.state.create-failed", `Capability '${id}' failed to create session state`, { cause });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return entry.promise;
|
|
39
|
+
}
|
|
40
|
+
abort(reason) {
|
|
41
|
+
this.#controller.abort(reason);
|
|
42
|
+
}
|
|
43
|
+
shutdown(reason) {
|
|
44
|
+
if (this.#shutdown)
|
|
45
|
+
return this.#shutdown;
|
|
46
|
+
this.abort(reason);
|
|
47
|
+
this.#shutdown = (async () => {
|
|
48
|
+
const created = [...this.#entries.values()].filter((entry) => entry.promise);
|
|
49
|
+
await Promise.allSettled(created.map((entry) => entry.promise));
|
|
50
|
+
for (const entry of [...created].reverse()) {
|
|
51
|
+
if (!entry.created || !entry.state.dispose)
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
await entry.state.dispose(entry.value);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.observe({
|
|
58
|
+
type: "capability.state.dispose.failed",
|
|
59
|
+
capabilityId: entry.id,
|
|
60
|
+
attributes: { message: error instanceof Error ? error.message : String(error) },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
return this.#shutdown;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import type { InputEvent, InputOptions } from "../types/session.js";
|
|
2
2
|
import { SubmissionStream } from "./submission-stream.js";
|
|
3
|
+
export type WorkEvent = InputEvent | {
|
|
4
|
+
readonly kind: "continue";
|
|
5
|
+
};
|
|
3
6
|
export interface QueuedInput {
|
|
4
|
-
readonly event:
|
|
7
|
+
readonly event: WorkEvent;
|
|
5
8
|
readonly options?: InputOptions;
|
|
6
9
|
readonly stream: SubmissionStream;
|
|
7
10
|
cancelled: boolean;
|
|
8
11
|
}
|
|
12
|
+
export type QueuedInterrupt = Omit<QueuedInput, "event"> & {
|
|
13
|
+
readonly event: Extract<InputEvent, {
|
|
14
|
+
kind: "interrupt";
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
9
17
|
export interface QueueAbortHandlers {
|
|
10
18
|
readonly isActive: () => boolean;
|
|
11
19
|
readonly abortActive: (reason: unknown) => void;
|
|
@@ -15,13 +23,14 @@ export declare function isInteractionReply(event: InputEvent): event is Extract<
|
|
|
15
23
|
kind: "approve" | "respond";
|
|
16
24
|
}>;
|
|
17
25
|
export declare function snapshotInput(event: InputEvent): InputEvent;
|
|
26
|
+
export declare function snapshotWork(event: WorkEvent): WorkEvent;
|
|
18
27
|
/** Serializes ordinary input while allowing a matching interaction reply to resume immediately. */
|
|
19
28
|
export declare class InputQueue {
|
|
20
29
|
private readonly values;
|
|
21
30
|
get size(): number;
|
|
22
31
|
add(input: QueuedInput, priority?: boolean): void;
|
|
23
32
|
take(waitingForInteraction: boolean): QueuedInput | undefined;
|
|
24
|
-
takeInterrupts():
|
|
33
|
+
takeInterrupts(): QueuedInterrupt[];
|
|
25
34
|
remove(input: QueuedInput): boolean;
|
|
26
35
|
drain(): readonly QueuedInput[];
|
|
27
36
|
}
|
|
@@ -18,6 +18,9 @@ export function snapshotInput(event) {
|
|
|
18
18
|
return Object.freeze({ ...event, value: copyJson(event.value) });
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
export function snapshotWork(event) {
|
|
22
|
+
return event.kind === "continue" ? Object.freeze({ kind: "continue" }) : snapshotInput(event);
|
|
23
|
+
}
|
|
21
24
|
/** Serializes ordinary input while allowing a matching interaction reply to resume immediately. */
|
|
22
25
|
export class InputQueue {
|
|
23
26
|
values = [];
|
|
@@ -32,7 +35,8 @@ export class InputQueue {
|
|
|
32
35
|
}
|
|
33
36
|
take(waitingForInteraction) {
|
|
34
37
|
const next = this.values[0];
|
|
35
|
-
if (!next ||
|
|
38
|
+
if (!next ||
|
|
39
|
+
(waitingForInteraction && (next.event.kind === "continue" || !isInteractionReply(next.event))))
|
|
36
40
|
return undefined;
|
|
37
41
|
return this.values.shift();
|
|
38
42
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ActiveExecutionRecord, SessionRecord, SessionSnapshot } from "../types/session.js";
|
|
2
|
+
import type { JsonObject } from "../types/shared.js";
|
|
3
|
+
export declare function sessionRecord(input: {
|
|
4
|
+
readonly state: SessionSnapshot;
|
|
5
|
+
readonly transition: SessionRecord["transition"];
|
|
6
|
+
readonly session: Readonly<{
|
|
7
|
+
readonly userId?: string;
|
|
8
|
+
readonly context?: JsonObject;
|
|
9
|
+
}>;
|
|
10
|
+
readonly active?: ActiveExecutionRecord;
|
|
11
|
+
}): SessionRecord;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { copyJson } from "../utils/immutable.js";
|
|
2
|
+
export function sessionRecord(input) {
|
|
3
|
+
const value = {
|
|
4
|
+
version: 1,
|
|
5
|
+
revision: input.state.revision,
|
|
6
|
+
transition: input.transition,
|
|
7
|
+
session: {
|
|
8
|
+
id: input.state.id,
|
|
9
|
+
...(input.session.userId === undefined ? {} : { userId: input.session.userId }),
|
|
10
|
+
...(input.session.context === undefined ? {} : { context: copyJson(input.session.context) }),
|
|
11
|
+
turnCount: input.state.turnCount,
|
|
12
|
+
status: input.state.status,
|
|
13
|
+
},
|
|
14
|
+
transcript: copyJson(input.state.transcript),
|
|
15
|
+
...(input.active === undefined ? {} : { active: copyJson(input.active) }),
|
|
16
|
+
};
|
|
17
|
+
return deepFreeze(value);
|
|
18
|
+
}
|
|
19
|
+
function deepFreeze(value, seen = new WeakSet()) {
|
|
20
|
+
if (!value || typeof value !== "object" || seen.has(value))
|
|
21
|
+
return value;
|
|
22
|
+
seen.add(value);
|
|
23
|
+
for (const child of Object.values(value))
|
|
24
|
+
deepFreeze(child, seen);
|
|
25
|
+
return Object.freeze(value);
|
|
26
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { InputEvent, InputOptions, SessionSnapshot } from "../types/session.js";
|
|
1
|
+
import type { InputEvent, InputOptions, SessionRecorder, SessionSnapshot } from "../types/session.js";
|
|
2
2
|
import type { Observer } from "../types/shared.js";
|
|
3
3
|
import { SessionEventLog } from "./event-log.js";
|
|
4
4
|
import { SubmissionStream } from "./submission-stream.js";
|
|
5
5
|
import { type LoopAgent } from "../build/agent.js";
|
|
6
|
+
import type { NormalizedSessionSeed } from "./seed.js";
|
|
6
7
|
/** Coordinates one active turn at a time; TurnRunner owns the turn's internal state machine. */
|
|
7
8
|
export declare class SessionScheduler {
|
|
8
9
|
readonly id: string;
|
|
@@ -16,18 +17,29 @@ export declare class SessionScheduler {
|
|
|
16
17
|
private pending?;
|
|
17
18
|
private inFlightPlan?;
|
|
18
19
|
private running;
|
|
20
|
+
private stopping;
|
|
19
21
|
private stopped;
|
|
22
|
+
private suspended;
|
|
20
23
|
private generation;
|
|
21
24
|
private stopPromise?;
|
|
25
|
+
private recordFailure?;
|
|
22
26
|
private readonly observers;
|
|
23
27
|
private readonly turns;
|
|
28
|
+
private readonly states;
|
|
24
29
|
constructor(id: string, agent: LoopAgent, session: Readonly<{
|
|
25
30
|
readonly userId?: string;
|
|
26
31
|
readonly context?: import("../types/shared.js").JsonObject;
|
|
27
|
-
}
|
|
32
|
+
}>, options?: {
|
|
33
|
+
readonly seed?: NormalizedSessionSeed;
|
|
34
|
+
readonly recorder?: SessionRecorder;
|
|
35
|
+
});
|
|
36
|
+
private readonly session;
|
|
37
|
+
private readonly recorder?;
|
|
28
38
|
get snapshot(): SessionSnapshot;
|
|
29
39
|
observe(listener: Observer): () => void;
|
|
30
40
|
submit(event: InputEvent, options?: InputOptions): SubmissionStream;
|
|
41
|
+
continue(options?: InputOptions): SubmissionStream;
|
|
42
|
+
private submitWork;
|
|
31
43
|
stop(reason?: string): Promise<void>;
|
|
32
44
|
private enqueueReply;
|
|
33
45
|
private enqueueOrdinaryInput;
|
|
@@ -40,9 +52,11 @@ export declare class SessionScheduler {
|
|
|
40
52
|
private run;
|
|
41
53
|
private resumeTurn;
|
|
42
54
|
private applyTurnOutcome;
|
|
43
|
-
private beginStop;
|
|
44
55
|
private emitObserve;
|
|
45
56
|
private commitCancelledPlan;
|
|
46
57
|
private claimInterrupts;
|
|
58
|
+
private commit;
|
|
59
|
+
private assertRecordable;
|
|
60
|
+
private failRecording;
|
|
47
61
|
private assertCurrent;
|
|
48
62
|
}
|