@theoremjs/react 0.3.0 → 0.3.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/README.md +728 -202
- package/client/composer-hints.js +1 -1
- package/client/decision-images.d.ts +76 -0
- package/client/decision-images.js +139 -0
- package/client/decision-transport.d.ts +26 -4
- package/client/decision-transport.js +79 -6
- package/client/embedding-input.d.ts +84 -0
- package/client/embedding-input.js +194 -0
- package/client/embedding-space.d.ts +82 -0
- package/client/embedding-space.js +389 -0
- package/client/embedding-transport.d.ts +40 -0
- package/client/embedding-transport.js +71 -0
- package/client/failure.d.ts +7 -10
- package/client/failure.js +9 -9
- package/client/host-call.d.ts +1 -1
- package/client/host-call.js +7 -2
- package/client/host-transport.d.ts +2 -0
- package/client/host-transport.js +3 -2
- package/client/index.d.ts +19 -11
- package/client/index.js +14 -10
- package/client/ink-waveform.d.ts +1 -1
- package/client/ink-waveform.js +1 -2
- package/client/live/apply-live-turn-tool-event.js +1 -1
- package/client/live/live-captions.d.ts +30 -0
- package/client/live/live-captions.js +105 -1
- package/client/live/live-mic-forward.d.ts +1 -0
- package/client/live/live-mic-forward.js +11 -2
- package/client/live/live-page-tool.d.ts +34 -0
- package/client/live/live-page-tool.js +11 -0
- package/client/live/live-state.d.ts +4 -2
- package/client/live/live-state.js +2 -0
- package/client/live/live-video.js +1 -1
- package/client/live/run-live-tool-call.d.ts +7 -2
- package/client/live/run-live-tool-call.js +48 -10
- package/client/live/use-live-caption-log.d.ts +17 -0
- package/client/live/use-live-caption-log.js +51 -0
- package/client/live-client.d.ts +52 -4
- package/client/live-client.js +250 -59
- package/client/live-messages.d.ts +51 -13
- package/client/live-messages.js +42 -9
- package/client/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/run-commit.d.ts +16 -43
- package/client/run-commit.js +26 -101
- package/client/run-session.d.ts +40 -41
- package/client/run-session.js +104 -112
- package/client/schema-fields.js +27 -6
- package/client/shaped-data.js +37 -8
- package/client/source-citations.js +1 -1
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -0
- package/client/state-fields.js +1 -1
- package/client/tool-result.js +30 -12
- package/client/tool-resume.d.ts +18 -1
- package/client/tool-resume.js +4 -1
- package/client/trace-feed.d.ts +1 -1
- package/client/trace-story.d.ts +2 -2
- package/client/trace-story.js +43 -19
- package/client/trace-view.d.ts +2 -2
- package/client/trace-view.js +56 -16
- package/client/transcript-groups.d.ts +28 -2
- package/client/transcript-groups.js +63 -13
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transport.d.ts +114 -26
- package/client/transport.js +106 -32
- package/client/turn-client.d.ts +17 -8
- package/client/turn-client.js +26 -31
- package/client/voice-recorder.d.ts +1 -1
- package/client/voice-recorder.js +2 -5
- package/client/wire-line.js +5 -5
- package/client/worklets/mic-capture.js +9 -9
- package/components/InkWaveform.d.ts +12 -1
- package/components/InkWaveform.js +65 -42
- package/components/live/use-live-runner-controls.d.ts +3 -4
- package/components/live/use-live-runner-controls.js +6 -10
- package/components/live/use-live-runner-model.d.ts +10 -6
- package/components/live/use-live-runner-model.js +29 -14
- package/components/live/use-live-runner-ui.d.ts +10 -8
- package/components/live/use-live-runner-ui.js +19 -11
- package/components/live/use-live-session-client.d.ts +4 -4
- package/components/live/use-live-session-client.js +11 -8
- package/components/use-composer-voice.d.ts +4 -4
- package/components/use-composer-voice.js +8 -8
- package/components/use-editor-selection.js +4 -1
- package/components/use-voice-playback.d.ts +2 -1
- package/components/use-voice-playback.js +4 -4
- package/hooks/use-described.d.ts +5 -1
- package/hooks/use-described.js +11 -9
- package/hooks/use-embedding-index.d.ts +27 -0
- package/hooks/use-embedding-index.js +43 -0
- package/hooks/use-one-call.d.ts +31 -0
- package/hooks/use-one-call.js +76 -0
- package/hooks/use-page-tool-warnings.d.ts +6 -0
- package/hooks/use-page-tool-warnings.js +25 -0
- package/hooks/use-second-ticker.d.ts +2 -0
- package/hooks/use-second-ticker.js +13 -0
- package/hooks/use-theorem-chat-actions.d.ts +25 -21
- package/hooks/use-theorem-chat-actions.js +67 -45
- package/hooks/use-theorem-chat-state.d.ts +6 -6
- package/hooks/use-theorem-chat-state.js +13 -9
- package/hooks/use-theorem-chat.d.ts +48 -14
- package/hooks/use-theorem-chat.js +329 -141
- package/hooks/use-theorem-decision.d.ts +5 -10
- package/hooks/use-theorem-decision.js +4 -67
- package/hooks/use-theorem-embedding.d.ts +18 -0
- package/hooks/use-theorem-embedding.js +12 -0
- package/hooks/use-theorem-host.d.ts +5 -4
- package/hooks/use-theorem-host.js +26 -8
- package/hooks/use-theorem-interface.d.ts +6 -3
- package/hooks/use-theorem-interface.js +12 -22
- package/index.d.ts +6 -3
- package/index.js +5 -2
- package/live.d.ts +1 -0
- package/live.js +1 -1
- package/package.json +6 -5
- package/server/checkpoint-ownership.d.ts +6 -0
- package/server/checkpoint-ownership.js +42 -0
- package/server/conversation-store.d.ts +92 -0
- package/server/conversation-store.js +131 -0
- package/server/conversation.d.ts +193 -0
- package/server/conversation.js +552 -0
- package/server/decision-handler.d.ts +12 -4
- package/server/decision-handler.js +29 -38
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +58 -20
- package/server/handler.js +441 -238
- package/server/mod.d.ts +15 -6
- package/server/mod.js +13 -5
- package/server/page-dispatch.d.ts +22 -0
- package/server/page-dispatch.js +127 -0
- package/server/request-body.d.ts +2 -0
- package/server/request-body.js +42 -0
- package/server/request-check.d.ts +6 -1
- package/server/request-check.js +27 -12
- package/server/session-authority.d.ts +8 -0
- package/server/session-authority.js +78 -0
- package/server/session-boundary.d.ts +26 -0
- package/server/session-boundary.js +64 -0
- package/server/session-identity.d.ts +16 -0
- package/server/session-identity.js +41 -0
- package/server/session-limits.d.ts +12 -0
- package/server/session-limits.js +59 -0
- package/server/session-store.d.ts +69 -20
- package/server/session-store.js +220 -5
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.js +1 -1
- package/server/tool-operations.d.ts +6 -0
- package/server/tool-operations.js +132 -0
- package/server/turn-input.d.ts +10 -0
- package/server/turn-input.js +29 -0
- package/styles/ink-controls.css +78 -78
- package/ui/CardWall.d.ts +10 -0
- package/ui/CardWall.js +61 -0
- package/ui/ChatComposerBar.d.ts +28 -5
- package/ui/ChatComposerBar.js +85 -30
- package/ui/ChatTranscript.d.ts +14 -1
- package/ui/ChatTranscript.js +110 -57
- package/ui/ConsoleFrame.d.ts +61 -4
- package/ui/ConsoleFrame.js +109 -9
- package/ui/DecisionAnswers.d.ts +40 -2
- package/ui/DecisionAnswers.js +166 -35
- package/ui/DecisionImages.d.ts +16 -0
- package/ui/DecisionImages.js +46 -0
- package/ui/EmbeddingDocuments.d.ts +78 -0
- package/ui/EmbeddingDocuments.js +278 -0
- package/ui/EmbeddingQuery.d.ts +27 -0
- package/ui/EmbeddingQuery.js +95 -0
- package/ui/EmbeddingResults.d.ts +41 -0
- package/ui/EmbeddingResults.js +165 -0
- package/ui/EmbeddingSpace.d.ts +26 -0
- package/ui/EmbeddingSpace.js +106 -0
- package/ui/LiveCaptionsPanel.d.ts +22 -0
- package/ui/LiveCaptionsPanel.js +27 -0
- package/ui/LiveRunner.d.ts +6 -4
- package/ui/LiveRunner.js +42 -55
- package/ui/PaneState.d.ts +33 -0
- package/ui/PaneState.js +50 -0
- package/ui/SchemaFields.d.ts +1 -1
- package/ui/SchemaFields.js +54 -26
- package/ui/ShapedData.js +73 -28
- package/ui/SidePanel.d.ts +74 -5
- package/ui/SidePanel.js +82 -9
- package/ui/StateFields.d.ts +10 -2
- package/ui/StateFields.js +56 -13
- package/ui/TheoremChat.d.ts +65 -14
- package/ui/TheoremChat.js +66 -78
- package/ui/TheoremDecision.d.ts +12 -20
- package/ui/TheoremDecision.js +78 -35
- package/ui/TheoremEmbedding.d.ts +27 -0
- package/ui/TheoremEmbedding.js +122 -0
- package/ui/TheoremHost.d.ts +22 -5
- package/ui/TheoremHost.js +127 -37
- package/ui/ToolGateCard.d.ts +5 -1
- package/ui/ToolGateCard.js +30 -23
- package/ui/ToolResult.js +12 -10
- package/ui/ToolResultCharts.js +29 -12
- package/ui/TraceCharts.d.ts +2 -2
- package/ui/TraceCharts.js +48 -17
- package/ui/TraceGuardrails.d.ts +3 -2
- package/ui/TraceGuardrails.js +24 -6
- package/ui/TraceInspector.d.ts +11 -1
- package/ui/TraceInspector.js +45 -28
- package/ui/TraceInspectorPanel.d.ts +31 -9
- package/ui/TraceInspectorPanel.js +77 -39
- package/ui/TraceOverview.d.ts +7 -3
- package/ui/TraceOverview.js +44 -14
- package/ui/TraceSpanDetail.js +24 -22
- package/ui/TraceStory.js +9 -8
- package/ui/TraceValues.js +19 -9
- package/ui/TraceWaterfall.js +12 -9
- package/ui/VoiceNote.js +4 -4
- package/ui/approval-heading.d.ts +4 -0
- package/ui/approval-heading.js +9 -0
- package/ui/arrive.d.ts +18 -0
- package/ui/arrive.js +46 -0
- package/ui/built/theme.css +86 -68
- package/ui/built/theme.css.d.ts +9 -0
- package/ui/built/theorem.d.ts +3 -3
- package/ui/built/theorem.js +34 -5
- package/ui/built/theorem.variants.d.ts +8 -2
- package/ui/code-language.d.ts +12 -0
- package/ui/code-language.js +50 -0
- package/ui/disclosure-motion.js +53 -9
- package/ui/embedding-space-gl.d.ts +37 -0
- package/ui/embedding-space-gl.js +663 -0
- package/ui/embedding-space-view.d.ts +8 -0
- package/ui/embedding-space-view.js +8 -0
- package/ui/glide.d.ts +7 -0
- package/ui/glide.js +44 -0
- package/ui/icons.js +2 -1
- package/ui/index.d.ts +20 -9
- package/ui/index.js +20 -10
- package/ui/labels-provider.js +5 -2
- package/ui/labels.d.ts +374 -85
- package/ui/labels.js +1028 -188
- package/ui/markdown-plugins.d.ts +9 -0
- package/ui/markdown-plugins.js +13 -0
- package/ui/mcp-icon.d.ts +6 -0
- package/ui/mcp-icon.js +8 -0
- package/ui/row-keys.d.ts +19 -0
- package/ui/row-keys.js +33 -0
- package/ui/theme.d.ts +1 -1
- package/ui/theme.js +2 -2
- package/ui/theorem-theme.js +32 -10
- package/ui/trace-palette.d.ts +2 -16
- package/ui/trace-palette.js +2 -16
- package/ui/trace-patterns.d.ts +11 -3
- package/ui/trace-patterns.js +16 -3
- package/ui/transcript-copy-text.js +1 -0
- package/ui/wall-columns.d.ts +8 -0
- package/ui/wall-columns.js +19 -0
- package/client/walk-away.d.ts +0 -34
- package/client/walk-away.js +0 -48
- package/server/walk-away.d.ts +0 -22
- package/server/walk-away.js +0 -41
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
+
import { z } from '@theoremjs/agents';
|
|
11
12
|
import type { ToolGateAuth } from '@theoremjs/agents/interface';
|
|
12
|
-
import type { ToolGate, ToolId,
|
|
13
|
+
import type { AnsweredGate, ToolGate, ToolId, ToolOperationIdentity, ToolOperationOutcome, ToolOperationUncertainty, TurnInput, TurnToolSnapshot } from '@theoremjs/agents/kernel';
|
|
14
|
+
import { type TheoremSessionLimits } from './session-limits.ts';
|
|
13
15
|
/** A tool call the kernel paused on a gate, as the server saw it. */
|
|
14
16
|
export type PendingToolGate = {
|
|
17
|
+
profileId: string;
|
|
18
|
+
operationId: string;
|
|
19
|
+
callId: string;
|
|
15
20
|
name: string;
|
|
16
21
|
/** The model's arguments; an approval runs the call with exactly these. */
|
|
17
22
|
arguments: Record<string, unknown>;
|
|
@@ -24,51 +29,95 @@ export type PendingToolGate = {
|
|
|
24
29
|
turnInput: TurnInput;
|
|
25
30
|
model?: string;
|
|
26
31
|
createdAt: number;
|
|
32
|
+
expiresAt?: number;
|
|
27
33
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
export type SettledToolGate = {
|
|
34
|
-
event: TurnEventOf<'tool'>;
|
|
35
|
-
/** When the call paused: a settled call is kept as long as its gate would have waited. */
|
|
36
|
-
createdAt: number;
|
|
34
|
+
export type SessionPageGrant = {
|
|
35
|
+
grantId: string;
|
|
36
|
+
expiresAt: number;
|
|
37
|
+
resultHash?: string;
|
|
37
38
|
};
|
|
39
|
+
export type SessionToolOperation = {
|
|
40
|
+
status: 'ready';
|
|
41
|
+
profileId: string;
|
|
42
|
+
pending: PendingToolGate;
|
|
43
|
+
answer: Omit<AnsweredGate, 'typed'>;
|
|
44
|
+
readyAt: number;
|
|
45
|
+
} | (ToolOperationIdentity & {
|
|
46
|
+
status: 'running';
|
|
47
|
+
claimToken: string;
|
|
48
|
+
startedAt: number;
|
|
49
|
+
pageGrant?: SessionPageGrant;
|
|
50
|
+
}) | (ToolOperationIdentity & {
|
|
51
|
+
status: 'settled';
|
|
52
|
+
pageGrant?: SessionPageGrant;
|
|
53
|
+
outcome: ToolOperationOutcome;
|
|
54
|
+
settledAt: number;
|
|
55
|
+
retainUntil?: number;
|
|
56
|
+
}) | (ToolOperationIdentity & {
|
|
57
|
+
status: 'uncertain';
|
|
58
|
+
pageGrant?: SessionPageGrant;
|
|
59
|
+
claimToken: string;
|
|
60
|
+
reason: ToolOperationUncertainty;
|
|
61
|
+
detectedAt: number;
|
|
62
|
+
});
|
|
63
|
+
export declare const DEFAULT_OPERATION_RETENTION_MS: number;
|
|
38
64
|
export type TheoremSessionState = {
|
|
65
|
+
version: 3;
|
|
66
|
+
operations: Record<string, SessionToolOperation>;
|
|
39
67
|
/** Tool ids the user allowed for the rest of this session. */
|
|
40
68
|
permissions: string[];
|
|
41
|
-
/**
|
|
69
|
+
/** invariant: Operation IDs index issued gates; both IDs must match an answer. */
|
|
42
70
|
gates: Record<string, PendingToolGate>;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
/** Checkpoint identities this session produced, newest last. */
|
|
72
|
+
providerCheckpoints: string[];
|
|
73
|
+
};
|
|
74
|
+
/** Stored authority is checked before the handler reads or changes it. */
|
|
75
|
+
export declare const theoremSessionStateSchema: z.ZodType<TheoremSessionState>;
|
|
76
|
+
export interface SessionSnapshot {
|
|
77
|
+
revision: string;
|
|
78
|
+
state: TheoremSessionState;
|
|
79
|
+
}
|
|
80
|
+
export type SessionWriteResult = {
|
|
81
|
+
written: true;
|
|
82
|
+
revision: string;
|
|
83
|
+
} | {
|
|
84
|
+
written: false;
|
|
47
85
|
};
|
|
48
86
|
/**
|
|
49
87
|
* Where session state lives. The default is process memory — use a shared
|
|
50
88
|
* store (KV, Redis, a database row) when requests can reach different instances.
|
|
51
89
|
*/
|
|
52
90
|
export interface TheoremSessionStore {
|
|
53
|
-
load(sessionId: string):
|
|
54
|
-
|
|
91
|
+
load(sessionId: string): Promise<SessionSnapshot | undefined>;
|
|
92
|
+
/** Compare and replace atomically; null creates only an absent session. */
|
|
93
|
+
compareAndSwap(sessionId: string, expectedRevision: string | null, next: TheoremSessionState): Promise<SessionWriteResult>;
|
|
55
94
|
}
|
|
56
95
|
export declare function emptySessionState(): TheoremSessionState;
|
|
57
96
|
/** Drop gates (waiting or settled) older than `ttlMs`. */
|
|
58
97
|
export declare function pruneGates<Gate extends {
|
|
59
98
|
createdAt: number;
|
|
99
|
+
expiresAt?: number;
|
|
60
100
|
}>(gates: Record<string, Gate>, now: number, ttlMs: number): Record<string, Gate>;
|
|
61
101
|
export type MemorySessionStoreOptions = {
|
|
62
|
-
/**
|
|
102
|
+
/** Finite authority storage bounds. */
|
|
103
|
+
sessionLimits?: Partial<TheoremSessionLimits>;
|
|
104
|
+
/** invariant: Idle retention is in milliseconds. Protected authority remains. Default 24h. */
|
|
63
105
|
ttlMs?: number;
|
|
64
106
|
/** Oldest sessions are evicted past this count. Default 10 000. */
|
|
65
107
|
maxSessions?: number;
|
|
66
108
|
};
|
|
67
|
-
|
|
109
|
+
export declare function memoryStoreLimits(options: MemorySessionStoreOptions): {
|
|
110
|
+
ttlMs: number;
|
|
111
|
+
maxSessions: number;
|
|
112
|
+
};
|
|
113
|
+
/** A value kept per session id for the separate credential store. */
|
|
68
114
|
export type PerSessionStore<T> = {
|
|
69
115
|
load(sessionId: string): T | undefined | Promise<T | undefined>;
|
|
70
116
|
save(sessionId: string, value: T): void | Promise<void>;
|
|
71
117
|
};
|
|
72
118
|
/** A per-session value kept in process memory: forgotten when idle, oldest evicted first. */
|
|
73
|
-
export declare function createMemorySessionMap<T>(options?: MemorySessionStoreOptions):
|
|
119
|
+
export declare function createMemorySessionMap<T>(options?: MemorySessionStoreOptions): {
|
|
120
|
+
load(sessionId: string): T | undefined;
|
|
121
|
+
save(sessionId: string, value: T): void;
|
|
122
|
+
};
|
|
74
123
|
export declare function createMemorySessionStore(options?: MemorySessionStoreOptions): TheoremSessionStore;
|
package/server/session-store.js
CHANGED
|
@@ -8,18 +8,191 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
+
import { jsonObjectSchema, jsonValueSchema, labelText, savedToolInvocationSchema, TheoremError, TOOL_AUTH_TYPES, TOOL_GATE_KINDS, TOOL_PERMISSION, toolOperationOutcomeSchema, turnHistoryMessageSchema, turnToolSnapshotSchema, z, } from '@theoremjs/agents';
|
|
11
12
|
import { gateExpired } from '@theoremjs/agents/kernel';
|
|
13
|
+
import { checkSessionLimits, resolveSessionLimits, } from "./session-limits.js";
|
|
14
|
+
export const DEFAULT_OPERATION_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
const blobSchema = z.strictObject({
|
|
16
|
+
data: z.string(),
|
|
17
|
+
mimeType: z.string(),
|
|
18
|
+
name: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
const inputSchema = z.strictObject({
|
|
21
|
+
text: z.string().optional(),
|
|
22
|
+
role: z.string().optional(),
|
|
23
|
+
slots: z.record(z.string(), z.string()).optional(),
|
|
24
|
+
context: z
|
|
25
|
+
.strictObject({ client: jsonValueSchema.optional(), server: jsonValueSchema.optional() })
|
|
26
|
+
.optional(),
|
|
27
|
+
attachments: z
|
|
28
|
+
.array(z.union([
|
|
29
|
+
blobSchema,
|
|
30
|
+
z.strictObject({
|
|
31
|
+
uri: z.string(),
|
|
32
|
+
mimeType: z.string(),
|
|
33
|
+
name: z.string().optional(),
|
|
34
|
+
}),
|
|
35
|
+
]))
|
|
36
|
+
.optional(),
|
|
37
|
+
voice: z.array(blobSchema).optional(),
|
|
38
|
+
history: z.array(turnHistoryMessageSchema).optional(),
|
|
39
|
+
repair: z
|
|
40
|
+
.strictObject({ previousOutput: z.string(), rejection: z.string(), guidance: z.string() })
|
|
41
|
+
.optional(),
|
|
42
|
+
historyTokens: z.number().nonnegative().optional(),
|
|
43
|
+
inputTokens: z.number().nonnegative().optional(),
|
|
44
|
+
});
|
|
45
|
+
const pendingSchema = z.strictObject({
|
|
46
|
+
profileId: z.string().min(1),
|
|
47
|
+
operationId: z.string().min(1).max(256),
|
|
48
|
+
callId: z.string().min(1),
|
|
49
|
+
name: z.string().min(1),
|
|
50
|
+
arguments: jsonObjectSchema,
|
|
51
|
+
gate: z
|
|
52
|
+
.strictObject({
|
|
53
|
+
kind: z.enum(TOOL_GATE_KINDS),
|
|
54
|
+
permission: z.enum(TOOL_PERMISSION).optional(),
|
|
55
|
+
auth: z
|
|
56
|
+
.strictObject({
|
|
57
|
+
slot: z.string().min(1),
|
|
58
|
+
authType: z.enum(TOOL_AUTH_TYPES),
|
|
59
|
+
service: z.string().min(1),
|
|
60
|
+
})
|
|
61
|
+
.optional(),
|
|
62
|
+
})
|
|
63
|
+
.superRefine((gate, ctx) => {
|
|
64
|
+
if ((gate.kind === 'auth') !== (gate.auth !== undefined)) {
|
|
65
|
+
ctx.addIssue({ code: 'custom', path: ['auth'] });
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
68
|
+
snapshot: turnToolSnapshotSchema.optional(),
|
|
69
|
+
promoted: z.array(z.string().min(1)),
|
|
70
|
+
turnInput: inputSchema,
|
|
71
|
+
model: z.string().optional(),
|
|
72
|
+
createdAt: z.number().finite().nonnegative(),
|
|
73
|
+
expiresAt: z.number().finite().nonnegative().optional(),
|
|
74
|
+
});
|
|
75
|
+
const acceptedAnswerSchema = z.strictObject({
|
|
76
|
+
input: jsonObjectSchema,
|
|
77
|
+
resume: z.strictObject({
|
|
78
|
+
granted: z.boolean().optional(),
|
|
79
|
+
gateKind: z.enum(TOOL_GATE_KINDS).optional(),
|
|
80
|
+
cause: z.enum(['declined', 'abandoned', 'expired']).optional(),
|
|
81
|
+
signIn: z.boolean().optional(),
|
|
82
|
+
edited: z.strictObject({ from: jsonObjectSchema }).optional(),
|
|
83
|
+
}),
|
|
84
|
+
sessionPermissions: z.array(z.string().min(1)),
|
|
85
|
+
page: z
|
|
86
|
+
.union([
|
|
87
|
+
z.strictObject({ output: jsonValueSchema }),
|
|
88
|
+
z.strictObject({ timedOut: z.literal(true) }),
|
|
89
|
+
z.strictObject({ unanswered: z.literal(true) }),
|
|
90
|
+
])
|
|
91
|
+
.optional(),
|
|
92
|
+
});
|
|
93
|
+
const identityFields = {
|
|
94
|
+
operationId: z.string().min(1).max(256),
|
|
95
|
+
callId: z.string().min(1),
|
|
96
|
+
profileId: z.string().min(1),
|
|
97
|
+
toolId: z.string().min(1),
|
|
98
|
+
invocationHash: z.string().min(1),
|
|
99
|
+
invocation: savedToolInvocationSchema.optional(),
|
|
100
|
+
};
|
|
101
|
+
const pageGrant = z.strictObject({
|
|
102
|
+
grantId: z.string().min(1),
|
|
103
|
+
expiresAt: z.number().finite().positive(),
|
|
104
|
+
resultHash: z.string().optional(),
|
|
105
|
+
});
|
|
106
|
+
const operationSchema = z.discriminatedUnion('status', [
|
|
107
|
+
z.strictObject({
|
|
108
|
+
status: z.literal('ready'),
|
|
109
|
+
profileId: z.string().min(1),
|
|
110
|
+
pending: pendingSchema,
|
|
111
|
+
answer: acceptedAnswerSchema,
|
|
112
|
+
readyAt: z.number().finite().nonnegative(),
|
|
113
|
+
}),
|
|
114
|
+
z.strictObject({
|
|
115
|
+
...identityFields,
|
|
116
|
+
status: z.literal('running'),
|
|
117
|
+
pageGrant: pageGrant.optional(),
|
|
118
|
+
claimToken: z.string().min(1),
|
|
119
|
+
startedAt: z.number().finite().nonnegative(),
|
|
120
|
+
}),
|
|
121
|
+
z.strictObject({
|
|
122
|
+
...identityFields,
|
|
123
|
+
status: z.literal('settled'),
|
|
124
|
+
pageGrant: pageGrant.optional(),
|
|
125
|
+
outcome: toolOperationOutcomeSchema,
|
|
126
|
+
settledAt: z.number().finite().nonnegative(),
|
|
127
|
+
retainUntil: z.number().finite().nonnegative().optional(),
|
|
128
|
+
}),
|
|
129
|
+
z.strictObject({
|
|
130
|
+
...identityFields,
|
|
131
|
+
status: z.literal('uncertain'),
|
|
132
|
+
pageGrant: pageGrant.optional(),
|
|
133
|
+
claimToken: z.string().min(1),
|
|
134
|
+
reason: z.enum(['execution_interrupted', 'settlement_write_failed', 'worker_lost']),
|
|
135
|
+
detectedAt: z.number().finite().nonnegative(),
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
138
|
+
/** Stored authority is checked before the handler reads or changes it. */
|
|
139
|
+
export const theoremSessionStateSchema = z
|
|
140
|
+
.strictObject({
|
|
141
|
+
version: z.literal(3),
|
|
142
|
+
operations: z.record(z.string().min(1), operationSchema),
|
|
143
|
+
permissions: z.array(z.string().min(1)),
|
|
144
|
+
gates: z.record(z.string().min(1), pendingSchema),
|
|
145
|
+
providerCheckpoints: z.array(z.string().min(1)),
|
|
146
|
+
})
|
|
147
|
+
.superRefine((state, ctx) => {
|
|
148
|
+
for (const [operationId, operation] of Object.entries(state.operations)) {
|
|
149
|
+
const reference = operation.status === 'ready' ? operation.pending : operation;
|
|
150
|
+
if (reference.operationId !== operationId ||
|
|
151
|
+
(operation.status === 'ready' && operation.profileId !== operation.pending.profileId) ||
|
|
152
|
+
operationId in state.gates ||
|
|
153
|
+
(operation.status === 'settled' &&
|
|
154
|
+
(operation.outcome.event.tool.operationId !== operationId ||
|
|
155
|
+
operation.outcome.event.tool.callId !== operation.callId ||
|
|
156
|
+
operation.outcome.event.tool.name !== operation.toolId))) {
|
|
157
|
+
ctx.addIssue({ code: 'custom', path: ['operations', operationId] });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [operationId, gate] of Object.entries(state.gates)) {
|
|
161
|
+
if (operationId !== gate.operationId)
|
|
162
|
+
ctx.addIssue({ code: 'custom', path: ['gates', operationId] });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
12
165
|
export function emptySessionState() {
|
|
13
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
version: 3,
|
|
168
|
+
operations: {},
|
|
169
|
+
permissions: [],
|
|
170
|
+
gates: {},
|
|
171
|
+
providerCheckpoints: [],
|
|
172
|
+
};
|
|
14
173
|
}
|
|
15
174
|
/** Drop gates (waiting or settled) older than `ttlMs`. */
|
|
16
175
|
export function pruneGates(gates, now, ttlMs) {
|
|
17
|
-
return Object.fromEntries(Object.entries(gates).filter(([, gate]) =>
|
|
176
|
+
return Object.fromEntries(Object.entries(gates).filter(([, gate]) => gate.expiresAt !== undefined
|
|
177
|
+
? now < gate.expiresAt
|
|
178
|
+
: !gateExpired(gate.createdAt, now, ttlMs)));
|
|
18
179
|
}
|
|
19
|
-
|
|
20
|
-
export function createMemorySessionMap(options = {}) {
|
|
180
|
+
export function memoryStoreLimits(options) {
|
|
21
181
|
const ttlMs = options.ttlMs ?? 24 * 60 * 60 * 1000;
|
|
22
182
|
const maxSessions = options.maxSessions ?? 10_000;
|
|
183
|
+
if (!Number.isFinite(ttlMs) ||
|
|
184
|
+
ttlMs <= 0 ||
|
|
185
|
+
!Number.isSafeInteger(maxSessions) ||
|
|
186
|
+
maxSessions <= 0) {
|
|
187
|
+
throw new TheoremError('config', labelText('error.config'), {
|
|
188
|
+
cause: { code: 'session_store_options', ttlMs, maxSessions },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return { ttlMs, maxSessions };
|
|
192
|
+
}
|
|
193
|
+
/** A per-session value kept in process memory: forgotten when idle, oldest evicted first. */
|
|
194
|
+
export function createMemorySessionMap(options = {}) {
|
|
195
|
+
const { ttlMs, maxSessions } = memoryStoreLimits(options);
|
|
23
196
|
const sessions = new Map();
|
|
24
197
|
return {
|
|
25
198
|
load(sessionId) {
|
|
@@ -45,5 +218,47 @@ export function createMemorySessionMap(options = {}) {
|
|
|
45
218
|
};
|
|
46
219
|
}
|
|
47
220
|
export function createMemorySessionStore(options = {}) {
|
|
48
|
-
|
|
221
|
+
const { ttlMs, maxSessions } = memoryStoreLimits(options);
|
|
222
|
+
const limits = resolveSessionLimits(options.sessionLimits);
|
|
223
|
+
const sessions = new Map();
|
|
224
|
+
const held = (entry) => Object.values(entry.snapshot.state.gates).some((gate) => gate.expiresAt === undefined || Date.now() < gate.expiresAt) ||
|
|
225
|
+
Object.values(entry.snapshot.state.operations).some((operation) => operation.status !== 'settled' ||
|
|
226
|
+
Date.now() <
|
|
227
|
+
(operation.retainUntil ?? operation.settledAt + DEFAULT_OPERATION_RETENTION_MS));
|
|
228
|
+
const current = (sessionId) => {
|
|
229
|
+
const entry = sessions.get(sessionId);
|
|
230
|
+
if (entry && !held(entry) && Date.now() - entry.touched >= ttlMs) {
|
|
231
|
+
sessions.delete(sessionId);
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
return entry?.snapshot;
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
load(sessionId) {
|
|
238
|
+
return Promise.resolve(structuredClone(current(sessionId)));
|
|
239
|
+
},
|
|
240
|
+
compareAndSwap(sessionId, expectedRevision, next) {
|
|
241
|
+
const snapshot = current(sessionId);
|
|
242
|
+
if ((snapshot?.revision ?? null) !== expectedRevision)
|
|
243
|
+
return Promise.resolve({ written: false });
|
|
244
|
+
const state = theoremSessionStateSchema.parse(next);
|
|
245
|
+
checkSessionLimits(state, limits);
|
|
246
|
+
if (!snapshot && sessions.size >= maxSessions) {
|
|
247
|
+
const removable = [...sessions].find(([, entry]) => !held(entry));
|
|
248
|
+
if (!removable) {
|
|
249
|
+
throw new TheoremError('unavailable', labelText('error.unavailable'), {
|
|
250
|
+
cause: { code: 'session_store_capacity' },
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
sessions.delete(removable[0]);
|
|
254
|
+
}
|
|
255
|
+
const revision = crypto.randomUUID();
|
|
256
|
+
sessions.delete(sessionId);
|
|
257
|
+
sessions.set(sessionId, {
|
|
258
|
+
snapshot: { revision, state: structuredClone(state) },
|
|
259
|
+
touched: Date.now(),
|
|
260
|
+
});
|
|
261
|
+
return Promise.resolve({ written: true, revision });
|
|
262
|
+
},
|
|
263
|
+
};
|
|
49
264
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape the decision and embedding handlers share: `GET <base>` answers the profile's
|
|
3
|
+
* interface, `POST <base>/<action>` answers `{ result }`, and a failure is worded with the
|
|
4
|
+
* profile's labels.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { type ProfileDefinition } from '@theoremjs/agents';
|
|
9
|
+
import { type ClientTurnOptions } from '@theoremjs/agents/host';
|
|
10
|
+
export type SingleActionOptions = {
|
|
11
|
+
/** The last path segment the POST is sent to, such as `decide`. */
|
|
12
|
+
action: string;
|
|
13
|
+
/** What `GET <base>` answers, as `{ interface }`. */
|
|
14
|
+
iface: unknown;
|
|
15
|
+
labels: ProfileDefinition['labels'];
|
|
16
|
+
clientEvents?: ClientTurnOptions;
|
|
17
|
+
onError?: (err: unknown, ctx: {
|
|
18
|
+
request: Request;
|
|
19
|
+
}) => void;
|
|
20
|
+
};
|
|
21
|
+
export declare function singleActionHandler({ action, iface, labels, clientEvents, onError }: SingleActionOptions, run: (request: Request) => Promise<unknown>): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape the decision and embedding handlers share: `GET <base>` answers the profile's
|
|
3
|
+
* interface, `POST <base>/<action>` answers `{ result }`, and a failure is worded with the
|
|
4
|
+
* profile's labels.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { labelText } from '@theoremjs/agents';
|
|
9
|
+
import { caughtStatus, clientError, HTTP_METHOD, } from '@theoremjs/agents/host';
|
|
10
|
+
import { jsonResponse } from "./json-response.js";
|
|
11
|
+
export function singleActionHandler({ action, iface, labels, clientEvents, onError }, run) {
|
|
12
|
+
const isAction = (request) => new URL(request.url).pathname.split('/').filter(Boolean).at(-1) === action;
|
|
13
|
+
return async (request) => {
|
|
14
|
+
try {
|
|
15
|
+
if (request.method === 'GET' && !isAction(request))
|
|
16
|
+
return jsonResponse(200, { interface: iface });
|
|
17
|
+
if (request.method !== 'POST' || !isAction(request)) {
|
|
18
|
+
return jsonResponse(HTTP_METHOD, {
|
|
19
|
+
error: labelText('error.request', {}, labels),
|
|
20
|
+
errorKind: 'request',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return jsonResponse(200, { result: await run(request) });
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
onError?.(err, { request });
|
|
27
|
+
return jsonResponse(caughtStatus(err), clientError(err, labels, clientEvents));
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
package/server/steer-inbox.js
CHANGED
|
@@ -14,7 +14,7 @@ const STEER_STAGES = new Set(['pre_turn', 'post_tool', 'before_end']);
|
|
|
14
14
|
*/
|
|
15
15
|
export function steerUnitOf(steer) {
|
|
16
16
|
const messages = steer.inject.filter((message) => message.role === 'user');
|
|
17
|
-
//
|
|
17
|
+
// labels-exempt: internal diagnostic; the user reads the error kind's (or copy key's) wording
|
|
18
18
|
if (!messages.length)
|
|
19
19
|
throw new TheoremError('request', 'inject must contain user messages');
|
|
20
20
|
return { id: steer.id, messages };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ToolExecutionCoordinator } from '@theoremjs/agents';
|
|
2
|
+
import type { TheoremSessionState } from './session-store.ts';
|
|
3
|
+
export declare function sessionToolCoordinator(store: {
|
|
4
|
+
mutate<T>(id: string, change: (state: TheoremSessionState) => T): Promise<T>;
|
|
5
|
+
}, sessionId: string): ToolExecutionCoordinator;
|
|
6
|
+
export declare function sameAcceptedAnswer(left: unknown, right: unknown): boolean;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { labelText, TheoremError, } from '@theoremjs/agents';
|
|
2
|
+
function unavailable() {
|
|
3
|
+
throw new TheoremError('unavailable', labelText('error.unavailable'));
|
|
4
|
+
}
|
|
5
|
+
function sameOperation(record, identity) {
|
|
6
|
+
return (record.operationId === identity.operationId &&
|
|
7
|
+
record.callId === identity.callId &&
|
|
8
|
+
record.profileId === identity.profileId &&
|
|
9
|
+
record.toolId === identity.toolId &&
|
|
10
|
+
record.invocationHash === identity.invocationHash);
|
|
11
|
+
}
|
|
12
|
+
function checkedRecord(state, identity) {
|
|
13
|
+
if (identity.operationId === '__proto__')
|
|
14
|
+
unavailable();
|
|
15
|
+
const record = Object.hasOwn(state.operations, identity.operationId)
|
|
16
|
+
? state.operations[identity.operationId]
|
|
17
|
+
: undefined;
|
|
18
|
+
if (record?.status === 'ready') {
|
|
19
|
+
if (record.pending.callId !== identity.callId ||
|
|
20
|
+
record.pending.profileId !== identity.profileId ||
|
|
21
|
+
record.pending.name !== identity.toolId ||
|
|
22
|
+
record.profileId !== identity.profileId)
|
|
23
|
+
unavailable();
|
|
24
|
+
}
|
|
25
|
+
else if (record && !sameOperation(record, identity))
|
|
26
|
+
unavailable();
|
|
27
|
+
return record;
|
|
28
|
+
}
|
|
29
|
+
function checkedClaim(state, claim) {
|
|
30
|
+
const record = checkedRecord(state, claim);
|
|
31
|
+
if (!record ||
|
|
32
|
+
(record.status !== 'running' && record.status !== 'uncertain') ||
|
|
33
|
+
record.claimToken !== claim.claimToken)
|
|
34
|
+
unavailable();
|
|
35
|
+
return record;
|
|
36
|
+
}
|
|
37
|
+
function existingResult(record) {
|
|
38
|
+
if (record?.status === 'settled')
|
|
39
|
+
return { kind: 'settled', outcome: record.outcome };
|
|
40
|
+
if (record?.status === 'running')
|
|
41
|
+
return { kind: 'running' };
|
|
42
|
+
if (record?.status === 'uncertain')
|
|
43
|
+
return { kind: 'uncertain' };
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function saveOutcome(state, identity, outcome) {
|
|
47
|
+
const before = state.operations[identity.operationId];
|
|
48
|
+
const pageGrant = before && before.status !== 'ready' ? before.pageGrant : undefined;
|
|
49
|
+
const settledAt = Date.now();
|
|
50
|
+
const { operationId, callId, profileId, toolId, invocationHash, invocation } = identity;
|
|
51
|
+
state.operations[identity.operationId] = {
|
|
52
|
+
operationId,
|
|
53
|
+
callId,
|
|
54
|
+
profileId,
|
|
55
|
+
toolId,
|
|
56
|
+
invocationHash,
|
|
57
|
+
...(invocation ? { invocation } : {}),
|
|
58
|
+
status: 'settled',
|
|
59
|
+
...(pageGrant ? { pageGrant } : {}),
|
|
60
|
+
outcome,
|
|
61
|
+
settledAt,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function sessionToolCoordinator(store, sessionId) {
|
|
65
|
+
return {
|
|
66
|
+
lookup(identity) {
|
|
67
|
+
return store.mutate(sessionId, (state) => {
|
|
68
|
+
const record = checkedRecord(state, identity);
|
|
69
|
+
return existingResult(record);
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
begin(identity) {
|
|
73
|
+
const claimToken = crypto.randomUUID();
|
|
74
|
+
const startedAt = Date.now();
|
|
75
|
+
return store.mutate(sessionId, (state) => {
|
|
76
|
+
const record = checkedRecord(state, identity);
|
|
77
|
+
const existing = existingResult(record);
|
|
78
|
+
if (existing)
|
|
79
|
+
return existing;
|
|
80
|
+
state.operations[identity.operationId] = {
|
|
81
|
+
...identity,
|
|
82
|
+
status: 'running',
|
|
83
|
+
claimToken,
|
|
84
|
+
startedAt,
|
|
85
|
+
};
|
|
86
|
+
return { kind: 'acquired', claimToken };
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
settle(claim, outcome) {
|
|
90
|
+
return store.mutate(sessionId, (state) => {
|
|
91
|
+
const record = checkedClaim(state, claim);
|
|
92
|
+
saveOutcome(state, { ...claim, invocation: record.invocation }, outcome);
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
settleNotStarted(identity, outcome) {
|
|
96
|
+
return store.mutate(sessionId, (state) => {
|
|
97
|
+
const record = checkedRecord(state, identity);
|
|
98
|
+
if (record && record.status !== 'ready')
|
|
99
|
+
unavailable();
|
|
100
|
+
if (outcome.effects !== 'not_started')
|
|
101
|
+
unavailable();
|
|
102
|
+
saveOutcome(state, identity, outcome);
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
markUncertain(claim, reason) {
|
|
106
|
+
return store.mutate(sessionId, (state) => {
|
|
107
|
+
const record = checkedClaim(state, claim);
|
|
108
|
+
state.operations[claim.operationId] = {
|
|
109
|
+
...claim,
|
|
110
|
+
invocation: record.invocation,
|
|
111
|
+
...(record.pageGrant ? { pageGrant: record.pageGrant } : {}),
|
|
112
|
+
status: 'uncertain',
|
|
113
|
+
reason,
|
|
114
|
+
detectedAt: Date.now(),
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function sameAcceptedAnswer(left, right) {
|
|
121
|
+
const canonical = (value) => {
|
|
122
|
+
if (Array.isArray(value))
|
|
123
|
+
return value.map(canonical);
|
|
124
|
+
if (value && typeof value === 'object')
|
|
125
|
+
return Object.fromEntries(Object.entries(value)
|
|
126
|
+
.filter(([, item]) => item !== undefined)
|
|
127
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
128
|
+
.map(([key, item]) => [key, canonical(item)]));
|
|
129
|
+
return value;
|
|
130
|
+
};
|
|
131
|
+
return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right));
|
|
132
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SessionRequest, TurnInput } from '@theoremjs/agents';
|
|
2
|
+
import type { LiveOpenMessage } from '../client/live-messages.ts';
|
|
3
|
+
import type { TheoremTurnInput } from '../client/transport.ts';
|
|
4
|
+
/** A turn input off the wire as the kernel takes it: the page's context is the client's. */
|
|
5
|
+
export declare function kernelTurnInput(input: TheoremTurnInput, server?: unknown): TurnInput;
|
|
6
|
+
/**
|
|
7
|
+
* What a live call's first message asks of the session, as `runSession` takes
|
|
8
|
+
* it: the page's context is the client's, and `server` is the host's own.
|
|
9
|
+
*/
|
|
10
|
+
export declare function liveSessionOpen(open: LiveOpenMessage, server?: unknown): Pick<SessionRequest, 'slots' | 'context' | 'providerState' | 'awayMs' | 'history'>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** The page's context as the client's and the host's as the server's; nothing when neither gave any. */
|
|
2
|
+
function turnContext(client, server) {
|
|
3
|
+
const context = {
|
|
4
|
+
...(client === undefined ? {} : { client }),
|
|
5
|
+
...(server === undefined ? {} : { server }),
|
|
6
|
+
};
|
|
7
|
+
return Object.keys(context).length > 0 ? context : undefined;
|
|
8
|
+
}
|
|
9
|
+
/** A turn input off the wire as the kernel takes it: the page's context is the client's. */
|
|
10
|
+
export function kernelTurnInput(input, server) {
|
|
11
|
+
const { context: client, ...rest } = input;
|
|
12
|
+
const context = turnContext(client, server);
|
|
13
|
+
return context ? { ...rest, context } : rest;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What a live call's first message asks of the session, as `runSession` takes
|
|
17
|
+
* it: the page's context is the client's, and `server` is the host's own.
|
|
18
|
+
*/
|
|
19
|
+
export function liveSessionOpen(open, server) {
|
|
20
|
+
const context = turnContext(open.context, server);
|
|
21
|
+
return {
|
|
22
|
+
...(open.history ? { history: open.history } : {}),
|
|
23
|
+
...(open.slots ? { slots: open.slots } : {}),
|
|
24
|
+
...(context ? { context } : {}),
|
|
25
|
+
...(open.resume
|
|
26
|
+
? { providerState: open.resume.providerState, awayMs: open.resume.awayMs }
|
|
27
|
+
: {}),
|
|
28
|
+
};
|
|
29
|
+
}
|