@h-ai/ai 0.1.0-alpha.67 → 0.1.0-alpha.68

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.en.md CHANGED
@@ -24,8 +24,11 @@ AI capabilities through a unified `ai` service: LLM chat, image generation, tool
24
24
  - `ai.audio`: ASR and TTS with full and streaming calls for OpenAI / MiMo / Qwen / Doubao.
25
25
  - `ai.image`: OpenAI GPT Image, Google Gemini Image / Nano Banana, Qwen-Image 2.0/3.0, Seedream 4.x/5.x, and Pollinations free-quota models.
26
26
  - `ai.a2a`: Agent-to-Agent request handling and remote calls.
27
- - `@h-ai/ai/client`: lightweight frontend client for API services.
28
- - `AIStoreProvider`: shared storage abstraction. Defaults to a temporary in-process store without databases; automatically uses persistent DB storage when reldb + vecdb are initialized.
27
+ - `@h-ai/ai/client`: standalone browser Audio WebSocket client.
28
+
29
+ `@h-ai/ai/client` exports only `createAudioClient({ url, getTicket })` and Audio client types. Use typed domain methods from `@h-ai/api-client` for HTTP. Default chat completions are non-streaming; bind application contracts for streaming and A2A. Redundant string-path clients and SSE helpers have been removed. The browser does not export the Node `ai` singleton.
30
+
31
+ - `AIStoreProvider`: storage abstraction. Production requires `stateScope: 'shared'`; the built-in provider needs initialized PostgreSQL/MySQL and a network vecdb. Development/explicit tests may use a temporary store.
29
32
 
30
33
  For the full method list, error codes, and longer examples, see [REFERENCE.md (Chinese)](./REFERENCE.md).
31
34
 
@@ -33,7 +36,7 @@ For the full method list, error codes, and longer examples, see [REFERENCE.md (C
33
36
 
34
37
  ### Quick start
35
38
 
36
- #### LLM-only (no database dependencies)
39
+ #### Development LLM-only (no database dependencies)
37
40
 
38
41
  ```ts
39
42
  import { ai } from '@h-ai/ai'
@@ -54,7 +57,7 @@ const result = await ai.llm.chat({
54
57
  await ai.close()
55
58
  ```
56
59
 
57
- Without initialized reldb/vecdb, AI stores session/runtime state in a temporary in-process store. Data does not survive `ai.close()`, process exit, or instance switching. Use the DB provider below for persistent Memory, Context, Persona, or sessions across restarts.
60
+ Development/explicit tests without initialized reldb/vecdb use a temporary process store, lost on close, exit, or instance switching. When either `NODE_ENV` or `HAI_CORE_ENV` is production, temporary stores, SQLite and local vector backends fail initialization. Initialize shared RelDB and network vecdb first, or supply a shared provider with equivalent atomic guarantees.
58
61
 
59
62
  #### Persistent DB provider (reldb + vecdb)
60
63
 
@@ -63,31 +66,42 @@ import { ai } from '@h-ai/ai'
63
66
  import { reldb } from '@h-ai/reldb'
64
67
  import { vecdb } from '@h-ai/vecdb'
65
68
 
66
- await reldb.init({ type: 'sqlite', database: './ai.db' })
67
- await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
68
-
69
- const init = await ai.init({
70
- llm: {
71
- model: 'gpt-4o-mini',
72
- apiKey: process.env.HAI_AI_LLM_APIKEY,
73
- },
74
- })
75
- if (!init.success) {
76
- // 按 init.error.code 处理配置/依赖错误
77
- }
78
-
79
- const result = await ai.llm.chat({
80
- messages: [{ role: 'user', content: '你好!' }],
81
- })
82
- if (result.success) {
69
+ // 开发/测试使用本地后端;生产改为共享 PostgreSQL/MySQL 与网络 vecdb。
70
+ const failures: unknown[] = []
71
+ try {
72
+ const database = await reldb.init({ type: 'sqlite', database: './ai.db' })
73
+ if (!database.success)
74
+ throw new Error(database.error.message)
75
+ const vectors = await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
76
+ if (!vectors.success)
77
+ throw new Error(vectors.error.message)
78
+ const initialized = await ai.init({
79
+ llm: { model: 'gpt-4o-mini', apiKey: process.env.HAI_AI_LLM_APIKEY },
80
+ })
81
+ if (!initialized.success)
82
+ throw new Error(initialized.error.message)
83
+ const result = await ai.llm.chat({ messages: [{ role: 'user', content: 'Hello!' }] })
84
+ if (!result.success)
85
+ throw new Error(result.error.message)
83
86
  const reply = result.data.choices[0]?.message.content ?? ''
84
87
  }
85
-
86
- await ai.close()
87
- await vecdb.close()
88
- await reldb.close()
88
+ catch (error) {
89
+ failures.push(error)
90
+ }
91
+ finally {
92
+ await ai.close()
93
+ for (const service of [vecdb, reldb]) {
94
+ const closed = await service.close()
95
+ if (!closed.success)
96
+ failures.push(new Error(closed.error.message))
97
+ }
98
+ }
99
+ if (failures.length)
100
+ throw new AggregateError(failures, 'AI operation or cleanup failed')
89
101
  ```
90
102
 
103
+ Concurrent MySQL first startup accepts only the explicit mysql2 ER_DUP_KEYNAME/1061 error, then verifies the actual columns and order, non-unique full columns, ascending visible BTREE, and no expressions. An existing index with the wrong definition fails. The account needs table creation and INDEX privileges; metadata, permission, or other DDL failures return CONFIGURATION_ERROR from ai.init. All started DDL settles before return. MySQL DDL is not rolled back: repair privileges/schema before retrying partial initialization. Check each initialization result, reject business traffic on failure, and close AI, vecdb, and reldb in reverse order.
104
+
91
105
  #### Custom StoreProvider
92
106
 
93
107
  ```ts
@@ -110,7 +124,9 @@ await ai.close()
110
124
  - Lifecycle: `await ai.init(config, options?)` / `await ai.close()`; closure waits for custom `AIStoreProvider.close()`.
111
125
  - Domain methods return `HaiResult<T>` or `Promise<HaiResult<T>>`, with failures expressed by `result.success === false` and `result.error.code`. Streaming `AsyncIterable`, client transports, and third-party callbacks can throw during establishment or iteration.
112
126
  - `ai.tools` and `ai.stream` are pure-function subsystems usable without initialization.
113
- - Without reldb/vecdb, storage is temporary and in-process; with both initialized, DB storage is automatic. Custom providers can encapsulate other backends.
127
+ - Development/tests without reldb/vecdb use temporary storage; both initialized selects DB storage. Production validates the real database/vector backend capability. Custom providers must accurately declare `stateScope`.
128
+
129
+ Inject stores through the public AIStoreProvider extension. Built-in LLM providers, factory dependencies and Context dependencies are internal. Configure model endpoints through configuration; history queries, AskOptions and ChatRecord remain public.
114
130
 
115
131
  ### API overview
116
132
 
@@ -144,6 +160,8 @@ const temp = await ai.llm.chat({
144
160
  })
145
161
  ```
146
162
 
163
+ SSE text decoding follows [WHATWG event-stream rules](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation): split LF/CRLF/CR, multiline data, one optional space, inherited valid id/retry, default message events, and no dispatch of unfinished EOF data. encodeSSE normalizes data line endings and explicitly throws for invalid metadata. Anthropic/MiMo share an internal UTF-8 reader that cancels/releases the body on exit, cancellation, termination or failure, without reconnecting or replaying business operations. Anthropic deadlines cover the body; MiMo retains separate deadlines per HTTP request.
164
+
147
165
  #### Tool calls
148
166
 
149
167
  ```ts
@@ -180,7 +198,13 @@ const chat = await ai.llm.chat({ messages, tools: registry.getDefinitions() })
180
198
 
181
199
  `ai.a2a.getAgentCard()` returns a complete A2A 0.3 Agent Card with `protocolVersion`, MIME, and streaming capabilities. Kit discovery defaults to `/.well-known/agent-card.json`. `handleRequest()` always returns JSON-RPC/SSE results; uninitialized calls return JSON-RPC errors. Trusted HTTP `agentId` maps to executor `context.user.userName`. JSON-RPC/SSE are supported; push notifications, gRPC, and REST are not claimed. Applications remain responsible for business resource authorization.
182
200
 
183
- A2A task/message tables initialize during `ai.init()`. Tasks are read in trusted `agentId` scopes, including get/cancel/continuation. Anonymous requests share one scope, so deployments requiring isolation must enable authentication. `listMessages` is a server administration API with `callerId`, `contextId`, and `since` filters; never expose it to unauthorized clients. Legacy tasks lack reliable subjects; upgrades neither infer ownership nor delete data. After review, applications must migrate `object_id` to `user:<agentId>` (`anonymous` for anonymous records). Legacy messages lacking subject/context fields do not match corresponding filters.
201
+ A2A journal, context ownership and message tables initialize during `ai.init()`. Tasks use trusted `agentId` scopes for get/cancel/continuation. Anonymous requests share one principal; authenticate at the trusted transport boundary in production. `listMessages` is a server administration API with `callerId`, `contextId`, and `since`; never expose it to unauthorized clients.
202
+
203
+ Tasks and events use one shared journal, without a separate legacy TaskStore. Push notification configuration and authenticated extended card RPCs return standard SDK unsupported/unconfigured errors without executing tasks. Executor types remain public; internal handler/Store assembly types are outside the public contract.
204
+
205
+ Inbound messages must persist before the executor starts, with shared generation/deadline checks repeated after that wait. Agent messages await logging inside the existing bounded event queue before journal publication. Logging failure produces a failed/interrupted Task rather than a false success; retrying the same message does not rerun business effects. Logs and the journal are separate records: a log is not proof of delivery. Reconcile possible effects using the Task's `requiresEffectReconciliation` marker. Query failures return `A2A_LIST_MESSAGES_FAILED` (984) without raw SQL or connection details.
206
+
207
+ Task and events publish in one CAS, so any node can subscribe. A retry with identical message ID/content reuses the current result; changed content or an older round fails. Cross-node cancellation invokes the real execution owner's executor.cancelTask. Pending cancellation blocks new messages on the same Task so a late callback cannot affect a later execution. Input-required tasks can continue after node restart. Expired leases, absolute deadlines, or an executor returning only working become failed/interrupted with `metadata.haiExecution.requiresEffectReconciliation=true`; late owners cannot write and unknown external effects are never replayed automatically. `a2a.execution` defaults to `leaseMs: 30000`, `pollIntervalMs: 250`, `maxExecutionMs: 300000`. Limits are 1 MiB/message, 8 MiB/journal, 2048 events/round, 256 receipts/Task and 32 reference tasks. Use stable business idempotency keys and reconcile unknown effects. See the [state/deployment matrix (Chinese)](../../.agents/instructions/distributed-state.md) for guarantees and validation boundaries.
184
208
 
185
209
  #### MCP Server
186
210
 
@@ -202,6 +226,16 @@ await server.connect(new StreamableHTTPServerTransport({ sessionIdGenerator: ()
202
226
 
203
227
  #### Memory / RAG / Knowledge
204
228
 
229
+ Each Knowledge ingestion creates a unique `revision` and chunk IDs. It prepares all chunks, embeddings and entities, persists a staging manifest, writes vectors, then publishes the complete document and entity snapshot with shared Store CAS. Only one writer can rebuild a document at a time. Failure preserves the complete old version; shorter replacements cannot expose old trailing chunks. Retrieval filters unpublished revisions and retries around publication; sustained contention returns an error. Candidate expansion and cleanup backlogs can increase query cost.
230
+
231
+ Successful ingestion returns `revision` and `cleanupPending`. A true flag means the new version is published but old vectors still require cleanup. Paginate `repair({ collection, offset, limit })` regularly across every manifest; setup checks the first 100. Staging expires after five minutes according to the storage clock and an expired writer cannot publish. Abandoned writes retain permanent cleanup tombstones so later scans can remove delayed external writes; do not prune these with logs. SQL and vector storage do not share a transaction. Removal hides data first and reports cleanup failures. Collection deletion retains the empty vector collection and generation tombstones for compensation; setup never revives the previous generation.
232
+
233
+ The DB Store propagates collection and vector failures. LanceDB uses persistent primary-key atomic merge. Qdrant writes/deletes wait until applied and counts are exact. Multiple nodes must share relational storage and a server vector backend. Custom KnowledgeStore implementations must provide the same publication and compensation contract.
234
+
235
+ With LanceDB, collection dimensions and metrics persist in the Arrow schema; connection metrics only affect new collections. Reuse the same persistent directory after restart and rebuild collections when embedding dimensions change. Local paths on separate nodes do not share data; distributed Knowledge should use a server vector backend.
236
+
237
+ Knowledge `retrieve/ask` and the default DB Store forward `filter` to the vector provider and propagate validation/query failures. Knowledge returns `KNOWLEDGE_RETRIEVE_FAILED` (`RAG_FAILED` from ask), without disguising failure as empty results. LanceDB supports AND equality on literal top-level keys with string, finite number, boolean, or null values, such as `{ tenant: 'acme', active: true }`. Null never matches missing keys; objects, arrays, and undefined are unsupported. Filtering precedes vector topK. Check null/complex-value semantics separately for other backends. LanceDB metadata now uses native JSON fields and retains nested JSON; rebuild legacy string-metadata collections before writing or filtering.
238
+
205
239
  ```ts
206
240
  const enriched = await ai.memory.injectMemories(messages, { objectId: 'user-001', topK: 5 })
207
241
  if (!enriched.success) {
@@ -221,10 +255,19 @@ const rag = await ai.rag.query('核心架构是什么?', { sources: ['docs'],
221
255
 
222
256
  const setup = await ai.knowledge.setup()
223
257
  if (setup.success) {
224
- await ai.knowledge.ingest({ documentId: 'doc-001', content: markdownText, title: '产品手册' })
258
+ const ingested = await ai.knowledge.ingest({ documentId: 'doc-001', content: markdownText, title: '产品手册' })
259
+ if (!ingested.success)
260
+ return ingested
261
+ if (ingested.data.cleanupPending) {
262
+ const repaired = await ai.knowledge.repair({ offset: 0, limit: 100 })
263
+ if (!repaired.success)
264
+ return repaired
265
+ }
225
266
  }
226
267
  ```
227
268
 
269
+ Knowledge chunks use datapipe units and hard bounds: words for word mode, Unicode code points otherwise, retaining separators from cleaned text. AI defaults remain markdown/maxSize=1500/overlap=200; a smaller maxSize override must also keep inherited overlap below it. Invalid chunk configuration or expressions return KNOWLEDGE_INGEST_FAILED before embedding and revision publication, without an unbounded whole-document fallback.
270
+
228
271
  #### Audio
229
272
 
230
273
  Register audio models in `ai.init()` and map the default transcription/synthesis models first. Credentials fall back to platform environment variables. Enable `inheritLlmApiKey` explicitly only when LLM and audio models are confirmed to share credentials:
@@ -332,6 +375,8 @@ Cancellation, timeout, and connection problems become domain errors: `AbortSigna
332
375
 
333
376
  Browsers/mobile clients use the unified audio WebSocket exposed by `@h-ai/serv`. `@h-ai/ai/client` offers the same `audio.*` API as Node, hiding transport details. Browser clients distinguish completion, cancellation (`AUDIO_CANCELLED`), and abnormal disconnect (`AUDIO_CONNECTION_FAILED`); cancellation or disconnect before `end` throws the domain code. `synthesize` never treats incomplete audio as success.
334
377
 
378
+ IndexTTS pairs the same Undici version's fetch, Agent, and multipart types on Node. Model `timeout` covers response-header and body waits; user cancellation returns `AUDIO_CANCELLED`, and deadlines return `AUDIO_TIMEOUT`. Dedicated connections close after success, failure, or cancellation. Segmented streaming fallback retains errors during iteration.
379
+
335
380
  > For self-hosted CPU/GPU model services (faster-whisper / IndexTTS / Qwen3-4B), weight downloads, and offline bundles, see [models/ (Chinese)](./models/README.md). Images and weights prefer ModelScope, with automatic HuggingFace mirror fallback.
336
381
 
337
382
  #### Image generation
@@ -364,8 +409,14 @@ if (result.success) {
364
409
 
365
410
  `referenceImages` is optional. Public APIs accept only image bytes and MIME; providers convert multipart, `inlineData`, and Data URLs. Set credentials on model entries or with `HAI_AI_IMAGE_<PROVIDER>_APIKEY` / vendor environment variables. Qwen and Seedream temporary URLs are downloaded immediately inside providers and never exposed publicly. See [REFERENCE.md (Chinese)](./REFERENCE.md#image) for model details and API differences.
366
411
 
412
+ Downloads require public HTTPS on default port 443 without URL credentials. Socket DNS lookup validates every returned address and connects using those same results. Private, loopback, link-local, mapped/translated, and reserved IPv4/IPv6 addresses are blocked. Each of at most three redirects is checked again. Downloads use isolated connections without vendor authentication or application cookies. `baseUrl` is trusted operator configuration; vendor API calls reject redirects.
413
+
414
+ `image.maxImageBytes` defaults to 20971520 (20 MiB; maximum 100 MiB) per output/reference image. `image.maxImages` defaults to 4 (maximum 16) for output/reference counts, with sequential URL downloads. Actual streamed bytes, including decompressed bytes, are bounded and excess responses are cancelled. JSON responses are limited to `max(64 KiB, 2 × maxImageBytes)` and error bodies to 64 KiB. Model `timeout` defaults to 120000ms for the entire vendor request, redirects, and downloads; `signal` cancels the whole process. Outputs require matching PNG/JPEG/GIF/WebP MIME and magic bytes; SVG is rejected. Magic checks do not decode complete images or verify dimensions. Address rejection, limits, and deadlines return `IMAGE_DOWNLOAD_BLOCKED`, `IMAGE_TOO_LARGE`, and `IMAGE_TIMEOUT`; check success before use.
415
+
367
416
  #### Context manager
368
417
 
418
+ Automatic memory extraction runs locally in the background; `pendingMemoryTasks=0` does not imply success. `flush()` waits for the batch and returns `MEMORY_EXTRACT_FAILED` with a count for thrown errors or failed HaiResults. Concurrent observers share that result. Returning acknowledges the report; later flushes observe new tasks without replaying partially completed effects. `save()`, `reset({ waitForMemoryTasks: true })`, and `consolidate()` return that failure before subsequent writes. Reconcile external effects after a process crash.
419
+
369
420
  ```ts
370
421
  const manager = ai.context.createManager({
371
422
  scope: { objectId: 'user-001', sessionId: 'sess-001' },
@@ -375,7 +426,9 @@ const manager = ai.context.createManager({
375
426
  })
376
427
  if (manager.success) {
377
428
  const reply = await manager.data.chat('你好')
378
- await manager.data.save()
429
+ const saved = await manager.data.save()
430
+ if (!saved.success)
431
+ return saved
379
432
  // reset 生命周期完整:终止活动生成、清空消息/摘要/轮次,默认保留系统提示词
380
433
  await manager.data.reset() // 可传 { preserveSystemPrompt, cancelActiveTurn, waitForMemoryTasks }
381
434
  }
@@ -392,12 +445,15 @@ With `turnCommit: 'manual'`, results are not stored automatically. A `turnId` is
392
445
  `chatStream` registers a turn and emits `turn_started` **before calling the model** (`turn_started → delta* → done`, or `turn_started → delta* → cancelled` on cancellation). Even if `AbortSignal` cancels generation midway, the caller gets `turnId` and generated text for committing actual content:
393
446
 
394
447
  ```ts
395
- const m = ai.context.createManager({ turnCommit: 'manual' /* ... */ }).data
448
+ const created = ai.context.createManager({ scope: { objectId: 'user-001', sessionId: 'sess-001' }, turnCommit: 'manual' })
449
+ if (!created.success)
450
+ return created
451
+ const m = created.data
396
452
  const controller = new AbortController()
397
453
 
398
454
  for await (const ev of m.chatStream('请展开讲讲', { signal: controller.signal })) {
399
455
  if (ev.type === 'turn_started') {
400
- m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
456
+ await m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
401
457
  }
402
458
  else if (ev.type === 'delta') {
403
459
  feedTts(ev.text) // 边生成边合成播放
@@ -450,6 +506,7 @@ await ai.persona.save({
450
506
  // 组合出可直接喂给 ContextManager 的系统提示词(systemPrompt + traits)
451
507
  const composed = await ai.persona.compose('xiaoq')
452
508
  const manager = ai.context.createManager({
509
+ scope: { objectId: 'user-001', sessionId: 'persona-session' },
453
510
  systemPrompt: composed.data,
454
511
  memory: { enable: true, enableExtract: true, scope: { personaId: 'xiaoq' } },
455
512
  })
@@ -457,6 +514,8 @@ const manager = ai.context.createManager({
457
514
 
458
515
  `ai.persona` exposes `save` / `get` / `update` / `remove` / `list` / `compose`. Profiles are shared globally, not isolated by `objectId`.
459
516
 
517
+ ### Configuration
518
+
460
519
  ```yaml
461
520
  llm:
462
521
  apiKey: ''
@@ -512,9 +571,17 @@ memory:
512
571
  ```
513
572
 
514
573
  - **`native` (default, recommended)**: HAI's native engine reuses vecdb, reldb, LLM, and Embedding. `extract` uses **Mem0-style batch merging**: one LLM call decides ADD / UPDATE / DELETE / NONE for extracted facts and related existing memories, providing incremental updates, deduplication, contradiction removal, and `category` labels. `maxEntriesPerObject`, `maxEntriesGlobal`, `recencyDecay`, `embeddingEnabled`, and `writebackRelatedTopK` apply. Eviction is partitioned by `objectId`, so heavy writes by one subject do not evict others' memories. `scope` candidates are narrowed by `objectId` (≤ `maxEntriesPerObject`); PostgreSQL also pushes scope into a JSONB containment query using a **GIN index**. SQLite/MySQL fall back to equivalent in-memory matching.
515
- - **`mem0` (mem0ai/oss)**: directly embeds the `mem0ai/oss` `Memory` engine without cloud services. LLM/Embedder come from `llm` configuration; `qdrant` / `pgvector` can reuse vecdb. Unmappable backends such as `lancedb` / `chroma` fail fast unless `memory.allowEphemeralFallback: true` explicitly allows mem0 in-memory storage, preventing silent memory loss after restart. History is disabled by default.
574
+ - **`mem0` (mem0ai/oss)**: directly embeds the `mem0ai/oss` `Memory` engine without cloud services. LLM/Embedder come from `llm` configuration; `qdrant` can reuse vecdb. The adapter maps only Qdrant with complete connection settings. Production (`NODE_ENV` or `HAI_CORE_ENV=production`) rejects actual ephemeral fallback even when `memory.allowEphemeralFallback: true`; missing backends also fail. Explicit fallback is limited to development or `HAI_E2E=1` / `HAI_E2E_MOCK=1`. History is disabled by default.
516
575
 
517
- Both providers share `ai.memory.*` (`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`) with `objectId` isolation and key-value `scope` filters such as `{ topicId, personaId }`. `recall` / `list` / `listPage` / `clear` strictly filter scope; `clear` with `types` / `scope` deletes only jointly matching entries. mem0 `extract` uses the shared extractor for classification/scoring (`types` / `model` / `minImportance` / `systemPrompt`), then writes with `infer:false`, preserving `hai_type` / `hai_importance`. Its `recall` supports `types` and `recencyWeight` decay, matching native behavior. One difference: mem0 `update` of type/importance/metadata rebuilds memory and changes `id`; native keeps IDs stable.
576
+ Both providers share `ai.memory.*` (`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`) with `objectId` isolation and key-value `scope` filters such as `{ topicId, personaId }`. `recall` / `list` / `listPage` / `clear` strictly filter scope; `clear` with `types` / `scope` deletes only jointly matching entries. mem0 `extract` uses the shared extractor for classification/scoring (`types` / `model` / `minImportance` / `systemPrompt`), then writes with `infer:false`, preserving `hai_type` / `hai_importance`. Its `recall` supports `types` and `recencyWeight` decay, matching native behavior. Mem0 updates text and metadata in place, preserving `id` and creation time. SDK queries use `filters.user_id`; writes and whole-subject deletion use `userId`. With no SDK list cursor, `list`, `listPage`, and filtered `clear` support at most 1000 raw subject candidates, including expired records. One extra candidate detects truncation; exceeding the bound returns `MEMORY_LIST_INCOMPLETE` (906), without a fabricated total or partial filtered deletion. Complete candidates are filtered by UTC expiration date, type and scope before pagination. Whole-subject `deleteAll` is independent of this listing limit. Public empty-filter clear is rejected; only trusted administration may explicitly call `admin.clearAll({ confirm: true })`.
577
+
578
+ Mem0 3.3.1 still calls `QdrantClient.search`. The catalog and AI optional peer pin the tested `@qdrant/js-client-rest` version `1.18.0`; startup checks this public method and rejects incompatible installations. Do not override it with 1.19, which removed that method.
579
+
580
+ The adapter injects a dedicated client through the SDK's public `client` configuration. Mem0 Qdrant upserts and deletes use `wait: true`, awaiting server application before readback or the next deletion batch.
581
+
582
+ Mem0 with pgvector is rejected in every environment: the SDK privately owns a PostgreSQL connection without a public close API, so the framework cannot manage its lifecycle. Use `memory.provider: native` for pgvector; native support is retained. `disableHistory: true` selects a resource-free history implementation and creates no history SQLite file. The factory awaits backend readiness through one bounded public read before reporting success.
583
+
584
+ SDK add receipts omit full business fields, so `add` / `extract` read the stored record before returning. Extraction waits for all pending writes. A write or readback failure sets `error.ext.requiresEffectReconciliation=true` and retains known IDs in `memoryIds` (possibly empty if receipts were lost). Reconcile subject records before recovery; blindly repeating add/extract can duplicate committed writes. The framework does not replay automatically.
518
585
 
519
586
  **Candidate pools and scope recall misses**: filtering scope after truncating to `topK` can let higher-ranked memories from other topics/personas displace the target scope. `recall` / `injectMemories` therefore fetch `topK × candidateMultiplier` candidates (default multiplier 5), filter, then take `topK`. Increase `candidateMultiplier` for finer isolation such as `topicId` + `personaId`:
520
587
 
@@ -560,8 +627,8 @@ Common error ranges:
560
627
  - `hai:ai:600-701`:Retrieval/RAG。
561
628
  - `hai:ai:800-805`:Knowledge。
562
629
  - `hai:ai:050-059`:Audio。
563
- - `hai:ai:060-065`:Image。
564
- - `hai:ai:900-905`:Memory。
630
+ - `hai:ai:060-068`: Image, including blocked downloads, resource limits, and total deadlines.
631
+ - `hai:ai:900-906`:Memory。
565
632
  - `hai:ai:980-984`:A2A。
566
633
 
567
634
  ## Development and testing
@@ -573,6 +640,8 @@ pnpm --filter @h-ai/ai test
573
640
  pnpm --filter @h-ai/ai build
574
641
  ```
575
642
 
643
+ Public JSDoc examples import actual package entrypoints; internal factory examples are marked with their module context and compiled against real local declarations. Run `pnpm check:jsdoc --package ai` from the repository root to verify coverage and example types. Ordinary business calls check HaiResult; streams and the browser AudioClient retain their explicit throwing contracts.
644
+
576
645
  ## License
577
646
 
578
647
  Apache-2.0
package/README.ja.md CHANGED
@@ -24,8 +24,11 @@
24
24
  - `ai.audio`:通常・ストリーミングの音声認識(ASR)と音声合成(TTS)。OpenAI / MiMo / Qwen / Doubao に対応。
25
25
  - `ai.image`:OpenAI GPT Image、Google Gemini Image / Nano Banana、Qwen-Image 2.0/3.0、Seedream 4.x/5.x、Pollinations の無料枠モデルによる画像生成。
26
26
  - `ai.a2a`:Agent-to-Agent のリクエスト処理とリモート呼び出し。
27
- - `@h-ai/ai/client`:API サービスと連携する軽量フロントエンドクライアント。
28
- - `AIStoreProvider`:共通のストレージ抽象。DB がなければプロセス内の一時 Store、reldb + vecdb が初期化済みなら永続 DB Provider を自動使用。
27
+ - `@h-ai/ai/client`:独立したブラウザー Audio WebSocket クライアント。
28
+
29
+ `@h-ai/ai/client` は `createAudioClient({ url, getTicket })` と Audio クライアント型のみを公開します。HTTP は `@h-ai/api-client` の型付きドメインメソッドを使います。既定のチャット補完は非ストリーミングで、ストリームと A2A はアプリ独自の contract に結び付けます。文字列パスの旧クライアントと重複 SSE helper は削除しました。ブラウザーには Node の `ai` シングルトンを公開しません。
30
+
31
+ - `AIStoreProvider`:ストレージ抽象。本番は `stateScope: 'shared'` が必須で、組み込み実装は PostgreSQL/MySQL とネットワーク vecdb の初期化を必要とします。開発・明示的テストでは一時 Store を利用できます。
29
32
 
30
33
  詳しいメソッド一覧・エラーコード・長い例は [REFERENCE.md(中国語)](./REFERENCE.md) を参照してください。
31
34
 
@@ -33,7 +36,7 @@
33
36
 
34
37
  ### クイックスタート
35
38
 
36
- #### LLM のみ(DB 依存なし)
39
+ #### 開発用 LLM のみ(DB 依存なし)
37
40
 
38
41
  ```ts
39
42
  import { ai } from '@h-ai/ai'
@@ -54,7 +57,7 @@ const result = await ai.llm.chat({
54
57
  await ai.close()
55
58
  ```
56
59
 
57
- reldb/vecdb が未初期化の場合、AI はセッションなどの状態をプロセス内の一時 Store に保存します。`ai.close()`、プロセス終了、インスタンス切り替え後は保持されません。Memory、Context、Persona、セッションを再起動後も保持する場合は以下の DB Provider を使います。
60
+ 開発・明示的テストで reldb/vecdb が未初期化の場合、一時 Store を使い、終了やインスタンス切り替えで失われます。`NODE_ENV` または `HAI_CORE_ENV` が production の場合、一時 Store・SQLite・ローカル vecdb は初期化に失敗します。共有 RelDB とネットワーク vecdb を先に初期化するか、同等の原子的契約を満たす共有 Provider を指定してください。
58
61
 
59
62
  #### 永続 DB Provider(reldb + vecdb)
60
63
 
@@ -63,31 +66,42 @@ import { ai } from '@h-ai/ai'
63
66
  import { reldb } from '@h-ai/reldb'
64
67
  import { vecdb } from '@h-ai/vecdb'
65
68
 
66
- await reldb.init({ type: 'sqlite', database: './ai.db' })
67
- await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
68
-
69
- const init = await ai.init({
70
- llm: {
71
- model: 'gpt-4o-mini',
72
- apiKey: process.env.HAI_AI_LLM_APIKEY,
73
- },
74
- })
75
- if (!init.success) {
76
- // 按 init.error.code 处理配置/依赖错误
77
- }
78
-
79
- const result = await ai.llm.chat({
80
- messages: [{ role: 'user', content: '你好!' }],
81
- })
82
- if (result.success) {
69
+ // 开发/测试使用本地后端;生产改为共享 PostgreSQL/MySQL 与网络 vecdb。
70
+ const failures: unknown[] = []
71
+ try {
72
+ const database = await reldb.init({ type: 'sqlite', database: './ai.db' })
73
+ if (!database.success)
74
+ throw new Error(database.error.message)
75
+ const vectors = await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
76
+ if (!vectors.success)
77
+ throw new Error(vectors.error.message)
78
+ const initialized = await ai.init({
79
+ llm: { model: 'gpt-4o-mini', apiKey: process.env.HAI_AI_LLM_APIKEY },
80
+ })
81
+ if (!initialized.success)
82
+ throw new Error(initialized.error.message)
83
+ const result = await ai.llm.chat({ messages: [{ role: 'user', content: 'Hello!' }] })
84
+ if (!result.success)
85
+ throw new Error(result.error.message)
83
86
  const reply = result.data.choices[0]?.message.content ?? ''
84
87
  }
85
-
86
- await ai.close()
87
- await vecdb.close()
88
- await reldb.close()
88
+ catch (error) {
89
+ failures.push(error)
90
+ }
91
+ finally {
92
+ await ai.close()
93
+ for (const service of [vecdb, reldb]) {
94
+ const closed = await service.close()
95
+ if (!closed.success)
96
+ failures.push(new Error(closed.error.message))
97
+ }
98
+ }
99
+ if (failures.length)
100
+ throw new AggregateError(failures, 'AI operation or cleanup failed')
89
101
  ```
90
102
 
103
+ MySQL の初回初期化は複数ノードで実行できます。mysql2 の明示的な ER_DUP_KEYNAME/1061 だけを受け入れ、実際の列・順序、非一意・列全体・昇順・可視 BTREE・式なしを再確認します。同名でも構造が異なれば失敗します。テーブル作成と INDEX 権限が必要で、メタデータ取得・権限・その他 DDL の失敗は ai.init の CONFIGURATION_ERROR になります。開始した全 DDL の終了を待ってから返します。MySQL DDL はロールバックされないため、権限や構造を修正してから再試行してください。各初期化結果を確認し、失敗時は業務リクエストを受けず、AI・vecdb・reldb の逆順で終了します。
104
+
91
105
  #### 独自 StoreProvider
92
106
 
93
107
  ```ts
@@ -110,7 +124,9 @@ await ai.close()
110
124
  - ライフサイクルは `await ai.init(config, options?)` / `await ai.close()`。終了時は独自 `AIStoreProvider.close()` を待ちます。
111
125
  - ドメイン操作は `HaiResult<T>` または `Promise<HaiResult<T>>` を返し、`result.success === false` と `result.error.code` で失敗を表します。`AsyncIterable`、クライアント通信、外部コールバックは開始・反復時に例外を投げる場合があります。
112
126
  - `ai.tools` と `ai.stream` は純粋関数で、初期化せず使用できます。
113
- - reldb/vecdb がなければプロセス内の一時 Store、両方が初期化済みなら DB Provider を自動使用します。独自 Provider はほかの保存先を隠蔽できます。
127
+ - 開発・テストで reldb/vecdb がなければ一時 Store、両方が初期化済みなら DB Provider を使います。本番では実際の DB/vecdb 共有能力を検証し、独自 Provider も正確な `stateScope` を宣言する必要があります。
128
+
129
+ Store の拡張には公開 AIStoreProvider を使用します。組み込み LLM Provider、ファクトリ依存、Context 内部依存は非公開です。モデル接続は設定で行い、履歴照会、AskOptions、ChatRecord は公開を維持します。
114
130
 
115
131
  ### API 一覧
116
132
 
@@ -144,6 +160,8 @@ const temp = await ai.llm.chat({
144
160
  })
145
161
  ```
146
162
 
163
+ SSE は [WHATWG のイベントストリーム規則](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation)に従い、分割された LF/CRLF/CR、複数 data 行、コロン直後の空白一つ、id/retry の継承、既定の message を扱います。EOF の未完了イベントは配信しません。encodeSSE は data の改行を正規化し、不正なメタデータは明示的に例外を返します。Anthropic/MiMo は UTF-8 reader を共有し、終了・取消・失敗時に本文を取消して解放します。自動再接続や業務再実行は行わず、Anthropic の timeout は本文まで、MiMo は各 HTTP リクエスト単位で適用します。
164
+
147
165
  #### ツール呼び出し
148
166
 
149
167
  ```ts
@@ -180,7 +198,13 @@ const chat = await ai.llm.chat({ messages, tools: registry.getDefinitions() })
180
198
 
181
199
  `ai.a2a.getAgentCard()` は `protocolVersion`、MIME、streaming 機能を含む完全な A2A 0.3 Agent Card を返します。Kit の既定検出パスは `/.well-known/agent-card.json`。`handleRequest()` は常に JSON-RPC/SSE の結果を返し、未初期化時は JSON-RPC エラーです。信頼された HTTP の `agentId` を executor の `context.user.userName` に対応付けます。JSON-RPC/SSE に対応し、push notifications・gRPC・REST 対応は宣言しません。業務リソースの認可はアプリの責任です。
182
200
 
183
- A2A のタスク・メッセージテーブルは `ai.init()` で初期化します。取得・キャンセル・継続を含め、タスクは信頼された `agentId` で分離します。匿名は共通 scope なので、分離が必要な環境は認証を有効にしてください。`listMessages` はサーバー管理 API で `callerId`・`contextId`・`since` を使えますが、未認可クライアントへ公開しません。旧タスクの主体は信頼できず、更新時に所有者の推定や自動削除はしません。アプリ側の審査後に `object_id` を `user:<agentId>`(匿名は `anonymous`)へ移行します。主体・context を持たない旧メッセージは対応フィルターに一致しません。
201
+ A2A の journal・context 所有者・メッセージテーブルは `ai.init()` で初期化します。取得・キャンセル・継続は信頼された `agentId` で分離します。匿名は共通主体のため、本番は信頼された伝送層で認証してください。`listMessages` は `callerId`・`contextId`・`since` を持つ管理 API であり、未認可クライアントへ公開しません。
202
+
203
+ Task とイベントは同じ共有 journal を使用し、従来の独立 TaskStore は組み込みません。push notification 設定と authenticated extended card の RPC は SDK 標準の未対応・未設定エラーを返し、タスクを実行しません。executor 関連型は公開を維持し、内部 handler/Store の組み立て型は公開契約に含めません。
204
+
205
+ 入力メッセージの永続化後、共有実行世代と期限を再確認してから executor を開始します。agent メッセージは既存の有界イベントキューでログ保存を待ち、journal に公開します。保存失敗は failed/interrupted Task となり、成功を偽装せず、同じメッセージの再送で業務を再実行しません。ログと journal は別レコードなので、ログの存在だけで配信成功と判断せず、Task の `requiresEffectReconciliation` に従って副作用を確認します。照会失敗は SQL・接続情報を含まない `A2A_LIST_MESSAGES_FAILED`(984)です。
206
+
207
+ Task とイベントは同じ CAS で保存し、任意ノードで購読できます。同一 message ID/内容の再試行は同じ結果を返し、内容変更や古いラウンドは拒否します。別ノードからのキャンセルは実行 owner の executor.cancelTask を呼び、確認待ちの間は同じ Task の新規メッセージを拒否して古いコールバックの影響を防ぎます。input-required は再起動後も継続できます。租約切れ・実行期限・working のまま終了した実行器は failed/interrupted となり、`metadata.haiExecution.requiresEffectReconciliation=true` を付けます。古い owner の書き込みと未知の外部効果の自動再実行を禁止します。`a2a.execution` の既定値は `leaseMs: 30000`、`pollIntervalMs: 250`、`maxExecutionMs: 300000`。上限はメッセージ 1 MiB、journal 8 MiB、1 ラウンド 2048 イベント、Task 256 回执、reference tasks 32 件です。安定した業務冪等キーと外部効果の照合が必要です。[共有状態表(中国語)](../../.agents/instructions/distributed-state.md)に保証と検証範囲を記載しています。
184
208
 
185
209
  #### MCP Server
186
210
 
@@ -202,6 +226,16 @@ await server.connect(new StreamableHTTPServerTransport({ sessionIdGenerator: ()
202
226
 
203
227
  #### Memory / RAG / Knowledge
204
228
 
229
+ Knowledge は取り込みごとに一意の `revision` と chunk ID を生成します。全文・ベクトル・エンティティを準備し、永続ステージングマニフェストとベクトルを書いた後、共有 Store CAS で完全なスナップショットを一度に公開します。同じ文書の再構築は一つの writer に制限され、失敗時は完全な旧版を保持します。短い新版に旧末尾 chunk が混在しません。検索は未公開版を除外し、公開と競合すれば再試行します。競合が続く場合はエラーを返します。候補拡張や清掃待ちは検索負荷を増やすことがあります。
230
+
231
+ 成功結果の `revision` と `cleanupPending` を確認してください。後者が true の場合、新版は公開済みですが旧ベクトルの清掃が残っています。`repair({ collection, offset, limit })` で全ページを定期処理してください。setup は最初の 100 マニフェストを確認します。ステージングは保存先時計で 5 分後に失効し、旧 writer は公開できません。遅延した外部書き込みを繰り返し清掃するため、放棄版の墓碑は永久保持し、ログと共に削除しません。SQL とベクトルは共通トランザクションではありません。文書・集合の削除は先に可視性を取り消し、清掃失敗を返します。集合削除は空ベクトル集合と世代墓碑を補償用に残し、再 setup で旧世代を復元しません。
232
+
233
+ DB Store は集合・ベクトルの失敗を伝播します。LanceDB は永続主キーの原子 merge を使い、Qdrant は書き込み・削除の適用を待ち、正確な件数を取得します。分散ノードは同一の関係保存先とサーバー型ベクトル基盤を共有します。独自 KnowledgeStore も同じ公開・補償契約が必要です。
234
+
235
+ LanceDB の集合次元と metric は Arrow schema に永続化され、接続設定は新規集合にのみ影響します。再起動後は同じ永続ディレクトリを使い、embedding 次元を変更する場合は集合を再作成してください。別ノードのローカルパスは自動共有されないため、分散 Knowledge にはサーバー型ベクトルバックエンドを使用します。
236
+
237
+ Knowledge の `retrieve/ask` と既定 DB Store は `filter` をベクトル Provider に渡し、検証・検索エラーを上位に伝えます。Knowledge は空結果に偽装せず `KNOWLEDGE_RETRIEVE_FAILED`(ask は `RAG_FAILED`)を返します。LanceDB は最上位のリテラルキーについて string・有限 number・boolean・null の AND 完全一致のみを支援します。例:`{ tenant: 'acme', active: true }`。null は欠落キーに一致せず、オブジェクト・配列・undefined は非対応です。フィルターはベクトル topK の前に適用します。他バックエンドの null・複合値の意味は個別に確認してください。LanceDB metadata は入れ子 JSON も保存できるネイティブ JSON フィールドになり、旧文字列形式は書き込み・フィルター前に再作成が必要です。
238
+
205
239
  ```ts
206
240
  const enriched = await ai.memory.injectMemories(messages, { objectId: 'user-001', topK: 5 })
207
241
  if (!enriched.success) {
@@ -221,10 +255,19 @@ const rag = await ai.rag.query('核心架构是什么?', { sources: ['docs'],
221
255
 
222
256
  const setup = await ai.knowledge.setup()
223
257
  if (setup.success) {
224
- await ai.knowledge.ingest({ documentId: 'doc-001', content: markdownText, title: '产品手册' })
258
+ const ingested = await ai.knowledge.ingest({ documentId: 'doc-001', content: markdownText, title: '产品手册' })
259
+ if (!ingested.success)
260
+ return ingested
261
+ if (ingested.data.cleanupPending) {
262
+ const repaired = await ai.knowledge.repair({ offset: 0, limit: 100 })
263
+ if (!repaired.success)
264
+ return repaired
265
+ }
225
266
  }
226
267
  ```
227
268
 
269
+ Knowledge は datapipe の単位と上限に従い、word は語数、その他は Unicode code point として清洗後テキストの区切りを保持します。AI の既定は markdown/maxSize=1500/overlap=200 のままです。maxSize を小さく上書きする際は継承する overlap もそれ未満にしてください。不正な分割設定・正規表現は embedding と版の公開前に KNOWLEDGE_INGEST_FAILED を返し、文書全体への無制限フォールバックは行いません。
270
+
228
271
  #### 音声(Audio)
229
272
 
230
273
  先に `ai.init()` で音声モデルを登録し、既定の認識・合成モデルを対応付けます。認証情報はプラットフォームの環境変数へフォールバックします。LLM と音声が同じ認証情報を使うと確認できた場合にのみ、`inheritLlmApiKey` を明示的に有効化してください:
@@ -332,6 +375,8 @@ return playable.data
332
375
 
333
376
  ブラウザー・モバイルは `@h-ai/serv` の共通音声 WebSocket を使用します。`@h-ai/ai/client` は通信詳細を隠して Node と同じ `audio.*` API を提供します。クライアントは正常終了・キャンセル(`AUDIO_CANCELLED`)・異常切断(`AUDIO_CONNECTION_FAILED`)を区別し、キャンセルや `end` 前の切断で対応コードを throw します。`synthesize` は未完了の部分音声を成功として返しません。
334
377
 
378
+ Node の IndexTTS は同じ Undici バージョンの fetch・Agent・multipart 型を組み合わせます。モデル `timeout` は応答ヘッダーと本文の待機に適用し、利用者の取消は `AUDIO_CANCELLED`、期限超過は `AUDIO_TIMEOUT` です。成功・失敗・取消のいずれも専用接続を閉じ、区間単位ストリーミングのエラーは反復中に送出します。
379
+
335
380
  > 自前の CPU/GPU モデルサービス(faster-whisper / IndexTTS / Qwen3-4B)、重み取得、オフライン化は [models/(中国語)](./models/README.md) を参照してください。イメージ・重みは ModelScope を優先し、HuggingFace ミラーへ自動フォールバックします。
336
381
 
337
382
  #### 画像生成(Image)
@@ -364,8 +409,14 @@ if (result.success) {
364
409
 
365
410
  `referenceImages` は省略可能です。公開層は画像バイト列と MIME のみを受け付け、multipart・`inlineData`・Data URL は Provider が変換します。認証情報はモデル項目、`HAI_AI_IMAGE_<PROVIDER>_APIKEY`、サービス環境変数で指定できます。Qwen・Seedream の一時 URL は Provider 内ですぐダウンロードし、公開戻り値に出しません。詳細は [REFERENCE.md(中国語)](./REFERENCE.md#image) を参照してください。
366
411
 
412
+ ダウンロードは認証情報を含まない既定ポート 443 の公開 HTTPS に限定します。socket の DNS 解決で全アドレスを検証し、同じ結果で接続します。IPv4/IPv6 のプライベート・ループバック・リンクローカル・変換/マップ・予約アドレスを拒否します。リダイレクトは最大 3 回で各段階を再検証し、独立接続に厂商認証ヘッダーやアプリ Cookie を渡しません。`baseUrl` は運用者が管理する信頼された設定で、厂商 API 自体のリダイレクトは拒否します。
413
+
414
+ `image.maxImageBytes` は出力/参照画像ごとに既定 20971520(20 MiB、最大 100 MiB)、`image.maxImages` は出力/参照画像数ごとに既定 4(最大 16)です。URL は順次取得し、解凍後を含む実測バイト数が上限を超えると応答をキャンセルします。JSON は `max(64 KiB, 2 × maxImageBytes)`、エラー本文は 64 KiB までです。モデル `timeout` は既定 120000ms で API・リダイレクト・全ダウンロードを含み、`signal` で全体をキャンセルできます。出力は PNG/JPEG/GIF/WebP の MIME とマジック値の一致を要求し、SVG は拒否します。完全な画像デコード・寸法検証は行いません。アドレス拒否・上限超過・タイムアウトはそれぞれ `IMAGE_DOWNLOAD_BLOCKED`・`IMAGE_TOO_LARGE`・`IMAGE_TIMEOUT` を返すため、success を確認してください。
415
+
367
416
  #### Context 管理
368
417
 
418
+ 自動記憶抽出は現在のノードのバックグラウンドで動作し、`pendingMemoryTasks=0` だけでは成功を示しません。`flush()` は当該バッチを待ち、例外と失敗 HaiResult を件数付き `MEMORY_EXTRACT_FAILED` として返します。同時の観測者は同じ結果を受け取り、戻り時点で報告を確認済みとし、次回は新規タスクだけを観測します。部分実行の可能性がある処理を自動再実行しません。`save()`・`reset({ waitForMemoryTasks: true })`・`consolidate()` は失敗時に後続の書き込みへ進まず、プロセス停止後の外部効果は別途確認します。
419
+
369
420
  ```ts
370
421
  const manager = ai.context.createManager({
371
422
  scope: { objectId: 'user-001', sessionId: 'sess-001' },
@@ -375,7 +426,9 @@ const manager = ai.context.createManager({
375
426
  })
376
427
  if (manager.success) {
377
428
  const reply = await manager.data.chat('你好')
378
- await manager.data.save()
429
+ const saved = await manager.data.save()
430
+ if (!saved.success)
431
+ return saved
379
432
  // reset 生命周期完整:终止活动生成、清空消息/摘要/轮次,默认保留系统提示词
380
433
  await manager.data.reset() // 可传 { preserveSystemPrompt, cancelActiveTurn, waitForMemoryTasks }
381
434
  }
@@ -392,12 +445,15 @@ if (manager.success) {
392
445
  `chatStream` は**モデル呼び出し前**にターン登録と `turn_started` を行います(`turn_started → delta* → done`、キャンセル時は `turn_started → delta* → cancelled`)。途中で `AbortSignal` により中止しても `turnId` と生成済みテキストを取得し、実際の内容を保存できます:
393
446
 
394
447
  ```ts
395
- const m = ai.context.createManager({ turnCommit: 'manual' /* ... */ }).data
448
+ const created = ai.context.createManager({ scope: { objectId: 'user-001', sessionId: 'sess-001' }, turnCommit: 'manual' })
449
+ if (!created.success)
450
+ return created
451
+ const m = created.data
396
452
  const controller = new AbortController()
397
453
 
398
454
  for await (const ev of m.chatStream('请展开讲讲', { signal: controller.signal })) {
399
455
  if (ev.type === 'turn_started') {
400
- m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
456
+ await m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
401
457
  }
402
458
  else if (ev.type === 'delta') {
403
459
  feedTts(ev.text) // 边生成边合成播放
@@ -450,6 +506,7 @@ await ai.persona.save({
450
506
  // 组合出可直接喂给 ContextManager 的系统提示词(systemPrompt + traits)
451
507
  const composed = await ai.persona.compose('xiaoq')
452
508
  const manager = ai.context.createManager({
509
+ scope: { objectId: 'user-001', sessionId: 'persona-session' },
453
510
  systemPrompt: composed.data,
454
511
  memory: { enable: true, enableExtract: true, scope: { personaId: 'xiaoq' } },
455
512
  })
@@ -457,6 +514,8 @@ const manager = ai.context.createManager({
457
514
 
458
515
  `ai.persona` は `save` / `get` / `update` / `remove` / `list` / `compose` を提供します。プロフィールは全体で共有し、`objectId` では分離しません。
459
516
 
517
+ ### 設定
518
+
460
519
  ```yaml
461
520
  llm:
462
521
  apiKey: ''
@@ -512,9 +571,17 @@ memory:
512
571
  ```
513
572
 
514
573
  - **`native`(既定・推奨)**:HAI のネイティブエンジンは vecdb、reldb、LLM、Embedding を再利用します。`extract` は **Mem0 型の一括マージ**で、抽出した事実と既存記憶に 1 回の LLM 呼び出しで ADD / UPDATE / DELETE / NONE を決定し、増分更新・重複除去・矛盾削除・`category` 分類を実現します。`maxEntriesPerObject`、`maxEntriesGlobal`、`recencyDecay`、`embeddingEnabled`、`writebackRelatedTopK` が適用されます。削除は `objectId` ごとなので、ある主体の大量書き込みでほかの主体の記憶は失いません。scope の候補は `objectId` で絞り(≤ `maxEntriesPerObject`)、PostgreSQL はさらに JSONB 包含検索へ scope を渡して **GIN インデックス**を使います。SQLite/MySQL はメモリー照合で同じ結果を返します。
515
- - **`mem0`(mem0ai/oss)**:`mem0ai/oss` の `Memory` エンジンを直接組み込み、クラウドを使いません。LLM/Embedder は `llm` 設定から取得し、`qdrant` / `pgvector` は vecdb を再利用できます。`lancedb` / `chroma` など対応できないバックエンドは即座に失敗し、明示的な `memory.allowEphemeralFallback: true` の場合のみメモリーへ移行します。再起動後の無通知の記憶消失を防ぎ、履歴は既定で無効です。
574
+ - **`mem0`(mem0ai/oss)**:`mem0ai/oss` の `Memory` エンジンを直接組み込み、クラウドを使いません。LLM/Embedder は `llm` 設定から取得し、`qdrant` は vecdb を再利用できます。現在のアダプターは完全な接続設定を持つ Qdrant のみをマッピングします。本番(`NODE_ENV` または `HAI_CORE_ENV=production`)では `memory.allowEphemeralFallback: true` でも実際のメモリーへの移行を拒否し、バックエンド未指定も失敗します。一時利用は開発または明示的な `HAI_E2E=1` / `HAI_E2E_MOCK=1` に限ります。履歴は既定で無効です。
516
575
 
517
- 両 Provider は同じ `ai.memory.*`(`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`)を提供し、`objectId` の主体分離と `{ topicId, personaId }` などの `scope` をサポートします。`recall` / `list` / `listPage` / `clear` は scope を厳密に絞り、`clear` に `types` / `scope` を渡すと同時一致のみを削除します。mem0 の `extract` は共通抽出器で分類・採点(`types` / `model` / `minImportance` / `systemPrompt`)後、`infer:false` で `hai_type` / `hai_importance` を保存します。`recall` も `types` と `recencyWeight` 減衰に対応し、native と同じ動作です。ただし mem0 の type/importance/metadata 更新は記憶を再作成して `id` を変更し、native は維持します。
576
+ 両 Provider は同じ `ai.memory.*`(`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`)を提供し、`objectId` の主体分離と `{ topicId, personaId }` などの `scope` をサポートします。`recall` / `list` / `listPage` / `clear` は scope を厳密に絞り、`clear` に `types` / `scope` を渡すと同時一致のみを削除します。mem0 の `extract` は共通抽出器で分類・採点(`types` / `model` / `minImportance` / `systemPrompt`)後、`infer:false` で `hai_type` / `hai_importance` を保存します。`recall` も `types` と `recencyWeight` 減衰に対応し、native と同じ動作です。Mem0 はテキストとメタデータをその場で更新し、`id` と作成時刻を保持します。SDK の検索は `filters.user_id`、書き込みと主体全体の削除は `userId` を使います。SDK に一覧カーソルがないため、`list` / `listPage` / 条件付き `clear` は期限切れを含む主体候補 1000 件まで対応します。1 件余分に読み、超過は `MEMORY_LIST_INCOMPLETE`(906)で失敗します。不正確な total や一部のみの条件付き削除は行いません。候補が完全な場合、UTC の有効期限、型、scope で絞ってからページ分割します。主体全体の `deleteAll` は一覧上限の対象外です。公開 API の空条件 clear は拒否し、全主体の削除は信頼された管理者による明示的な `admin.clearAll({ confirm: true })` に限定します。
577
+
578
+ Mem0 3.3.1 は `QdrantClient.search` を呼び出すため、カタログと AI の optional peer は検証済みの `@qdrant/js-client-rest` `1.18.0` に固定します。起動時に公開メソッドを確認し、不整合は設定エラーにします。このメソッドを削除した 1.19 への上書きはできません。
579
+
580
+ アダプターは SDK の公開 `client` 設定から専用クライアントを渡します。Mem0 の Qdrant 書き込みと削除は `wait: true` を指定し、サーバーでの適用完了後に書き込み後の読み取りや次の削除バッチへ進みます。
581
+
582
+ Mem0 + pgvector は全環境で起動を拒否します。SDK が PostgreSQL 接続を私有オブジェクトに保持し、公開された close API がないためです。pgvector は `memory.provider: native` で利用でき、ネイティブの対応は維持します。`disableHistory: true` はリソースを持たない履歴実装を選び、履歴 SQLite ファイルを作成しません。ファクトリーは上限付きの公開読み取りでバックエンドの準備完了を待ってから成功を公開します。
583
+
584
+ SDK の追加応答には完全な業務フィールドがないため、`add` / `extract` は書き込み後に保存済みレコードを読み取ります。extract は全ての進行中の書き込み完了を待ちます。書き込みまたは再読み取り失敗時、`error.ext.requiresEffectReconciliation=true` と既知の ID の `memoryIds` を返します(応答消失時は空の場合もあります)。主体の実データを照合してから復旧し、add/extract を無条件に繰り返さないでください。自動再実行は行いません。
518
585
 
519
586
  **候補集合と scope の検索漏れ**:`topK` で先に切り捨てると、別の話題・人格の関連性が高い記憶が対象 scope を押し出します。そのため `recall` / `injectMemories` は `topK × candidateMultiplier`(既定 5)件を取得してから絞り、`topK` 件にします。`topicId` + `personaId` など細かく分離する場合は `candidateMultiplier` を増やせます:
520
587
 
@@ -560,8 +627,8 @@ if (!result.success) {
560
627
  - `hai:ai:600-701`:Retrieval/RAG。
561
628
  - `hai:ai:800-805`:Knowledge。
562
629
  - `hai:ai:050-059`:Audio。
563
- - `hai:ai:060-065`:Image。
564
- - `hai:ai:900-905`:Memory。
630
+ - `hai:ai:060-068`:Image(ダウンロード拒否、リソース上限、全体タイムアウトを含む)。
631
+ - `hai:ai:900-906`:Memory。
565
632
  - `hai:ai:980-984`:A2A。
566
633
 
567
634
  ## 開発とテスト
@@ -573,6 +640,8 @@ pnpm --filter @h-ai/ai test
573
640
  pnpm --filter @h-ai/ai build
574
641
  ```
575
642
 
643
+ 公開 JSDoc の例は実際のパッケージ入口から import し、内部ファクトリの例はモジュール内部コンテキストを明示して実宣言で型検証します。ルートで `pnpm check:jsdoc --package ai` を実行すると網羅性と例の型を検証できます。通常の業務 API は HaiResult を確認し、ストリームとブラウザー AudioClient は明示された例外契約に従います。
644
+
576
645
  ## ライセンス
577
646
 
578
647
  Apache-2.0