purra-mem0 0.5.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lybrands
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # purra-mem0 · TypeScript
2
+
3
+ English | [简体中文](README.zh-CN.md)
4
+
5
+ Scoped memory, retrieval, and lifecycle management using `Memory` from
6
+ `mem0ai/oss`. Requires Node.js 22.13+; hosted Mem0 Platform clients are not supported.
7
+
8
+ ## Install
9
+
10
+ Follow [source installation](../../README.md#source-installation), selecting
11
+ `integrations/mem0/typescript`. Mem0's local store needs the `better-sqlite3`
12
+ native installation scripts.
13
+
14
+ For managed LLM/Embedding callbacks, install the optional peer in your application:
15
+
16
+ ```sh
17
+ npm install @langchain/core@1.1.47
18
+ ```
19
+
20
+ Before importing Mem0, set `MEM0_TELEMETRY=false` and an application-owned
21
+ `MEM0_DIR`. Explicitly configure the LLM, embedder, `vectorStore.config.dbPath`,
22
+ and `historyDbPath`.
23
+
24
+ ## Save and read
25
+
26
+ The application supplies `mem0Config`, authenticated `userId`, authorized
27
+ `projectId`, and a persistent `journalPath`:
28
+
29
+ ```ts
30
+ import { Memory } from "mem0ai/oss";
31
+ import { Mem0Memory } from "purra-mem0";
32
+
33
+ const sdk = new Memory(mem0Config);
34
+ const memory = new Mem0Memory({
35
+ client: sdk,
36
+ scope: { user: userId, project: projectId },
37
+ journalPath,
38
+ });
39
+
40
+ async function savePreference() {
41
+ const saved = await memory.add("Reply in Chinese.", {
42
+ source: { id: "preference:language", revision: "1" },
43
+ key: "preference:language:1",
44
+ });
45
+ return memory.get(saved.ids[0]!);
46
+ }
47
+ ```
48
+
49
+ `add` creates an active record by default; use `state: "pending"` for review first.
50
+ `get` returns `undefined` for unavailable records. Updates, state changes, and
51
+ deletions require the current `version` and a stable operation `key`.
52
+
53
+ ## Connect to an Agent
54
+
55
+ Supply `memoryQuery` as a function that selects query text from a Core `ContextRequest`:
56
+
57
+ ```ts
58
+ import { RetrieverTool } from "purra";
59
+ import { MemoryContext } from "purra-mem0";
60
+
61
+ const recall = new RetrieverTool({
62
+ retriever: memory,
63
+ name: "recallMemory",
64
+ description: "Recall saved preferences and facts.",
65
+ });
66
+ const context = new MemoryContext({ memory, query: memoryQuery });
67
+ ```
68
+
69
+ Add `recall.definition` to the Agent's tools, or compose `context` with its context
70
+ providers. The memory instance already binds the scope. For explicit record
71
+ selection, use `assembleMemoryContext(memory, ids, allowance)`.
72
+
73
+ ## Managed model calls
74
+
75
+ Use the following configuration instead of a raw SDK client when calls need
76
+ persistent budgets. Storage paths, embedding dimensions, and the budget key come
77
+ from the application; the numeric limits below illustrate a budget configuration.
78
+
79
+ `modelTasks` is the runner supplied by a PurrA extension factory.
80
+ The async `embed(texts, signal)` callback must return `EmbeddingResult` with
81
+ vectors and any reported input-token usage.
82
+
83
+ ```ts
84
+ import { createManagedClient, runModel } from "purra-mem0";
85
+
86
+ const client = await createManagedClient({
87
+ embeddingDims: embeddingDimensions,
88
+ config: { vectorStore: vectorStoreConfig, historyDbPath: historyPath },
89
+ });
90
+ const memory = new Mem0Memory({
91
+ client,
92
+ scope: { user: userId, project: projectId },
93
+ journalPath,
94
+ allowInference: true,
95
+ providers: {
96
+ budget: {
97
+ key: budgetKey,
98
+ maxLlmCalls: 4,
99
+ maxEmbeddingCalls: 64,
100
+ maxInputChars: 100_000,
101
+ maxOutputTokens: 8192,
102
+ resultCapacityTargetTokens: 2048,
103
+ },
104
+ complete: runModel(modelTasks),
105
+ embed,
106
+ },
107
+ });
108
+ ```
109
+
110
+ Use `memory.budgetUsage()` to inspect admitted and reported usage. Raw SDK mode
111
+ reports internal usage as unknown.
112
+
113
+ `resultCapacityTargetTokens` reserves the expected per-call memory result size.
114
+ It is not the Provider generation limit; `runModel()` preserves the Run's user
115
+ generation ceiling and records the memory value as a workflow capacity target.
116
+
117
+ ## Extract and review
118
+
119
+ With managed providers and `allowInference: true`:
120
+
121
+ ```ts
122
+ import { MemoryWorkflow, type MemorySource } from "purra-mem0";
123
+
124
+ const workflow = new MemoryWorkflow(memory);
125
+
126
+ async function capture(
127
+ messages: readonly { role: "user" | "assistant"; content: string }[],
128
+ source: MemorySource,
129
+ operationKey: string,
130
+ ) {
131
+ return workflow.capture(messages, { source, key: operationKey });
132
+ }
133
+ ```
134
+
135
+ This configuration reviews candidates and leaves them pending. To apply decisions,
136
+ pass `policy(candidate, review)` and a stable `policyRevision` to `MemoryWorkflow`.
137
+ Return an authorized `MemoryResolution` that retains the review key, or `undefined`
138
+ to leave the candidate pending. The policy may be async.
139
+
140
+ ## Lifecycle
141
+
142
+ Before shutdown, call `await memory.drain()` and `memory.close()`, then close SDK
143
+ and provider resources. For paging, withdrawal, evidence validation, and uncertain
144
+ writes, see the [memory lifecycle guide](../README.md).
@@ -0,0 +1,133 @@
1
+ # purra-mem0 · TypeScript
2
+
3
+ [English](README.md) | 简体中文
4
+
5
+ 使用 `mem0ai/oss` 的 `Memory` 提供作用域记忆、检索和生命周期管理。
6
+ 要求 Node.js 22.13+,不支持托管的 Mem0 Platform 客户端。
7
+
8
+ ## 安装
9
+
10
+ 按照[源码安装说明](../../README.zh-CN.md#从源码安装)操作,选择
11
+ `integrations/mem0/typescript`。Mem0 的本地存储需要执行 `better-sqlite3` 的原生安装脚本。
12
+
13
+ 需要受控的 LLM/Embedding 回调时,在应用中安装可选 peer 依赖:
14
+
15
+ ```sh
16
+ npm install @langchain/core@1.1.47
17
+ ```
18
+
19
+ 导入 Mem0 前,设置 `MEM0_TELEMETRY=false`,并将 `MEM0_DIR` 指向应用管理的数据目录。
20
+ 显式配置 LLM、embedder、`vectorStore.config.dbPath` 和 `historyDbPath`。
21
+
22
+ ## 保存与读取
23
+
24
+ 应用提供 `mem0Config`、已认证的 `userId`、已授权的 `projectId`,以及持久化 `journalPath`:
25
+
26
+ ```ts
27
+ import { Memory } from "mem0ai/oss";
28
+ import { Mem0Memory } from "purra-mem0";
29
+
30
+ const sdk = new Memory(mem0Config);
31
+ const memory = new Mem0Memory({
32
+ client: sdk,
33
+ scope: { user: userId, project: projectId },
34
+ journalPath,
35
+ });
36
+
37
+ async function savePreference() {
38
+ const saved = await memory.add("Reply in Chinese.", {
39
+ source: { id: "preference:language", revision: "1" },
40
+ key: "preference:language:1",
41
+ });
42
+ return memory.get(saved.ids[0]!);
43
+ }
44
+ ```
45
+
46
+ `add` 默认创建已启用记录;需要先审核时设置 `state: "pending"`。
47
+ 记录不可用时,`get` 返回 `undefined`。更新、状态切换和删除需要当前 `version` 与稳定的操作 `key`。
48
+
49
+ ## 接入 Agent
50
+
51
+ 提供 `memoryQuery` 函数,从 Core 的 `ContextRequest` 中选取查询文本:
52
+
53
+ ```ts
54
+ import { RetrieverTool } from "purra";
55
+ import { MemoryContext } from "purra-mem0";
56
+
57
+ const recall = new RetrieverTool({
58
+ retriever: memory,
59
+ name: "recallMemory",
60
+ description: "Recall saved preferences and facts.",
61
+ });
62
+ const context = new MemoryContext({ memory, query: memoryQuery });
63
+ ```
64
+
65
+ 将 `recall.definition` 加入 Agent 工具,或将 `context` 与其他上下文提供器组合。
66
+ 记忆实例已经绑定作用域。需要显式选择记录时,使用 `assembleMemoryContext(memory, ids, allowance)`。
67
+
68
+ ## 受控模型调用
69
+
70
+ 调用需要持久化预算时,使用以下配置替代原生 SDK 客户端。
71
+ 存储路径、向量维度和预算键由应用提供,数值仅用于演示预算配置。
72
+
73
+ `modelTasks` 是 PurrA 扩展工厂提供的执行器。
74
+ 异步回调 `embed(texts, signal)` 需要返回 `EmbeddingResult`,包含向量和服务报告的输入 Token 用量。
75
+
76
+ ```ts
77
+ import { createManagedClient, runModel } from "purra-mem0";
78
+
79
+ const client = await createManagedClient({
80
+ embeddingDims: embeddingDimensions,
81
+ config: { vectorStore: vectorStoreConfig, historyDbPath: historyPath },
82
+ });
83
+ const memory = new Mem0Memory({
84
+ client,
85
+ scope: { user: userId, project: projectId },
86
+ journalPath,
87
+ allowInference: true,
88
+ providers: {
89
+ budget: {
90
+ key: budgetKey,
91
+ maxLlmCalls: 4,
92
+ maxEmbeddingCalls: 64,
93
+ maxInputChars: 100_000,
94
+ maxOutputTokens: 8192,
95
+ resultCapacityTargetTokens: 2048,
96
+ },
97
+ complete: runModel(modelTasks),
98
+ embed,
99
+ },
100
+ });
101
+ ```
102
+
103
+ 通过 `memory.budgetUsage()` 查看已准入调用和报告用量。原生 SDK 模式的内部用量记为未知。
104
+
105
+ `resultCapacityTargetTokens` 用于预留单次记忆结果的预期容量,并不是 Provider 生成上限。
106
+ `runModel()` 会保留 Run 的用户生成上限,并把该值记录为工作流容量目标。
107
+
108
+ ## 提取与审查
109
+
110
+ 配置受控模型调用并设置 `allowInference: true` 后:
111
+
112
+ ```ts
113
+ import { MemoryWorkflow, type MemorySource } from "purra-mem0";
114
+
115
+ const workflow = new MemoryWorkflow(memory);
116
+
117
+ async function capture(
118
+ messages: readonly { role: "user" | "assistant"; content: string }[],
119
+ source: MemorySource,
120
+ operationKey: string,
121
+ ) {
122
+ return workflow.capture(messages, { source, key: operationKey });
123
+ }
124
+ ```
125
+
126
+ 此配置审查候选并让其保持待处理。需要应用决定时,为 `MemoryWorkflow` 传入
127
+ `policy(candidate, review)` 和稳定的 `policyRevision`。
128
+ 策略返回已授权且保留审查键的 `MemoryResolution`,或返回 `undefined` 保持待处理;策略可以是异步函数。
129
+
130
+ ## 生命周期
131
+
132
+ 关闭前调用 `await memory.drain()` 和 `memory.close()`,再关闭 SDK 与模型服务资源。
133
+ 分页、来源撤回、证据校验和不确定写入的处理见[记忆生命周期指南](../README.zh-CN.md)。
@@ -0,0 +1,29 @@
1
+ import type { ContextBlock, ContextEvidenceReceipt, ContextBudget, ContextBudgetClaim, ContextBundle, ContextProvider, ContextRequest } from "purra";
2
+ import { Mem0Memory } from "./memory.js";
3
+ export declare class MemoryContext implements ContextProvider {
4
+ #private;
5
+ constructor(options: {
6
+ memory: Mem0Memory;
7
+ query: (request: ContextRequest) => string;
8
+ countTokens?: (content: string) => number;
9
+ name?: string;
10
+ desiredTokens?: number;
11
+ limit?: number;
12
+ });
13
+ describeContextDemands(): readonly ContextBudgetClaim[];
14
+ buildContext(request: ContextRequest, budget: ContextBudget, signal?: AbortSignal): Promise<ContextBundle>;
15
+ }
16
+ export interface MemoryContextResult {
17
+ readonly block?: ContextBlock;
18
+ readonly included: readonly string[];
19
+ readonly deferred: readonly string[];
20
+ readonly missing: readonly string[];
21
+ readonly receipts: readonly ContextEvidenceReceipt[];
22
+ }
23
+ /** Fresh authorized records; shared by Agent and non-Run consumers. */
24
+ export declare function assembleMemoryContext(memory: Mem0Memory, ids: readonly string[], allowance: number, options?: {
25
+ name?: string;
26
+ countTokens?: (content: string) => number;
27
+ expectedEpoch?: number;
28
+ signal?: AbortSignal;
29
+ }): Promise<MemoryContextResult>;
@@ -0,0 +1,69 @@
1
+ import { estimateJsonTokens } from "purra";
2
+ import { Mem0Memory, positiveInteger, requiredText } from "./memory.js";
3
+ export class MemoryContext {
4
+ #memory;
5
+ #query;
6
+ #countTokens;
7
+ #name;
8
+ #desiredTokens;
9
+ #limit;
10
+ constructor(options) {
11
+ this.#memory = options.memory;
12
+ this.#query = options.query;
13
+ this.#countTokens = options.countTokens ?? estimateJsonTokens;
14
+ this.#name = requiredText(options.name ?? "memory", "context name", 128);
15
+ this.#desiredTokens = positiveInteger(options.desiredTokens ?? 1024, "desiredTokens", 1_000_000);
16
+ this.#limit = positiveInteger(options.limit ?? 8, "limit");
17
+ if (typeof this.#query !== "function" || typeof this.#countTokens !== "function")
18
+ throw new TypeError("query and countTokens must be host functions");
19
+ }
20
+ describeContextDemands() {
21
+ return [{ name: this.#name, desiredTokens: this.#desiredTokens }];
22
+ }
23
+ async buildContext(request, budget, signal) {
24
+ const allowance = budget.contextAllocations[this.#name] ?? 0;
25
+ if (!allowance)
26
+ return { blocks: [] };
27
+ const epoch = this.#memory.epoch;
28
+ const hits = await this.#memory.retrieve({ query: this.#query(request), limit: this.#limit, scope: {} }, signal);
29
+ const result = await assembleMemoryContext(this.#memory, hits.map(hit => hit.id), allowance, { name: this.#name, countTokens: this.#countTokens, expectedEpoch: epoch, ...(signal ? { signal } : {}) });
30
+ return { blocks: result.block ? [result.block] : [] };
31
+ }
32
+ }
33
+ /** Fresh authorized records; shared by Agent and non-Run consumers. */
34
+ export async function assembleMemoryContext(memory, ids, allowance, options = {}) {
35
+ if (!Number.isSafeInteger(allowance) || allowance < 0 || allowance > 1_000_000)
36
+ throw new TypeError("invalid context allowance");
37
+ const name = requiredText(options.name ?? "memory", "context name", 128);
38
+ const countTokens = options.countTokens ?? estimateJsonTokens;
39
+ const epoch = options.expectedEpoch ?? memory.epoch;
40
+ memory.assertEpoch(epoch);
41
+ const hits = await memory.select(ids, options.signal ? { signal: options.signal } : {});
42
+ const selected = [], deferred = [], rows = [];
43
+ let content = "", tokenCount = 0;
44
+ for (const hit of hits) {
45
+ const row = { id: hit.id, version: hit.version, text: hit.content, sourceId: hit.metadata.sourceId,
46
+ sourceRevision: hit.metadata.sourceRevision, metadata: hit.metadata.metadata };
47
+ const candidate = JSON.stringify([...rows, row]);
48
+ let count = countTokens(candidate);
49
+ if (!Number.isSafeInteger(count) || count < 0)
50
+ throw new TypeError("countTokens must return a non-negative integer");
51
+ count = Math.max(count, estimateJsonTokens(candidate));
52
+ if (count <= allowance) {
53
+ rows.push(row);
54
+ selected.push(hit);
55
+ content = candidate;
56
+ tokenCount = count;
57
+ }
58
+ else
59
+ deferred.push(hit.id);
60
+ }
61
+ const receipts = Object.freeze(selected.map(hit => Object.freeze({ evidenceId: hit.metadata.evidenceId,
62
+ contextBlock: name, source: hit.source, itemId: hit.id, version: String(hit.version) })));
63
+ await memory.validateEvidence(receipts, options.signal ? { signal: options.signal } : {});
64
+ memory.assertEpoch(epoch);
65
+ const found = new Set(hits.map(hit => hit.id));
66
+ return Object.freeze({ ...(selected.length ? { block: { name, content, tokenCount, untrusted: true, evidence: receipts } } : {}),
67
+ included: Object.freeze(selected.map(hit => hit.id)), deferred: Object.freeze(deferred),
68
+ missing: Object.freeze([...new Set(ids.filter(id => !found.has(id)))]), receipts });
69
+ }
@@ -0,0 +1,10 @@
1
+ /** Optional Node.js integration; not imported by PurrA Core. */
2
+ export { MemoryError } from "./journal.js";
3
+ export { Mem0Memory } from "./memory.js";
4
+ export type { Mem0Client, MemoryScope, MemorySource, MemoryRecord, MemoryMetadata, MemoryFilters, MemoryPage, MemoryOperation, MemoryRef, MemoryLink, MemoryLinkPage, MemoryResolution, MemoryReview, MemoryMatch } from "./memory.js";
5
+ export type { MemoryContextResult } from "./context.js";
6
+ export { MemoryContext, assembleMemoryContext } from "./context.js";
7
+ export { createManagedClient, runModel } from "./providers.js";
8
+ export type { MemoryBudget, MemoryProviders, MemoryUsage, EmbeddingResult, ManagedMem0Config } from "./providers.js";
9
+ export { MemoryWorkflow } from "./workflow.js";
10
+ export type { MemoryWorkflowResult, MemoryDecisionPolicy } from "./workflow.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ /** Optional Node.js integration; not imported by PurrA Core. */
2
+ export { MemoryError } from "./journal.js";
3
+ export { Mem0Memory } from "./memory.js";
4
+ export { MemoryContext, assembleMemoryContext } from "./context.js";
5
+ export { createManagedClient, runModel } from "./providers.js";
6
+ export { MemoryWorkflow } from "./workflow.js";
@@ -0,0 +1,101 @@
1
+ import type { MemoryUsage } from "./providers.js";
2
+ import type { MemoryMetadata, MemoryResolution, MemoryRef, MemoryReview, MemoryLink } from "./memory.js";
3
+ export declare class MemoryError extends Error {
4
+ readonly code: string;
5
+ constructor(code: string);
6
+ }
7
+ export interface Metadata {
8
+ purra_scope: string;
9
+ purra_store: string;
10
+ purra_operation: string;
11
+ purra_version: number;
12
+ purra_state: "active" | "pending" | "disabled";
13
+ purra_source: string;
14
+ purra_revision: string;
15
+ purra_inferred: boolean;
16
+ purra_expires: string | null;
17
+ purra_metadata: MemoryMetadata;
18
+ purra_reason: string | null;
19
+ purra_created: string;
20
+ purra_updated: string;
21
+ }
22
+ export interface ItemView {
23
+ version: number;
24
+ state: Metadata["purra_state"];
25
+ metadata: MemoryMetadata;
26
+ reason: string | null;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ resolution?: string | null;
30
+ }
31
+ export interface Item {
32
+ id: string;
33
+ meta: Metadata;
34
+ hash: string;
35
+ deleted: boolean;
36
+ view?: ItemView;
37
+ }
38
+ export declare function itemView(row: Item): ItemView;
39
+ export interface Plan {
40
+ kind: string;
41
+ target: string | null;
42
+ meta: Metadata | null;
43
+ hash?: string | null;
44
+ discarding?: boolean;
45
+ budget?: string;
46
+ provider_error?: string;
47
+ providers_verified?: boolean;
48
+ resolution?: MemoryResolution;
49
+ review_key?: string;
50
+ review_epoch?: number;
51
+ review_refs?: readonly MemoryRef[];
52
+ review?: Omit<MemoryReview, "proposal" | "key">;
53
+ policy_hash?: string;
54
+ changes?: Partial<ItemView>;
55
+ link?: Omit<MemoryLink, "key" | "valid">;
56
+ }
57
+ export interface OperationRow {
58
+ key: string;
59
+ fingerprint: string;
60
+ state: "running" | "unknown" | "failed" | "complete" | "discarded";
61
+ plan: Plan;
62
+ ids: string[] | null;
63
+ }
64
+ /** Only control metadata is stored here. Content and vectors remain in Mem0. */
65
+ export declare class Journal {
66
+ private readonly scope;
67
+ private readonly db;
68
+ readonly store: string;
69
+ constructor(path: string, scope: string);
70
+ private transaction;
71
+ operation(key: string): OperationRow | undefined;
72
+ begin(key: string, fingerprint: string, plan: Plan): OperationRow | undefined;
73
+ savePlan(key: string, plan: Plan): void;
74
+ saveIds(key: string, ids: string[]): void;
75
+ budget(key: string, limits: Record<string, number>): void;
76
+ usage(column: "budget" | "operation", key: string): MemoryUsage;
77
+ admit(budget: string, operation: string | undefined, kind: "llm" | "embedding", chars: number, reserve: number): string;
78
+ settle(id: string, state: string, inputTokens?: number | null, generationTokens?: number | null): void;
79
+ providerError(key: string, code: string): void;
80
+ verifyProviders(key: string): void;
81
+ fail(key: string, dispatched: boolean): void;
82
+ discard(key: string): void;
83
+ revoked(source: string, revision: string): boolean;
84
+ assertSource(meta: Pick<Metadata, "purra_source" | "purra_revision">): void;
85
+ revokeSource(key: string, fingerprint: string, source: string, revision: string | null): void;
86
+ item(id: string): Item | undefined;
87
+ resolve(key: string, fingerprint: string, plan: Plan, epoch: number): void;
88
+ control(key: string, fingerprint: string, plan: Plan, epoch: number, refs: readonly MemoryRef[], changes: readonly Partial<ItemView>[]): void;
89
+ assertSnapshot(plan: Plan): void;
90
+ links(id: string, after: string | undefined, limit: number): {
91
+ key: string;
92
+ data: NonNullable<Plan["link"]>;
93
+ }[];
94
+ reviewPlan(key: string): Plan;
95
+ finishReview(key: string, plan: Plan): void;
96
+ items(state: string | null, after: string | undefined, limit: number): Item[];
97
+ writing(id: string): boolean;
98
+ commit(key: string, records: Item[]): void;
99
+ get epoch(): number;
100
+ close(): void;
101
+ }