@graphorin/agent 0.6.0 → 0.7.0

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +31 -11
  3. package/dist/errors/index.d.ts +17 -4
  4. package/dist/errors/index.d.ts.map +1 -1
  5. package/dist/errors/index.js +19 -3
  6. package/dist/errors/index.js.map +1 -1
  7. package/dist/factory.d.ts.map +1 -1
  8. package/dist/factory.js +153 -1930
  9. package/dist/factory.js.map +1 -1
  10. package/dist/fanout/index.d.ts +13 -1
  11. package/dist/fanout/index.d.ts.map +1 -1
  12. package/dist/fanout/index.js +13 -4
  13. package/dist/fanout/index.js.map +1 -1
  14. package/dist/index.d.ts +7 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +11 -9
  17. package/dist/index.js.map +1 -1
  18. package/dist/lateral-leak/index.js +1 -1
  19. package/dist/package.js +6 -0
  20. package/dist/package.js.map +1 -0
  21. package/dist/run-state/index.d.ts +32 -6
  22. package/dist/run-state/index.d.ts.map +1 -1
  23. package/dist/run-state/index.js +46 -22
  24. package/dist/run-state/index.js.map +1 -1
  25. package/dist/runtime/agent-surface.js +122 -0
  26. package/dist/runtime/agent-surface.js.map +1 -0
  27. package/dist/runtime/agent-to-tool.d.ts +51 -0
  28. package/dist/runtime/agent-to-tool.d.ts.map +1 -0
  29. package/dist/runtime/agent-to-tool.js +145 -0
  30. package/dist/runtime/agent-to-tool.js.map +1 -0
  31. package/dist/runtime/approvals.js +0 -0
  32. package/dist/runtime/approvals.js.map +1 -0
  33. package/dist/runtime/dispatch.js +108 -0
  34. package/dist/runtime/dispatch.js.map +1 -0
  35. package/dist/runtime/executor-wiring.js +128 -0
  36. package/dist/runtime/executor-wiring.js.map +1 -0
  37. package/dist/runtime/fallback-chain.js +139 -0
  38. package/dist/runtime/fallback-chain.js.map +1 -0
  39. package/dist/runtime/handoff.js +307 -0
  40. package/dist/runtime/handoff.js.map +1 -0
  41. package/dist/runtime/messages.d.ts +22 -0
  42. package/dist/runtime/messages.d.ts.map +1 -0
  43. package/dist/runtime/messages.js +204 -0
  44. package/dist/runtime/messages.js.map +1 -0
  45. package/dist/runtime/provider-events.js +117 -0
  46. package/dist/runtime/provider-events.js.map +1 -0
  47. package/dist/runtime/run-compaction.js +210 -0
  48. package/dist/runtime/run-compaction.js.map +1 -0
  49. package/dist/runtime/run-finish.js +48 -0
  50. package/dist/runtime/run-finish.js.map +1 -0
  51. package/dist/runtime/run-gates.js +336 -0
  52. package/dist/runtime/run-gates.js.map +1 -0
  53. package/dist/runtime/run-init.js +81 -0
  54. package/dist/runtime/run-init.js.map +1 -0
  55. package/dist/runtime/run-input.js +46 -0
  56. package/dist/runtime/run-input.js.map +1 -0
  57. package/dist/runtime/step-catalogue.js +173 -0
  58. package/dist/runtime/step-catalogue.js.map +1 -0
  59. package/dist/runtime/tool-call-walk.js +189 -0
  60. package/dist/runtime/tool-call-walk.js.map +1 -0
  61. package/dist/runtime/tool-wiring.js +159 -0
  62. package/dist/runtime/tool-wiring.js.map +1 -0
  63. package/dist/tooling/adapters.js +1 -1
  64. package/dist/tooling/dataflow.js +1 -1
  65. package/dist/tooling/policy.js +2 -0
  66. package/dist/tooling/policy.js.map +1 -1
  67. package/dist/types.d.ts +63 -13
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +20 -20
  70. package/src/errors/index.ts +320 -0
  71. package/src/evaluator-optimizer/index.ts +212 -0
  72. package/src/factory.ts +957 -0
  73. package/src/fallback/index.ts +108 -0
  74. package/src/fanout/index.ts +523 -0
  75. package/src/filters/index.ts +347 -0
  76. package/src/index.ts +180 -0
  77. package/src/internal/ids.ts +46 -0
  78. package/src/internal/usage-accumulator.ts +90 -0
  79. package/src/lateral-leak/causality-monitor.ts +221 -0
  80. package/src/lateral-leak/index.ts +35 -0
  81. package/src/lateral-leak/merge-guard.ts +151 -0
  82. package/src/lateral-leak/protocol-guard.ts +222 -0
  83. package/src/preferred-model/index.ts +210 -0
  84. package/src/progress/index.ts +238 -0
  85. package/src/run-state/index.ts +607 -0
  86. package/src/runtime/agent-surface.ts +218 -0
  87. package/src/runtime/agent-to-tool.ts +323 -0
  88. package/src/runtime/approvals.ts +0 -0
  89. package/src/runtime/dispatch.ts +183 -0
  90. package/src/runtime/executor-wiring.ts +331 -0
  91. package/src/runtime/fallback-chain.ts +250 -0
  92. package/src/runtime/handoff.ts +428 -0
  93. package/src/runtime/messages.ts +309 -0
  94. package/src/runtime/provider-events.ts +175 -0
  95. package/src/runtime/run-compaction.ts +288 -0
  96. package/src/runtime/run-finish.ts +93 -0
  97. package/src/runtime/run-gates.ts +419 -0
  98. package/src/runtime/run-init.ts +169 -0
  99. package/src/runtime/run-input.ts +102 -0
  100. package/src/runtime/step-catalogue.ts +338 -0
  101. package/src/runtime/tool-call-walk.ts +301 -0
  102. package/src/runtime/tool-wiring.ts +218 -0
  103. package/src/testing/replay-provider.ts +121 -0
  104. package/src/tooling/adapters.ts +403 -0
  105. package/src/tooling/catalogue.ts +36 -0
  106. package/src/tooling/dataflow.ts +171 -0
  107. package/src/tooling/plan.ts +123 -0
  108. package/src/tooling/policy.ts +67 -0
  109. package/src/tooling/registry-build.ts +191 -0
  110. package/src/types.ts +696 -0
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Compaction lifecycle for the agent run loop (WI-09 / P1-1, CE-3 /
3
+ * AG-13, context-engine-06): the auto-compaction trigger evaluated
4
+ * before each provider request, the last-resort emergency compaction at
5
+ * hard context overflow, the servicing of `agent.compact()` manual
6
+ * requests, and the prefix-pinned splice shared by all three paths.
7
+ * Extracted verbatim from `factory.ts` (issue #23); the former run-loop
8
+ * closures now take an explicit {@link CompactionRunEnv}.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ import type { AgentEvent, Message, RunState, Sensitivity } from '@graphorin/core';
14
+ import type { Memory } from '@graphorin/memory';
15
+ import type { CompactionApiResult, CompactOptions } from '../types.js';
16
+ import type { MutableRunState } from './run-input.js';
17
+
18
+ /** Return envelope of `ContextEngine.compactNow` (shared splice paths, CE-3). */
19
+ export type CompactionEnvelope = Awaited<ReturnType<Memory['contextEngine']['compactNow']>>;
20
+
21
+ /**
22
+ * Manual-compaction requests enqueued by `agent.compact()` (CE-3 /
23
+ * AG-13). The run loop owns the live message buffer, so the splice
24
+ * must happen inside the loop: `maybeAutoCompact` services the queue
25
+ * at the next step boundary, and `finishRun` settles leftovers as
26
+ * explicit no-ops.
27
+ */
28
+ export interface PendingManualCompact {
29
+ readonly options: CompactOptions | undefined;
30
+ readonly resolve: (result: CompactionApiResult) => void;
31
+ readonly reject: (cause: unknown) => void;
32
+ }
33
+
34
+ export const noopCompactionResult = (
35
+ skippedReason: NonNullable<CompactionApiResult['skippedReason']>,
36
+ ): CompactionApiResult => ({
37
+ beforeTokens: 0,
38
+ afterTokens: 0,
39
+ summaryTokens: 0,
40
+ durationMs: 0,
41
+ hooksFiredCount: 0,
42
+ summary: '',
43
+ applied: false,
44
+ skippedReason,
45
+ });
46
+
47
+ /**
48
+ * The run-scoped context the compaction paths operate on. Field names
49
+ * mirror the run-loop locals the former closures captured; the live
50
+ * message buffer and the pending-manual queue are shared references.
51
+ */
52
+ export interface CompactionRunEnv {
53
+ readonly config: { readonly sensitivity?: Sensitivity };
54
+ readonly memory: Memory | undefined;
55
+ readonly state: MutableRunState & RunState;
56
+ readonly messages: Message[];
57
+ readonly sessionId: string;
58
+ readonly agentId: string;
59
+ readonly systemPrefixLength: number;
60
+ readonly pendingManualCompacts: PendingManualCompact[];
61
+ }
62
+
63
+ /**
64
+ * Auto-compaction trigger (WI-09 / P1-1). Before assembling each
65
+ * provider request, ask the memory {@link ContextEngine} whether the
66
+ * in-flight buffer has crossed its per-provider threshold
67
+ * (`shouldCompact`); when it has, summarise the older turns
68
+ * (`compactNow`, `source: 'auto-trigger'`), splice the result back in
69
+ * - preserving the byte-stable system prefix and the most-recent
70
+ * turns verbatim - and emit `context.compacted`. The compaction is
71
+ * configured on the memory facade (`createMemory({ contextEngine })`,
72
+ * RB-46); there is no parallel agent-level knob.
73
+ *
74
+ * No-op when no memory is wired, when compaction is disabled or below
75
+ * threshold (the engine returns `false`), or for `secret`-tier runs
76
+ * (secret history is never shipped to the summarizer - a less-trusted
77
+ * external sink; per-result handle references land in WI-10). Best
78
+ * effort: a misconfigured engine (e.g. no summarizer) is swallowed and
79
+ * the run proceeds uncompacted rather than aborting mid-flight.
80
+ *
81
+ * Operator-requested compactions (`agent.compact()`, CE-3/AG-13) are
82
+ * serviced here too, FIRST - the queue carries the `compact()` promise
83
+ * resolvers, and manual requests bypass the trigger evaluation.
84
+ */
85
+ export async function* maybeAutoCompact<TOutput>(
86
+ env: CompactionRunEnv,
87
+ ): AsyncGenerator<AgentEvent<TOutput>, void, void> {
88
+ const { config, memory, state, messages, sessionId, agentId, systemPrefixLength } = env;
89
+ const { pendingManualCompacts } = env;
90
+ while (pendingManualCompacts.length > 0) {
91
+ const pending = pendingManualCompacts.shift();
92
+ if (pending !== undefined) yield* serviceManualCompact<TOutput>(env, pending);
93
+ }
94
+ const mem = memory;
95
+ if (mem === undefined) return;
96
+ // Sensitivity gate (WI-09 step 2): drop, never re-route, secret-tier
97
+ // content. Auto-compaction is an LLM summarizer call, so a secret
98
+ // run is left un-compacted here.
99
+ if (config.sensitivity === 'secret') return;
100
+ const engine = mem.contextEngine;
101
+ // context-engine-04: trigger, reclaim floor, and anti-thrash guard
102
+ // share one basis - the engine sees the full buffer for the trigger
103
+ // total, learns where the pinned (uncompactable) prefix ends, and
104
+ // receives the prefix messages so the guard arms against the FULL
105
+ // post-splice context instead of the sliced body.
106
+ const triggered = await engine
107
+ .shouldCompact(messages, { compactableFromIndex: systemPrefixLength })
108
+ .catch(() => false);
109
+ if (!triggered) return;
110
+
111
+ const startedAt = Date.now();
112
+ const envelope = await engine
113
+ .compactNow({
114
+ scope: { userId: state.userId ?? agentId, sessionId, agentId },
115
+ runId: state.id,
116
+ sessionId,
117
+ agentId,
118
+ source: 'auto-trigger',
119
+ messages: messages.slice(systemPrefixLength),
120
+ prefixMessages: messages.slice(0, systemPrefixLength),
121
+ memory: mem,
122
+ })
123
+ // No summarizer configured (or the strategy threw) - proceed with
124
+ // the un-compacted buffer rather than failing a live run.
125
+ .catch(() => undefined);
126
+ if (envelope === undefined) return;
127
+ // Nothing was old enough to trim (body ≤ preserve-recent) - skip the
128
+ // splice + event so `context.compacted` only fires on real work.
129
+ if (envelope.result.droppedMessageIndices.length === 0) return;
130
+
131
+ spliceCompacted(env, envelope);
132
+ yield {
133
+ type: 'context.compacted',
134
+ runId: state.id,
135
+ sessionId,
136
+ agentId,
137
+ beforeTokens: envelope.result.beforeTokens,
138
+ afterTokens: envelope.result.afterTokens,
139
+ summaryTokens: envelope.result.summaryTokens,
140
+ durationMs: Date.now() - startedAt,
141
+ source: 'auto-trigger',
142
+ hooksFiredCount: envelope.result.hooksFiredCount,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * context-engine-06: last-resort tier at hard context overflow. When
148
+ * a provider rejects the request as over-window, force ONE aggressive
149
+ * compaction (`preserveRecentTurns: 2`, trigger evaluation bypassed)
150
+ * and let the caller retry the same provider - the fallback chain's
151
+ * members usually share the same window, so without this the run just
152
+ * dies. Returns `true` when the buffer actually shrank (retry is
153
+ * worthwhile); `false` when memory is not wired, the run is
154
+ * secret-tier, compaction trimmed nothing, or the engine threw.
155
+ */
156
+ export async function* tryEmergencyCompact<TOutput>(
157
+ env: CompactionRunEnv,
158
+ ): AsyncGenerator<AgentEvent<TOutput>, boolean, void> {
159
+ const { config, memory, state, messages, sessionId, agentId, systemPrefixLength } = env;
160
+ const mem = memory;
161
+ if (mem === undefined || config.sensitivity === 'secret') return false;
162
+ const startedAt = Date.now();
163
+ const envelope = await mem.contextEngine
164
+ .compactNow({
165
+ scope: { userId: state.userId ?? agentId, sessionId, agentId },
166
+ runId: state.id,
167
+ sessionId,
168
+ agentId,
169
+ source: 'auto-trigger',
170
+ messages: messages.slice(systemPrefixLength),
171
+ prefixMessages: messages.slice(0, systemPrefixLength),
172
+ memory: mem,
173
+ preserveRecentTurns: 2,
174
+ })
175
+ .catch(() => undefined);
176
+ if (envelope === undefined || envelope.result.droppedMessageIndices.length === 0) {
177
+ return false;
178
+ }
179
+ spliceCompacted(env, envelope);
180
+ yield {
181
+ type: 'context.compacted',
182
+ runId: state.id,
183
+ sessionId,
184
+ agentId,
185
+ beforeTokens: envelope.result.beforeTokens,
186
+ afterTokens: envelope.result.afterTokens,
187
+ summaryTokens: envelope.result.summaryTokens,
188
+ durationMs: Date.now() - startedAt,
189
+ source: 'auto-trigger',
190
+ hooksFiredCount: envelope.result.hooksFiredCount,
191
+ };
192
+ return true;
193
+ }
194
+
195
+ /**
196
+ * Prefix-pinned splice shared by the auto + manual compaction paths
197
+ * (CE-3): stable system prefix + [summary, ...recent turns], with the
198
+ * post-compaction hooks' text Context Essentials re-anchored as a
199
+ * trailing system message so they survive the trim (RB-46). Mutates
200
+ * BOTH the live loop buffer and `state.messages`.
201
+ */
202
+ function spliceCompacted(env: CompactionRunEnv, envelope: CompactionEnvelope): void {
203
+ const { state, messages, systemPrefixLength } = env;
204
+ const prefix = messages.slice(0, systemPrefixLength);
205
+ const rebuilt: Message[] = [...prefix, ...envelope.result.trimmedMessages];
206
+ const essentials = envelope.extraContent
207
+ .map((part) =>
208
+ typeof part === 'object' && part !== null && 'text' in part
209
+ ? String((part as { readonly text: unknown }).text)
210
+ : '',
211
+ )
212
+ .filter((text) => text.length > 0)
213
+ .join('\n\n');
214
+ if (essentials.length > 0) {
215
+ rebuilt.push({ role: 'system', content: essentials });
216
+ }
217
+ messages.splice(0, messages.length, ...rebuilt);
218
+ state.messages.splice(0, state.messages.length, ...rebuilt);
219
+ }
220
+
221
+ /**
222
+ * Service one `agent.compact()` request inside the loop (CE-3/AG-13):
223
+ * same prefix-pinned splice as the auto path, `source: 'manual'` (or
224
+ * the caller's `'pre-step'`), `preserveRecentTurns` forwarded as a
225
+ * per-call strategy override. An engine failure rejects the caller's
226
+ * promise but never aborts the live run; a summarize that trims
227
+ * nothing resolves `applied: false` without an event.
228
+ */
229
+ async function* serviceManualCompact<TOutput>(
230
+ env: CompactionRunEnv,
231
+ pending: PendingManualCompact,
232
+ ): AsyncGenerator<AgentEvent<TOutput>, void, void> {
233
+ const { memory, state, messages, sessionId, agentId, systemPrefixLength } = env;
234
+ const mem = memory;
235
+ if (mem === undefined) {
236
+ pending.resolve(noopCompactionResult('no-memory'));
237
+ return;
238
+ }
239
+ const source = pending.options?.source ?? 'manual';
240
+ const startedAt = Date.now();
241
+ let envelope: CompactionEnvelope;
242
+ try {
243
+ envelope = await mem.contextEngine.compactNow({
244
+ scope: { userId: state.userId ?? agentId, sessionId, agentId },
245
+ runId: state.id,
246
+ sessionId,
247
+ agentId,
248
+ source,
249
+ messages: messages.slice(systemPrefixLength),
250
+ // context-engine-04: same accounting basis as the auto path.
251
+ prefixMessages: messages.slice(0, systemPrefixLength),
252
+ memory: mem,
253
+ ...(pending.options?.preserveRecentTurns !== undefined
254
+ ? { preserveRecentTurns: pending.options.preserveRecentTurns }
255
+ : {}),
256
+ });
257
+ } catch (cause) {
258
+ pending.reject(cause);
259
+ return;
260
+ }
261
+ const { result } = envelope;
262
+ const applied = result.droppedMessageIndices.length > 0;
263
+ if (applied) {
264
+ spliceCompacted(env, envelope);
265
+ yield {
266
+ type: 'context.compacted',
267
+ runId: state.id,
268
+ sessionId,
269
+ agentId,
270
+ beforeTokens: result.beforeTokens,
271
+ afterTokens: result.afterTokens,
272
+ summaryTokens: result.summaryTokens,
273
+ durationMs: Date.now() - startedAt,
274
+ source,
275
+ hooksFiredCount: result.hooksFiredCount,
276
+ };
277
+ }
278
+ pending.resolve({
279
+ beforeTokens: result.beforeTokens,
280
+ afterTokens: result.afterTokens,
281
+ summaryTokens: result.summaryTokens,
282
+ durationMs: Date.now() - startedAt,
283
+ hooksFiredCount: result.hooksFiredCount,
284
+ summary: result.summary ?? '',
285
+ applied,
286
+ ...(applied ? {} : { skippedReason: 'nothing-to-trim' as const }),
287
+ });
288
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The terminal path of every agent run: settle leftover manual-compact
3
+ * requests (CE-3 / AG-13), assemble the final {@link AgentResult}
4
+ * (AG-9), clear terminal-run spill artifacts (TL-10), and end the
5
+ * stream with `agent.end` (AG-20). Extracted verbatim from
6
+ * `factory.ts` (issue #23); the former factory closures now hang off
7
+ * {@link createRunFinisher}'s deps.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ import type { AgentEvent, AgentResult, CheckpointStore, RunState } from '@graphorin/core';
13
+ import type { createDefaultSpillWriter } from '@graphorin/tools/result';
14
+ import { noopCompactionResult, type PendingManualCompact } from './run-compaction.js';
15
+ import type { InternalRunSnapshot, MutableRunState } from './run-input.js';
16
+
17
+ /** What the terminal path needs from the agent factory scope. */
18
+ export interface RunFinisherDeps {
19
+ readonly pendingManualCompacts: PendingManualCompact[];
20
+ readonly spillWriter: ReturnType<typeof createDefaultSpillWriter>;
21
+ /** W-005: thread erasure on terminal runs (`'delete-on-terminal'`). */
22
+ readonly checkpointStore?: CheckpointStore | undefined;
23
+ readonly checkpointPolicy?: 'keep' | 'delete-on-terminal' | undefined;
24
+ }
25
+
26
+ /**
27
+ * Build the factory's `finishRunBase` over the shared manual-compact
28
+ * queue and spill writer.
29
+ */
30
+ export function createRunFinisher<TOutput>(
31
+ deps: RunFinisherDeps,
32
+ ): (
33
+ state: MutableRunState & RunState,
34
+ snapshot: InternalRunSnapshot<TOutput>,
35
+ ) => AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void> {
36
+ const { pendingManualCompacts, spillWriter, checkpointStore, checkpointPolicy } = deps;
37
+
38
+ /**
39
+ * Terminal wrapper around {@link finalize}: every exit path of the run
40
+ * loop - completed, failed, aborted, suspended - ends the stream with
41
+ * an `agent.end` event carrying the final {@link AgentResult} (AG-20).
42
+ *
43
+ * The parameter is `MutableRunState & RunState`: the single caller
44
+ * (factory `finishRun`) is typed exactly so, and the intersection lets
45
+ * {@link finalize} assemble `AgentResult.state` without a cast (W-047).
46
+ */
47
+ async function* finishRunBase(
48
+ state: MutableRunState & RunState,
49
+ snapshot: InternalRunSnapshot<TOutput>,
50
+ ): AsyncGenerator<AgentEvent<TOutput>, AgentResult<TOutput>, void> {
51
+ // CE-3/AG-13: settle manual-compact requests the loop never serviced -
52
+ // the run is over, there is no live buffer left to splice.
53
+ while (pendingManualCompacts.length > 0) {
54
+ pendingManualCompacts.shift()?.resolve(noopCompactionResult('no-active-run'));
55
+ }
56
+ const result = finalize(state, snapshot);
57
+ // TL-10: spill artifacts are run-scoped scratch - drop them once the
58
+ // run is terminal. `awaiting_approval` and `aborted` runs keep
59
+ // theirs (handles must survive resume); orphans fall to the writer's
60
+ // startup TTL sweep.
61
+ if (result.status === 'completed' || result.status === 'failed') {
62
+ await spillWriter.clear?.(result.state.id).catch(() => {});
63
+ // W-005: opt-in checkpoint hygiene mirrors the spill lifecycle -
64
+ // same statuses, best-effort, and NEVER for awaiting_approval /
65
+ // aborted runs (their thread is the resume state). Default 'keep'
66
+ // preserves post-hoc debugging and process-restart resume.
67
+ if (checkpointPolicy === 'delete-on-terminal' && checkpointStore !== undefined) {
68
+ await checkpointStore.deleteThread(result.state.id).catch(() => {});
69
+ }
70
+ }
71
+ yield { type: 'agent.end', runId: result.state.id, result };
72
+ return result;
73
+ }
74
+
75
+ function finalize(
76
+ state: MutableRunState & RunState,
77
+ snapshot: InternalRunSnapshot<TOutput>,
78
+ ): AgentResult<TOutput> {
79
+ state.finishedAt = state.finishedAt ?? new Date().toISOString();
80
+ // AG-9: the result carries the terminal status, the failure (when
81
+ // any), and the final RunState - a suspended run is resumable from
82
+ // the result alone, no checkpointStore required.
83
+ return {
84
+ output: snapshot.output,
85
+ usage: state.usage,
86
+ status: state.status,
87
+ ...(state.error !== undefined ? { error: state.error } : {}),
88
+ state,
89
+ };
90
+ }
91
+
92
+ return finishRunBase;
93
+ }