@harrylabsj/kiwi 0.7.21 → 0.8.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 (146) hide show
  1. package/README.md +45 -10
  2. package/dist/a2a/node.js +1 -1
  3. package/dist/a2a/node.js.map +1 -1
  4. package/dist/agent/buyer/buyer-tools.d.ts +7 -0
  5. package/dist/agent/buyer/buyer-tools.js +53 -0
  6. package/dist/agent/buyer/buyer-tools.js.map +1 -1
  7. package/dist/agent/buyer/scheduler.d.ts +4 -0
  8. package/dist/agent/buyer/scheduler.js +5 -1
  9. package/dist/agent/buyer/scheduler.js.map +1 -1
  10. package/dist/agent/buyer/task-store.d.ts +5 -0
  11. package/dist/agent/buyer/task-store.js +13 -0
  12. package/dist/agent/buyer/task-store.js.map +1 -1
  13. package/dist/agent/chat-tools.js +10 -3
  14. package/dist/agent/chat-tools.js.map +1 -1
  15. package/dist/agent/context/fencing.d.ts +27 -0
  16. package/dist/agent/context/fencing.js +126 -0
  17. package/dist/agent/context/fencing.js.map +1 -0
  18. package/dist/agent/context/grounding.d.ts +18 -0
  19. package/dist/agent/context/grounding.js +22 -0
  20. package/dist/agent/context/grounding.js.map +1 -0
  21. package/dist/agent/host/events.d.ts +31 -0
  22. package/dist/agent/host/events.js +62 -0
  23. package/dist/agent/host/events.js.map +1 -0
  24. package/dist/agent/host/tui-renderer.d.ts +17 -0
  25. package/dist/agent/host/tui-renderer.js +125 -0
  26. package/dist/agent/host/tui-renderer.js.map +1 -0
  27. package/dist/agent/host/web-renderer.d.ts +71 -0
  28. package/dist/agent/host/web-renderer.js +185 -0
  29. package/dist/agent/host/web-renderer.js.map +1 -0
  30. package/dist/agent/kernel-builder.d.ts +3 -1
  31. package/dist/agent/kernel-builder.js +4 -1
  32. package/dist/agent/kernel-builder.js.map +1 -1
  33. package/dist/agent/kernel.d.ts +22 -1
  34. package/dist/agent/kernel.js +294 -22
  35. package/dist/agent/kernel.js.map +1 -1
  36. package/dist/agent/memory/schema.d.ts +1 -1
  37. package/dist/agent/memory/schema.js +67 -1
  38. package/dist/agent/memory/schema.js.map +1 -1
  39. package/dist/agent/merchant/intelligence/backend.d.ts +40 -0
  40. package/dist/agent/merchant/intelligence/backend.js +2 -0
  41. package/dist/agent/merchant/intelligence/backend.js.map +1 -0
  42. package/dist/agent/merchant/intelligence/default-backend.d.ts +38 -0
  43. package/dist/agent/merchant/intelligence/default-backend.js +454 -0
  44. package/dist/agent/merchant/intelligence/default-backend.js.map +1 -0
  45. package/dist/agent/merchant/intelligence/http-analytics-source.d.ts +55 -0
  46. package/dist/agent/merchant/intelligence/http-analytics-source.js +193 -0
  47. package/dist/agent/merchant/intelligence/http-analytics-source.js.map +1 -0
  48. package/dist/agent/merchant/intelligence/types.d.ts +109 -0
  49. package/dist/agent/merchant/intelligence/types.js +2 -0
  50. package/dist/agent/merchant/intelligence/types.js.map +1 -0
  51. package/dist/agent/merchant/merchant-enrichment.d.ts +10 -0
  52. package/dist/agent/merchant/merchant-enrichment.js +38 -0
  53. package/dist/agent/merchant/merchant-enrichment.js.map +1 -0
  54. package/dist/agent/merchant/merchant-grounding.d.ts +5 -0
  55. package/dist/agent/merchant/merchant-grounding.js +76 -0
  56. package/dist/agent/merchant/merchant-grounding.js.map +1 -0
  57. package/dist/agent/merchant/merchant-presentations.d.ts +8 -0
  58. package/dist/agent/merchant/merchant-presentations.js +184 -0
  59. package/dist/agent/merchant/merchant-presentations.js.map +1 -0
  60. package/dist/agent/merchant/merchant-tools.d.ts +8 -0
  61. package/dist/agent/merchant/merchant-tools.js +208 -9
  62. package/dist/agent/merchant/merchant-tools.js.map +1 -1
  63. package/dist/agent/presentation/registry.d.ts +14 -0
  64. package/dist/agent/presentation/registry.js +24 -0
  65. package/dist/agent/presentation/registry.js.map +1 -0
  66. package/dist/agent/presentation/runner.d.ts +11 -0
  67. package/dist/agent/presentation/runner.js +17 -0
  68. package/dist/agent/presentation/runner.js.map +1 -0
  69. package/dist/agent/presentation/sanitize.d.ts +16 -0
  70. package/dist/agent/presentation/sanitize.js +37 -0
  71. package/dist/agent/presentation/sanitize.js.map +1 -0
  72. package/dist/agent/presentation/types.d.ts +28 -0
  73. package/dist/agent/presentation/types.js +8 -0
  74. package/dist/agent/presentation/types.js.map +1 -0
  75. package/dist/agent/session.d.ts +1 -1
  76. package/dist/agent/session.js +2 -2
  77. package/dist/agent/session.js.map +1 -1
  78. package/dist/agent/skills/registry.d.ts +11 -0
  79. package/dist/agent/skills/registry.js +119 -0
  80. package/dist/agent/skills/registry.js.map +1 -0
  81. package/dist/agent/skills/tools.d.ts +6 -0
  82. package/dist/agent/skills/tools.js +36 -0
  83. package/dist/agent/skills/tools.js.map +1 -0
  84. package/dist/agent/skills/types.d.ts +9 -0
  85. package/dist/agent/skills/types.js +2 -0
  86. package/dist/agent/skills/types.js.map +1 -0
  87. package/dist/agent/supplier/scheduler.d.ts +86 -0
  88. package/dist/agent/supplier/scheduler.js +618 -0
  89. package/dist/agent/supplier/scheduler.js.map +1 -0
  90. package/dist/agent/supplier/store.d.ts +173 -0
  91. package/dist/agent/supplier/store.js +387 -0
  92. package/dist/agent/supplier/store.js.map +1 -0
  93. package/dist/agent/system-prompt.d.ts +5 -1
  94. package/dist/agent/system-prompt.js +20 -3
  95. package/dist/agent/system-prompt.js.map +1 -1
  96. package/dist/cli.d.ts +5 -0
  97. package/dist/cli.js +177 -12
  98. package/dist/cli.js.map +1 -1
  99. package/dist/config/profile.d.ts +21 -0
  100. package/dist/config/profile.js +59 -0
  101. package/dist/config/profile.js.map +1 -1
  102. package/dist/discovery/catalog-source/kiwi-record.d.ts +2 -0
  103. package/dist/discovery/catalog-source/kiwi-record.js.map +1 -1
  104. package/dist/discovery/catalog-source/kiwi-source.js +2 -0
  105. package/dist/discovery/catalog-source/kiwi-source.js.map +1 -1
  106. package/dist/handoff/authorization.js +1 -1
  107. package/dist/http/merchant-server.d.ts +65 -0
  108. package/dist/http/merchant-server.js +346 -0
  109. package/dist/http/merchant-server.js.map +1 -0
  110. package/dist/merchant/stats-store.d.ts +7 -0
  111. package/dist/merchant/stats-store.js +11 -0
  112. package/dist/merchant/stats-store.js.map +1 -1
  113. package/dist/negotiation/action-candidate.js +1 -1
  114. package/dist/operator/types.d.ts +1 -1
  115. package/dist/product-cli.d.ts +2 -2
  116. package/dist/product-cli.js +14 -2
  117. package/dist/product-cli.js.map +1 -1
  118. package/dist/product-supplier.d.ts +148 -0
  119. package/dist/product-supplier.js +452 -0
  120. package/dist/product-supplier.js.map +1 -0
  121. package/dist/runtime/fake-model.d.ts +1 -1
  122. package/dist/trust/identity/jwk.d.ts +11 -0
  123. package/dist/trust/identity/jwk.js +2 -0
  124. package/dist/trust/identity/jwk.js.map +1 -0
  125. package/dist/trust/identity/jws.d.ts +2 -1
  126. package/dist/trust/identity/jws.js.map +1 -1
  127. package/dist/trust/identity/keys.d.ts +2 -1
  128. package/dist/trust/identity/keys.js.map +1 -1
  129. package/dist/trust/identity/message-signature.d.ts +1 -14
  130. package/dist/trust/identity/message-signature.js.map +1 -1
  131. package/dist/weixin/channel.d.ts +3 -0
  132. package/dist/weixin/channel.js +18 -2
  133. package/dist/weixin/channel.js.map +1 -1
  134. package/examples/profiles/merchant.deepseek.yaml +6 -0
  135. package/examples/profiles/merchant.fake.yaml +6 -0
  136. package/examples/profiles/merchant.local.yaml +6 -0
  137. package/package.json +7 -6
  138. package/skills/kiwi-buyer/SKILL.md +66 -0
  139. package/skills/merchant/catalog-operations/SKILL.md +17 -0
  140. package/skills/merchant/change-approval/SKILL.md +16 -0
  141. package/skills/merchant/human-review/SKILL.md +17 -0
  142. package/skills/merchant/inventory-operations/SKILL.md +17 -0
  143. package/skills/merchant/negotiation-review/SKILL.md +18 -0
  144. package/skills/merchant/performance-insights/SKILL.md +19 -0
  145. package/contracts/candidate-agent-dto-1.0.README.md +0 -43
  146. package/contracts/conformance/python-service/README.md +0 -19
@@ -29,15 +29,27 @@
29
29
  import { AgentHarness } from "@earendil-works/pi-agent-core";
30
30
  import { existsSync, readFileSync, rmSync } from "node:fs";
31
31
  import path from "node:path";
32
+ import { fileURLToPath } from "node:url";
32
33
  import { ensureAgentPaths, openAgentDatabase } from "./agent-db.js";
33
34
  import { HandoffEventStore, HandoffIdempotencyStore, foldCandidateLifecycle, deliveryState, recordLaunch, recordOpenEvidence, } from "../handoff/index.js";
34
35
  import { buildBuyerTools } from "./buyer/buyer-tools.js";
35
36
  import { TaskScheduler } from "./buyer/scheduler.js";
36
37
  import { BuyerTaskStore } from "./buyer/task-store.js";
38
+ import { SupplierScheduler } from "./supplier/scheduler.js";
39
+ import { SupplierRelationshipStore } from "./supplier/store.js";
40
+ import { TrustRecordStore } from "../trust/records/index.js";
37
41
  import { buildMemoryTools } from "./chat-tools.js";
38
42
  import { AGENT_MODES, DEFAULT_AGENT_MODE, isAgentMode } from "./mode.js";
39
43
  import { WriteApprovalCandidateStore, executeApprovedCandidate, } from "./merchant/action-candidate.js";
40
44
  import { buildMerchantTools } from "./merchant/merchant-tools.js";
45
+ import { publicCandidatePreview } from "./merchant/merchant-enrichment.js";
46
+ import { DefaultMerchantIntelligenceBackend } from "./merchant/intelligence/default-backend.js";
47
+ import { emitHostEvent, sanitizeHostEventData, SerializedEventSink } from "./host/events.js";
48
+ import { fenceModelPayload } from "./context/fencing.js";
49
+ import { groundingReads } from "./context/grounding.js";
50
+ import { MERCHANT_GROUNDING_RULES, merchantGroundingContext } from "./merchant/merchant-grounding.js";
51
+ import { SkillRegistry } from "./skills/registry.js";
52
+ import { buildSkillTools } from "./skills/tools.js";
41
53
  import { DeterministicNegotiationRunner } from "../operator/runner.js";
42
54
  import { MemoryStore } from "./memory/store.js";
43
55
  import { MemoryError } from "./memory/types.js";
@@ -45,7 +57,7 @@ import { PrivateVault } from "./memory/vault.js";
45
57
  import { openMainSession } from "./session.js";
46
58
  import { registerCatalogAgent } from "../discovery/catalog-source/register.js";
47
59
  import { KiwiCatalogSource } from "../discovery/catalog-source/kiwi-source.js";
48
- import { baseSystemPrompt, renderMemoryBriefing } from "./system-prompt.js";
60
+ import { baseSystemPrompt, combineDynamicBriefing, renderMemoryBriefing } from "./system-prompt.js";
49
61
  export const MAIN_SESSION_ID = "main";
50
62
  /** 审查 P2-I:autopilot 对网关权威门持续拒绝的消息——连续失败上限与冷却窗口。 */
51
63
  export const AUTOPILOT_MAX_SUBMIT_FAILURES = 3;
@@ -94,6 +106,61 @@ function assistantText(message) {
94
106
  .join("\n")
95
107
  .trim();
96
108
  }
109
+ const PRIVATE_TOOL_RESULT_NAMES = new Set(["view_private_thresholds"]);
110
+ export function toolResultSummary(toolName, result) {
111
+ if (PRIVATE_TOOL_RESULT_NAMES.has(toolName))
112
+ return "工具调用已完成。";
113
+ if (result !== null && typeof result === "object") {
114
+ const content = result.content;
115
+ if (Array.isArray(content)) {
116
+ const text = content
117
+ .filter((item) => item !== null && typeof item === "object" &&
118
+ item.type === "text" &&
119
+ typeof item.text === "string")
120
+ .map((item) => item.text)
121
+ .join("\n")
122
+ .trim();
123
+ if (text !== "")
124
+ return text.slice(0, 1_000);
125
+ }
126
+ }
127
+ return "工具调用已完成。";
128
+ }
129
+ /** Bridge the stable AgentHarness stream/tool hooks into the optional host protocol. */
130
+ function attachHarnessHostEvents(harness, emitEvent) {
131
+ return harness.subscribe(async (event) => {
132
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
133
+ if (event.assistantMessageEvent.delta !== "") {
134
+ await emitEvent("text_delta", { text: event.assistantMessageEvent.delta });
135
+ }
136
+ return;
137
+ }
138
+ if (event.type === "tool_execution_start") {
139
+ await emitEvent("tool_call", {
140
+ tool: event.toolName,
141
+ call_id: event.toolCallId,
142
+ input: event.args,
143
+ });
144
+ return;
145
+ }
146
+ if (event.type === "tool_execution_update") {
147
+ await emitEvent("ui_partial", {
148
+ tool: event.toolName,
149
+ call_id: event.toolCallId,
150
+ partial: event.partialResult,
151
+ });
152
+ return;
153
+ }
154
+ if (event.type === "tool_execution_end") {
155
+ await emitEvent("tool_result", {
156
+ tool: event.toolName,
157
+ call_id: event.toolCallId,
158
+ status: event.isError ? "error" : "ok",
159
+ summary: toolResultSummary(event.toolName, event.result),
160
+ });
161
+ }
162
+ });
163
+ }
97
164
  /** The owner's own Restricted memory values (Vault). Owner-only by isolation. */
98
165
  function listRestrictedValues(store) {
99
166
  const out = [];
@@ -201,12 +268,19 @@ export class AgentKernel {
201
268
  closed = false;
202
269
  taskStore;
203
270
  scheduler;
271
+ /** M1:Buyer-owned supplier pull scheduler(buyer 角色构造;与 TaskScheduler 同一 tick 链)。 */
272
+ supplierScheduler;
204
273
  approvals;
205
274
  /** v0.7.0 KTH:handoff 存储(open() 构造,buyer 角色注入)。 */
206
275
  handoffRuntime;
207
276
  commerceClient;
208
277
  merchantClient;
209
278
  broker;
279
+ merchantIntelligence;
280
+ /** Optional host event projection; it never owns business state. */
281
+ emitEvent;
282
+ /** Removes the optional AgentHarness stream bridge during kernel shutdown. */
283
+ unsubscribeHarnessEvents;
210
284
  /** Shared mutable mode ref (tools read it via a getter before the kernel exists). */
211
285
  modeRef = { value: DEFAULT_AGENT_MODE };
212
286
  /** Live execution hooks for pending candidates (v0.3.0-C /approve). */
@@ -249,6 +323,8 @@ export class AgentKernel {
249
323
  this.taskStore = options.taskStore;
250
324
  if (options.scheduler !== undefined)
251
325
  this.scheduler = options.scheduler;
326
+ if (options.supplierScheduler !== undefined)
327
+ this.supplierScheduler = options.supplierScheduler;
252
328
  if (options.approvals !== undefined)
253
329
  this.approvals = options.approvals;
254
330
  if (options.handoffRuntime !== undefined)
@@ -259,6 +335,10 @@ export class AgentKernel {
259
335
  this.merchantClient = options.merchantClient;
260
336
  if (options.broker !== undefined)
261
337
  this.broker = options.broker;
338
+ if (options.merchantIntelligence !== undefined)
339
+ this.merchantIntelligence = options.merchantIntelligence;
340
+ this.emitEvent = options.emitEvent;
341
+ this.unsubscribeHarnessEvents = options.unsubscribeHarnessEvents;
262
342
  if (options.modeRef !== undefined)
263
343
  this.modeRef = options.modeRef;
264
344
  this.pendingHooks = options.pendingHooks ?? new Map();
@@ -287,7 +367,7 @@ export class AgentKernel {
287
367
  store.bindPrincipal(principal.principal_id);
288
368
  let session;
289
369
  try {
290
- session = await openMainSession(paths);
370
+ session = await openMainSession(paths, options.eventSessionId ?? MAIN_SESSION_ID);
291
371
  }
292
372
  catch (err) {
293
373
  db.close(); // never leak the SQLite handle on a fail-closed open
@@ -300,22 +380,55 @@ export class AgentKernel {
300
380
  const modeRef = { value: options.mode ?? DEFAULT_AGENT_MODE };
301
381
  const pendingHooks = new Map();
302
382
  const turnId = { current: MAIN_SESSION_ID };
303
- const registerPending = (id, hooks) => {
383
+ const registerPendingHooks = (id, hooks) => {
304
384
  pendingHooks.set(id, hooks);
305
385
  };
386
+ const hostSequence = { value: 0 };
387
+ const hostSink = options.eventSink === undefined ? undefined : new SerializedEventSink(options.eventSink);
388
+ const emitEvent = options.eventSink === undefined
389
+ ? undefined
390
+ : async (type, data) => {
391
+ hostSequence.value += 1;
392
+ await emitHostEvent(hostSink, {
393
+ eventId: `host-${hostSequence.value}`,
394
+ sessionId: options.eventSessionId ?? MAIN_SESSION_ID,
395
+ sequence: hostSequence.value,
396
+ type,
397
+ occurredAt: clock(),
398
+ data: sanitizeHostEventData(type, data),
399
+ });
400
+ };
306
401
  const approvals = new WriteApprovalCandidateStore({
307
402
  db,
308
403
  principalId: principal.principal_id,
309
404
  now: clock,
310
405
  });
406
+ const registerPending = (id, hooks) => {
407
+ registerPendingHooks(id, hooks);
408
+ const candidate = approvals.get(id);
409
+ void emitEvent?.("candidate_update", {
410
+ candidate_id: id,
411
+ status: candidate?.status ?? "pending_approval",
412
+ preview: publicCandidatePreview(candidate),
413
+ });
414
+ };
311
415
  // Execution hooks are deliberately process-local. Invalidate durable
312
416
  // candidates before wiring tools so a restarted kernel never exposes a
313
417
  // /pending item that its /approve path cannot execute.
314
418
  approvals.expireForRecovery();
315
419
  let taskStore;
316
420
  let scheduler;
421
+ let supplierScheduler;
317
422
  let buyerTools = [];
318
423
  let merchantTools = [];
424
+ let merchantSkillTools = [];
425
+ let merchantIntelligence;
426
+ const merchantSkillRegistry = options.profile.role === "merchant" &&
427
+ options.profile.merchant_experience?.enabled === true &&
428
+ options.profile.merchant_experience.skills === true
429
+ ? SkillRegistry.fromDir(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../skills/merchant"), "merchant")
430
+ : new SkillRegistry([]);
431
+ merchantSkillTools = buildSkillTools(merchantSkillRegistry);
319
432
  let handoffRuntime;
320
433
  if (options.profile.role === "buyer" && options.connector !== undefined) {
321
434
  taskStore = new BuyerTaskStore({
@@ -333,6 +446,26 @@ export class AgentKernel {
333
446
  ? { catalogSource: new KiwiCatalogSource({ baseUrl: options.catalog }) }
334
447
  : {}),
335
448
  });
449
+ // M1:Buyer-owned supplier pull scheduler(pull-relationship §7.1/§8)。
450
+ // 与 TaskScheduler 同一 DB、同一 tick 串行链;catalogSource 不可得时仍可
451
+ // 做 Agent Card / UCP Profile 直拉(容错:可选依赖缺省不阻塞构造)。
452
+ // trustStore 落 agent data dir(trust/ 子目录,首次观察时创建)。
453
+ // 同一 store 也注入 buyer 工具:RFQ 成功后的 supplier_save_suggested
454
+ // 本地建议要查已有关系(§13 M0)。
455
+ const supplierStore = new SupplierRelationshipStore({
456
+ db,
457
+ principalId: principal.principal_id,
458
+ now: clock,
459
+ });
460
+ supplierScheduler = new SupplierScheduler({
461
+ store: supplierStore,
462
+ now: clock,
463
+ ...(options.catalog !== undefined
464
+ ? { catalogSource: new KiwiCatalogSource({ baseUrl: options.catalog }) }
465
+ : {}),
466
+ trustStore: new TrustRecordStore({ dir: path.dirname(paths.db), now: clock }),
467
+ ...(options.allowLoopback === true ? { allowLoopback: true } : {}),
468
+ });
336
469
  // v0.7.0 KTH:handoff 存储(Ledger 事件 + 执行幂等)落在 agent data dir,
337
470
  // 注入 buyer 工具(handoff_agreement 工具挂载)。
338
471
  const handoffDir = path.dirname(paths.db);
@@ -360,11 +493,26 @@ export class AgentKernel {
360
493
  now: clock,
361
494
  recordNegotiation: (input) => rememberNegotiation(store, input),
362
495
  handoff: { ledger: handoffLedger, idempotency: handoffIdempotency },
496
+ supplierStore,
363
497
  });
364
498
  }
365
499
  else if (options.profile.role === "merchant" &&
366
500
  options.merchantClient !== undefined &&
367
501
  options.broker !== undefined) {
502
+ if (options.profile.merchant_experience?.enabled === true &&
503
+ options.profile.merchant_experience.intelligence !== false) {
504
+ merchantIntelligence = new DefaultMerchantIntelligenceBackend({
505
+ merchant_id: principal.owner_id,
506
+ data_dir: path.dirname(paths.db),
507
+ principal_id: principal.principal_id,
508
+ merchant_client: options.merchantClient,
509
+ approvals,
510
+ ...(options.merchantAnalyticsSource !== undefined
511
+ ? { analytics_source: options.merchantAnalyticsSource }
512
+ : {}),
513
+ now: clock,
514
+ });
515
+ }
368
516
  merchantTools = buildMerchantTools({
369
517
  profile: options.profile,
370
518
  merchantClient: options.merchantClient,
@@ -372,17 +520,20 @@ export class AgentKernel {
372
520
  // fail closed,目录/库存只读与审批式写工具照常可用(见 §15.3/§15.4)。
373
521
  ...(options.commerceClient !== undefined ? { commerceClient: options.commerceClient } : {}),
374
522
  broker: options.broker,
523
+ principalId: principal.principal_id,
375
524
  approvals,
376
525
  mode: () => modeRef.value,
377
526
  registerPending,
378
527
  now: clock,
379
528
  privateValues: () => listRestrictedValues(store),
529
+ ...(merchantIntelligence !== undefined ? { intelligence: merchantIntelligence } : {}),
530
+ ...(emitEvent !== undefined ? { emitEvent } : {}),
380
531
  // 商家 A2A 节点 ledger 基础目录(<dataDir>/a2a)——LedgerStore 会再拼
381
532
  // /ledger 到 <dataDir>/a2a/ledger,list_a2a_negotiations 用。
382
533
  a2aLedgerDir: path.join(path.dirname(paths.db), "a2a"),
383
534
  });
384
535
  }
385
- const base = baseSystemPrompt(options.profile, principal);
536
+ const base = baseSystemPrompt(options.profile, principal, merchantSkillRegistry.names.length > 0 ? merchantSkillRegistry : undefined);
386
537
  let briefing;
387
538
  const harness = new AgentHarness({
388
539
  session,
@@ -392,12 +543,19 @@ export class AgentKernel {
392
543
  ...buildMemoryTools(store, { turnId: () => turnId.current }),
393
544
  ...buyerTools,
394
545
  ...merchantTools,
546
+ ...merchantSkillTools,
395
547
  ],
396
548
  systemPrompt: async () => (briefing === undefined ? base : `${base}\n\n${briefing}`),
397
549
  // §18.1: a hung model/provider request must NOT wedge the chat forever —
398
550
  // abort after the profile's turn timeout and surface an error text.
399
551
  streamOptions: {
400
552
  timeoutMs: options.profile.runtime.turn_timeout_seconds * 1000,
553
+ ...(options.profile.merchant_experience?.prompt_cache_retention !== undefined
554
+ ? {
555
+ cacheRetention: options.profile.merchant_experience.prompt_cache_retention,
556
+ sessionId: options.eventSessionId ?? MAIN_SESSION_ID,
557
+ }
558
+ : {}),
401
559
  },
402
560
  ...(options.thinkingLevel !== undefined ? { thinkingLevel: options.thinkingLevel } : {}),
403
561
  });
@@ -407,6 +565,9 @@ export class AgentKernel {
407
565
  if (options.model !== undefined) {
408
566
  await harness.setModel(options.model);
409
567
  }
568
+ const unsubscribeHarnessEvents = emitEvent === undefined
569
+ ? undefined
570
+ : attachHarnessHostEvents(harness, emitEvent);
410
571
  const kernel = new AgentKernel({
411
572
  profile: options.profile,
412
573
  paths,
@@ -423,9 +584,13 @@ export class AgentKernel {
423
584
  now: clock,
424
585
  ...(taskStore !== undefined ? { taskStore } : {}),
425
586
  ...(scheduler !== undefined ? { scheduler } : {}),
587
+ ...(supplierScheduler !== undefined ? { supplierScheduler } : {}),
426
588
  ...(options.commerceClient !== undefined ? { commerceClient: options.commerceClient } : {}),
427
589
  ...(options.merchantClient !== undefined ? { merchantClient: options.merchantClient } : {}),
428
590
  ...(options.broker !== undefined ? { broker: options.broker } : {}),
591
+ ...(merchantIntelligence !== undefined ? { merchantIntelligence } : {}),
592
+ emitEvent,
593
+ ...(unsubscribeHarnessEvents !== undefined ? { unsubscribeHarnessEvents } : {}),
429
594
  });
430
595
  kernel.briefingSetter = (value) => {
431
596
  briefing = value;
@@ -576,7 +741,7 @@ export class AgentKernel {
576
741
  /**
577
742
  * Change the runtime mode. Switching INTO autopilot requires an explicit
578
743
  * `confirm` — the same fail-closed rule as the operator control plane
579
- * (docs/operator-tui-v0.2.md §8).
744
+ * (operator control-plane design §8).
580
745
  */
581
746
  setMode(mode, options) {
582
747
  if (!isAgentMode(mode)) {
@@ -629,17 +794,26 @@ export class AgentKernel {
629
794
  if (candidate === undefined) {
630
795
  throw new MemoryError("validation", `未知审批候选 ${candidateId}`);
631
796
  }
797
+ const report = async (result) => {
798
+ const current = this.approvals?.get(candidateId);
799
+ await this.emitEvent?.("candidate_update", {
800
+ candidate_id: candidateId,
801
+ status: current?.status ?? result.candidate.status,
802
+ preview: publicCandidatePreview(current ?? result.candidate),
803
+ });
804
+ return result;
805
+ };
632
806
  // manual 模式语义(评审项 P3-3):manual = advice only(never executes)。
633
807
  // routeWriteCandidate 在 manual 分支仍注册执行钩子(供 /pending 显示与
634
808
  // /revise 重算),但批准路径必须拒绝——此前 /approve 绕过模式直接执行,
635
809
  // 与 operator 平面分叉(controller 对 advice_only 候选明确拒绝批准:
636
810
  // "manual 模式只提供建议,不自动提交")。
637
811
  if (this.getMode() === "manual") {
638
- return {
812
+ return report({
639
813
  kind: "not_approvable",
640
814
  candidate,
641
815
  reason: "manual 模式只提供建议,不自动执行(/approve 拒绝 advice-only 候选)",
642
- };
816
+ });
643
817
  }
644
818
  const hooks = this.pendingHooks.get(candidateId);
645
819
  if (hooks === undefined) {
@@ -647,19 +821,19 @@ export class AgentKernel {
647
821
  // candidate cannot be re-validated against the current marketplace.
648
822
  if (candidate.status === "expired") {
649
823
  this.releasePending(candidateId);
650
- return { kind: "expired", candidate };
824
+ return report({ kind: "expired", candidate });
651
825
  }
652
826
  if (candidate.status !== "pending_approval" && candidate.status !== "approved") {
653
827
  this.releasePending(candidateId);
654
- return {
828
+ return report({
655
829
  kind: "not_approvable",
656
830
  candidate,
657
831
  reason: `候选 ${candidateId} 状态为 ${candidate.status},不可批准。`,
658
- };
832
+ });
659
833
  }
660
834
  const expired = this.approvals.expireCandidate(candidateId);
661
835
  this.releasePending(candidateId);
662
- return { kind: "expired", candidate: expired };
836
+ return report({ kind: "expired", candidate: expired });
663
837
  }
664
838
  this.approvals.markApproved(candidateId);
665
839
  const outcome = await executeApprovedCandidate(this.approvals, candidateId, hooks);
@@ -668,7 +842,7 @@ export class AgentKernel {
668
842
  if (outcome.kind !== "not_approvable") {
669
843
  this.releasePending(candidateId);
670
844
  }
671
- return outcome;
845
+ return report(outcome);
672
846
  }
673
847
  /** Reject a pending/advice-only WriteApprovalCandidate. Never executes. */
674
848
  rejectCandidate(candidateId) {
@@ -682,6 +856,11 @@ export class AgentKernel {
682
856
  }
683
857
  this.approvals.reject(candidateId);
684
858
  this.releasePending(candidateId);
859
+ void this.emitEvent?.("candidate_update", {
860
+ candidate_id: candidateId,
861
+ status: this.approvals.get(candidateId)?.status ?? "rejected",
862
+ preview: publicCandidatePreview(this.approvals.get(candidateId)),
863
+ });
685
864
  return { ok: true };
686
865
  }
687
866
  /**
@@ -697,16 +876,24 @@ export class AgentKernel {
697
876
  // 审查 P3:交付域同样清扫——DELIVERED/LAUNCHED 打开时限过期落
698
877
  // handoff_expired(此前该事件全仓无生产者,"过期未打开"不可观测)。
699
878
  this.handoffRuntime?.ledger.sweepExpiredHandoffs(this.clock());
700
- if (this.scheduler === undefined) {
701
- return {
702
- checked_rules: 0,
703
- notifications: [],
704
- tasks_searched: [],
705
- tasks_expired: [],
706
- errors: [],
707
- };
879
+ const empty = {
880
+ checked_rules: 0,
881
+ requests_used: 0,
882
+ notifications: [],
883
+ tasks_searched: [],
884
+ tasks_expired: [],
885
+ errors: [],
886
+ };
887
+ const result = this.scheduler === undefined ? empty : await this.scheduler.tick(budget);
888
+ // M1:供应商关系 pull tick 走同一 kernel 串行链,共享请求预算;
889
+ // supplier tick 失败不影响 task tick 结果(错误进 supplier.errors)。
890
+ if (this.supplierScheduler !== undefined) {
891
+ const totalBudget = budget.max_requests ?? 20;
892
+ result.supplier = await this.supplierScheduler.tick({
893
+ max_requests: Math.max(0, totalBudget - result.requests_used),
894
+ });
708
895
  }
709
- return this.scheduler.tick(budget);
896
+ return result;
710
897
  });
711
898
  }
712
899
  /** 惰性过期清扫入口(/handoff 渲染前调用;幂等,可重复调)。 */
@@ -886,6 +1073,71 @@ export class AgentKernel {
886
1073
  timestamp: Date.now(),
887
1074
  });
888
1075
  }
1076
+ async runMerchantGroundingRead(read) {
1077
+ await this.emitEvent?.("grounding_started", {
1078
+ rule: read.tool,
1079
+ tool: read.tool,
1080
+ reason: read.reason,
1081
+ });
1082
+ try {
1083
+ let payload;
1084
+ if (read.tool === "get_business_snapshot" && this.merchantIntelligence !== undefined) {
1085
+ payload = await this.merchantIntelligence.getBusinessSnapshot({ merchant_id: this.principal.owner_id });
1086
+ }
1087
+ else if (read.tool === "get_negotiation_digest" && this.merchantIntelligence !== undefined) {
1088
+ payload = await this.merchantIntelligence.getNegotiationDigest({
1089
+ merchant_id: this.principal.owner_id,
1090
+ status: "active",
1091
+ limit: 20,
1092
+ });
1093
+ }
1094
+ else if (read.tool === "get_pending_actions" && this.merchantIntelligence !== undefined) {
1095
+ payload = await this.merchantIntelligence.getPendingActions();
1096
+ }
1097
+ else if (read.tool === "get_catalog_health" && this.merchantIntelligence !== undefined) {
1098
+ payload = await this.merchantIntelligence.getCatalogHealth({ merchant_id: this.principal.owner_id });
1099
+ }
1100
+ else if (read.tool === "get_human_review_queue" && this.merchantClient !== undefined) {
1101
+ payload = await this.merchantClient.getHumanReviewQueue(this.principal.owner_id);
1102
+ }
1103
+ else if (read.tool === "list_catalog_products" && this.merchantClient !== undefined) {
1104
+ payload = await this.merchantClient.listProducts(this.principal.owner_id);
1105
+ }
1106
+ else {
1107
+ throw new Error("grounding dependency is unavailable");
1108
+ }
1109
+ await this.emitEvent?.("grounding_completed", {
1110
+ rule: read.tool,
1111
+ tool: read.tool,
1112
+ status: "ok",
1113
+ });
1114
+ return [
1115
+ `[Grounding · ${read.reason}]`,
1116
+ fenceModelPayload(read.tool === "get_negotiation_digest" ? "a2a_message" : "merchant_api", payload, { maxChars: this.profile.merchant_experience?.max_external_context_chars ?? 12_000 }),
1117
+ ].join("\n");
1118
+ }
1119
+ catch {
1120
+ await this.emitEvent?.("grounding_completed", {
1121
+ rule: read.tool,
1122
+ tool: read.tool,
1123
+ status: "error",
1124
+ retryable: true,
1125
+ });
1126
+ return `[Grounding · ${read.reason}] 当前权威读取不可用,不要把缺失数据当作零值。`;
1127
+ }
1128
+ }
1129
+ /** Run at most two deterministic, read-only merchant grounding reads per turn. */
1130
+ async merchantGrounding(text) {
1131
+ if (this.profile.role !== "merchant" ||
1132
+ this.profile.merchant_experience?.enabled !== true ||
1133
+ this.profile.merchant_experience.grounding === false) {
1134
+ return undefined;
1135
+ }
1136
+ const reads = groundingReads(MERCHANT_GROUNDING_RULES, merchantGroundingContext(this.profile, text), 2);
1137
+ if (reads.length === 0)
1138
+ return undefined;
1139
+ return (await Promise.all(reads.map((read) => this.runMerchantGroundingRead(read)))).join("\n\n");
1140
+ }
889
1141
  /**
890
1142
  * 把一轮 A2A 磋商结果写入记忆(episode namespace,active 无需人工确认):
891
1143
  * 持久上下文,`/why` 可查、跨重启可恢复。
@@ -900,12 +1152,16 @@ export class AgentKernel {
900
1152
  if (text.startsWith("/")) {
901
1153
  return await this.handleSlash(text);
902
1154
  }
1155
+ const startedAt = Date.now();
1156
+ await this.emitEvent?.("message", { role: "user", text });
903
1157
  const memories = this.store.retrieve({
904
1158
  session_id: MAIN_SESSION_ID,
905
1159
  purpose: "clarify",
906
1160
  text,
907
1161
  });
908
- this.briefingSetter(renderMemoryBriefing(memories));
1162
+ const memoryBriefing = renderMemoryBriefing(memories);
1163
+ const groundingBriefing = await this.merchantGrounding(text);
1164
+ this.briefingSetter(combineDynamicBriefing([memoryBriefing, groundingBriefing]));
909
1165
  // Advance the turn id so several remember calls inside this one turn
910
1166
  // dedup into a single evidence piece (§9.3).
911
1167
  this.turnId.current = `${MAIN_SESSION_ID}:${++this.turnSeq}`;
@@ -915,20 +1171,35 @@ export class AgentKernel {
915
1171
  // §18.1: a model failure (throw OR empty response) must leave the
916
1172
  // conversation and TUI intact.
917
1173
  if (reply === "") {
1174
+ await this.emitEvent?.("error", {
1175
+ code: "empty_model_response",
1176
+ message: "model returned an empty response",
1177
+ retryable: true,
1178
+ });
918
1179
  return {
919
1180
  text: "暂时没能给出有效回复——当前目录里可能没有匹配的商品或商家,也可能是这条需求还不够具体。补充品牌、型号或预算范围后再问我一次即可。",
920
1181
  quit: false,
921
1182
  };
922
1183
  }
1184
+ await this.emitEvent?.("message", { role: "assistant", text: reply });
923
1185
  return { text: reply, quit: false };
924
1186
  }
925
1187
  catch (err) {
1188
+ await this.emitEvent?.("error", {
1189
+ code: "model_turn_failed",
1190
+ message: err instanceof Error ? err.message : String(err),
1191
+ retryable: true,
1192
+ });
926
1193
  return {
927
1194
  text: `模型处理失败:${err instanceof Error ? err.message : String(err)}。对话状态未改变,请重试。`,
928
1195
  quit: false,
929
1196
  };
930
1197
  }
931
1198
  finally {
1199
+ await this.emitEvent?.("turn_complete", {
1200
+ elapsed_ms: Date.now() - startedAt,
1201
+ stop_reason: "complete",
1202
+ });
932
1203
  this.briefingSetter(undefined);
933
1204
  }
934
1205
  });
@@ -1258,6 +1529,7 @@ export class AgentKernel {
1258
1529
  catch {
1259
1530
  // best-effort flush; closing the store is what must not be skipped
1260
1531
  }
1532
+ this.unsubscribeHarnessEvents?.();
1261
1533
  this.db.close();
1262
1534
  }
1263
1535
  }