@t2000/engine 1.18.0 → 1.20.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/dist/index.js CHANGED
@@ -19,7 +19,7 @@ function buildTool(opts) {
19
19
  jsonSchema: opts.jsonSchema,
20
20
  call: opts.call,
21
21
  isReadOnly,
22
- isConcurrencySafe: isReadOnly,
22
+ isConcurrencySafe: opts.isConcurrencySafe ?? isReadOnly,
23
23
  permissionLevel: opts.permissionLevel ?? (isReadOnly ? "auto" : "confirm"),
24
24
  flags: opts.flags ?? {},
25
25
  preflight: opts.preflight,
@@ -5171,14 +5171,22 @@ var todoStatusSchema = z.enum(["pending", "in_progress", "completed"]);
5171
5171
  var todoItemSchema = z.object({
5172
5172
  id: z.string().min(1, "id must be a non-empty string").max(40, "id must be \u226440 chars (use a slug, not a sentence)"),
5173
5173
  label: z.string().min(1, "label must be a non-empty string").max(80, "label must be \u226480 chars (the whole point of this tool is concision)"),
5174
- status: todoStatusSchema
5174
+ status: todoStatusSchema,
5175
+ // [SPEC 9 v0.1.3 P9.3] Per-item persistence flag — opt this todo item
5176
+ // into the long-lived `Goal` row surface. Hosts that wire goal storage
5177
+ // (audric) write a Goal row with `content: label`, `status: 'in_progress'`,
5178
+ // `sourceSessionId: <currentSession>` when this flag is true. Default
5179
+ // false — most turn-scoped items don't survive the turn. Engine is
5180
+ // unaware of how/where the host persists; it just passes the flag
5181
+ // through on the `todo_update` side-channel event.
5182
+ persist: z.boolean().optional()
5175
5183
  });
5176
5184
  var inputSchema6 = z.object({
5177
5185
  items: z.array(todoItemSchema).min(1, "items must contain at least 1 entry").max(8, "items must contain at most 8 entries (SPEC 8 ceiling)")
5178
5186
  });
5179
5187
  var updateTodoTool = buildTool({
5180
5188
  name: "update_todo",
5181
- description: "Declare or replace your plan for the current turn as a structured todo list. Call this when the user's ask is multi-step (\u22653 tools, \u22652 reasoning hops) so the user can see what you're doing as you do it. Each call replaces the entire list \u2014 the tool is idempotent. \n\nRules: 1\u20138 items, each label \u226480 chars, exactly 1 item must be `in_progress`. Use stable `id`s across calls within the same turn so the UI can track item transitions (e.g. `id: 'check-balance'` first as `pending`, later as `completed`). \n\nDO NOT call this for single-step asks ('balance', 'rate') \u2014 it's wasted tokens. DO call it before kicking off long flows where the user benefits from seeing the plan unfold ('save my idle USDC' \u2192 check balance \u2192 check rates \u2192 compute split \u2192 propose). \n\nThis call doesn't count against your turn budget \u2014 re-narrating the plan as items move from pending \u2192 in_progress \u2192 completed is encouraged.",
5189
+ description: "Declare or replace your plan for the current turn as a structured todo list. Call this when the user's ask is multi-step (\u22653 tools, \u22652 reasoning hops) so the user can see what you're doing as you do it. Each call replaces the entire list \u2014 the tool is idempotent. \n\nRules: 1\u20138 items, each label \u226480 chars, exactly 1 item must be `in_progress`. Use stable `id`s across calls within the same turn so the UI can track item transitions (e.g. `id: 'check-balance'` first as `pending`, later as `completed`). \n\nDO NOT call this for single-step asks ('balance', 'rate') \u2014 it's wasted tokens. DO call it before kicking off long flows where the user benefits from seeing the plan unfold ('save my idle USDC' \u2192 check balance \u2192 check rates \u2192 compute split \u2192 propose). \n\nThis call doesn't count against your turn budget \u2014 re-narrating the plan as items move from pending \u2192 in_progress \u2192 completed is encouraged. \n\n[SPEC 9 v0.1.3] To promote an item into a long-lived goal that survives across sessions, set `persist: true` on that item. Reserve this for multi-week commitments the user explicitly wants remembered (e.g. \"save $500 by month-end\", \"track NAVI USDC APY weekly\"). DO NOT set persist on within-turn steps (\"check balance\", \"compute split\") \u2014 those vanish when the turn ends, which is what you want. Default behaviour (no persist field) is don't-persist.",
5182
5190
  inputSchema: inputSchema6,
5183
5191
  jsonSchema: {
5184
5192
  type: "object",
@@ -5202,6 +5210,10 @@ var updateTodoTool = buildTool({
5202
5210
  type: "string",
5203
5211
  enum: ["pending", "in_progress", "completed"],
5204
5212
  description: "Lifecycle state. Exactly one item must be `in_progress` per call."
5213
+ },
5214
+ persist: {
5215
+ type: "boolean",
5216
+ description: 'Set true to promote this item into a long-lived goal that survives across sessions (e.g. "save $500 by month-end"). Default false \u2014 only set true when the item represents a multi-week / multi-session commitment, not a within-turn step. When false or omitted, the item lives only for this turn.'
5205
5217
  }
5206
5218
  },
5207
5219
  required: ["id", "label", "status"]
@@ -5268,6 +5280,92 @@ var updateTodoTool = buildTool({
5268
5280
  };
5269
5281
  }
5270
5282
  });
5283
+ var ADD_RECIPIENT_FORM = {
5284
+ fields: [
5285
+ {
5286
+ name: "name",
5287
+ label: "Nickname",
5288
+ kind: "text",
5289
+ required: true,
5290
+ placeholder: "Mom",
5291
+ helpText: "How you'll refer to this contact in chat."
5292
+ },
5293
+ {
5294
+ name: "identifier",
5295
+ label: "Audric handle, SuiNS name, or wallet address",
5296
+ // [v0.1.3 R6] Polymorphic kind — accepts handles, names, and 0x.
5297
+ // The host renderer treats this as a single text input with
5298
+ // help-text guidance; server-side `normalizeAddressInput` does
5299
+ // the resolution.
5300
+ kind: "sui-recipient",
5301
+ required: true,
5302
+ placeholder: "mom.audric.sui / alex.sui / 0x40cd\u20263e62",
5303
+ helpText: "Type @alice for an Audric user, alex.sui for any SuiNS, or paste a 0x address. We'll resolve it to the canonical wallet automatically."
5304
+ }
5305
+ ]
5306
+ };
5307
+ var addRecipientTool = buildTool({
5308
+ name: "add_recipient",
5309
+ description: `Add a new contact to the user's saved-recipients list. Call this when you (the LLM) need to reference a contact that isn't saved yet \u2014 e.g. the user said "send $10 to Mom" but no contact named "Mom" exists. The user will fill in the nickname + identifier (an Audric handle, SuiNS name, or wallet address) via an inline form. The contact is persisted before this tool returns; the resumed call returns confirmation only. Do NOT call when the user manually opens the contact-add UI from settings \u2014 that's a separate user-initiated flow that doesn't need the LLM. Only call when YOU need to add a contact mid-conversation to make a downstream action work.`,
5310
+ inputSchema: z.object({
5311
+ name: z.string().min(1).optional(),
5312
+ identifier: z.string().min(1).optional()
5313
+ }),
5314
+ jsonSchema: {
5315
+ type: "object",
5316
+ properties: {
5317
+ name: {
5318
+ type: "string",
5319
+ description: 'Optional nickname for the contact ("Mom"). Omit to let the user fill the form.'
5320
+ },
5321
+ identifier: {
5322
+ type: "string",
5323
+ description: "Optional polymorphic identifier (Audric handle, SuiNS name, or 0x address). Omit to let the user fill the form."
5324
+ }
5325
+ },
5326
+ required: []
5327
+ },
5328
+ // Permission: read-only from the engine's perspective. The HOST writes
5329
+ // the Contact row in the resume endpoint; the tool's call() body is a
5330
+ // thin confirmation message. Treating as read-only keeps the engine's
5331
+ // permission gate from yielding `pending_action` (which would conflict
5332
+ // with the `pending_input` flow — you'd get a form, then a confirm card,
5333
+ // for what's semantically one user action).
5334
+ isReadOnly: true,
5335
+ // [SPEC 9 v0.1.3 P9.4] Opt out of EarlyToolDispatcher. Early-dispatch
5336
+ // runs the tool's call() mid-stream BEFORE the post-stream guard loop
5337
+ // (where preflight is invoked). If add_recipient ran via early-dispatch,
5338
+ // call() would fire with name/identifier=undefined and "save" garbage
5339
+ // before the form pause path is even consulted. Forcing the tool
5340
+ // through the post-stream loop guarantees preflight runs first.
5341
+ isConcurrencySafe: false,
5342
+ permissionLevel: "auto",
5343
+ flags: {},
5344
+ preflight: (input) => {
5345
+ if (!input.name || !input.identifier) {
5346
+ return {
5347
+ valid: false,
5348
+ needsInput: {
5349
+ schema: ADD_RECIPIENT_FORM,
5350
+ description: "Add a new contact"
5351
+ }
5352
+ };
5353
+ }
5354
+ return { valid: true };
5355
+ },
5356
+ async call(input) {
5357
+ const name = input.name;
5358
+ const identifier = input.identifier;
5359
+ return {
5360
+ data: {
5361
+ saved: true,
5362
+ name,
5363
+ identifier
5364
+ },
5365
+ displayText: `Saved ${name} (${identifier}) to contacts.`
5366
+ };
5367
+ }
5368
+ });
5271
5369
  var tokenPricesTool = buildTool({
5272
5370
  name: "token_prices",
5273
5371
  description: 'Get current USD prices for Sui tokens, with optional 24h change. Accepts full coin type strings (e.g. "0x2::sui::SUI"). Returns price per token and (when requested) 24h change percentage. Use for "what is X worth?" or "did Y move today?". For balance + portfolio rendering, prefer balance_check / portfolio_analysis instead \u2014 they bundle the same prices into the standard cards.',
@@ -5467,7 +5565,73 @@ Only offer to execute actions you have tools for. If you retrieved a quote, data
5467
5565
  ## Safety
5468
5566
  - Never encourage risky financial behavior.
5469
5567
  - Warn when health factor < 1.5.
5470
- - All amounts in USDC unless stated otherwise.`;
5568
+ - All amounts in USDC unless stated otherwise.
5569
+
5570
+ ## Proactive insights (only when there's a clear opportunity)
5571
+ - When you spot a financial insight worth surfacing \u2014 idle balance worth saving, health factor approaching the warning band, APY drift on a known position, progress against a saved goal \u2014 emit a \`<proactive type="..." subjectKey="...">BODY</proactive>\` block. ALWAYS use the wrapper \u2014 plain-text proactive prose without the wrapper renders as regular text and skips the engine's per-session cooldown (the same nudge will then re-fire every turn).
5572
+ - Two valid placements \u2014 pick whichever fits the turn:
5573
+ - **No user question** (or the question is unrelated to the insight): wrap your ENTIRE response in the \`<proactive>\` block.
5574
+ - **You're answering a user question AND have a related insight to add**: answer the question normally, then APPEND the \`<proactive>\` block at the end, separated by a line break. The "after the answer" form is also taught in detail under \xA7 Proactive Awareness \u2014 same syntax, both placements valid.
5575
+ - The host renders the wrapped block with a distinct "\u2726 ADDED BY AUDRIC" lockup so the user knows this is your suggestion, not an answer.
5576
+ - Allowed types (closed list \u2014 anything else is dropped): \`idle_balance\` (cash sitting idle that could earn yield), \`hf_warning\` (debt position approaching liquidation), \`apy_drift\` (rate change on a position they hold), \`goal_progress\` (update on a saved goal).
5577
+ - \`subjectKey\` is a stable identifier for the SPECIFIC subject \u2014 examples: \`USDC\` for an idle-balance insight on USDC, \`1.45\` for a HF warning at that level, \`save-500-by-may\` for goal progress. Same (type, subjectKey) won't fire twice in one session \u2014 pick the same key for the same subject so the engine cooldown works.
5578
+ - Cap: at most ONE proactive block per turn.
5579
+ - Skip proactive blocks when nothing notable changed since the last turn, when the user is mid-flow on something else, or when you'd just be restating the financial-context block. Quality over quantity \u2014 a block ignored is worse than no block.`;
5580
+
5581
+ // src/proactive-marker.ts
5582
+ var VALID_TYPES = /* @__PURE__ */ new Set([
5583
+ "idle_balance",
5584
+ "hf_warning",
5585
+ "apy_drift",
5586
+ "goal_progress"
5587
+ ]);
5588
+ var MARKER_REGEX = /<proactive\s+([^>]+)>([\s\S]*?)<\/proactive>/g;
5589
+ var ATTR_TYPE_REGEX = /\btype\s*=\s*"([^"]+)"/;
5590
+ var ATTR_SUBJECT_KEY_REGEX = /\bsubjectKey\s*=\s*"([^"]+)"/;
5591
+ function parseProactiveMarker(text) {
5592
+ if (!text.includes("<proactive")) return null;
5593
+ const matches = [];
5594
+ for (const match of text.matchAll(MARKER_REGEX)) {
5595
+ matches.push({ attrs: match[1] ?? "", body: match[2] ?? "" });
5596
+ }
5597
+ if (matches.length === 0) return null;
5598
+ for (const { attrs, body } of matches) {
5599
+ const typeMatch = attrs.match(ATTR_TYPE_REGEX);
5600
+ const subjectKeyMatch = attrs.match(ATTR_SUBJECT_KEY_REGEX);
5601
+ if (!typeMatch || !subjectKeyMatch) continue;
5602
+ const proactiveType = typeMatch[1];
5603
+ const subjectKey = subjectKeyMatch[1].trim();
5604
+ if (!VALID_TYPES.has(proactiveType)) continue;
5605
+ if (subjectKey.length === 0) continue;
5606
+ const trimmedBody = body.trim();
5607
+ if (trimmedBody.length === 0) continue;
5608
+ return {
5609
+ proactiveType,
5610
+ subjectKey,
5611
+ body: trimmedBody,
5612
+ markerCount: matches.length
5613
+ };
5614
+ }
5615
+ return null;
5616
+ }
5617
+ function stripProactiveMarkers(text) {
5618
+ if (!text.includes("<proactive")) return text;
5619
+ return text.replace(MARKER_REGEX, (_match, _attrs, body) => body);
5620
+ }
5621
+ function extractAllProactiveMarkers(text) {
5622
+ if (!text.includes("<proactive")) return [];
5623
+ const out = [];
5624
+ for (const match of text.matchAll(MARKER_REGEX)) {
5625
+ const attrs = match[1] ?? "";
5626
+ const typeMatch = attrs.match(ATTR_TYPE_REGEX);
5627
+ const subjectKeyMatch = attrs.match(ATTR_SUBJECT_KEY_REGEX);
5628
+ if (!typeMatch || !subjectKeyMatch) continue;
5629
+ const subjectKey = subjectKeyMatch[1].trim();
5630
+ if (subjectKey.length === 0) continue;
5631
+ out.push({ proactiveType: typeMatch[1], subjectKey });
5632
+ }
5633
+ return out;
5634
+ }
5471
5635
 
5472
5636
  // src/cost.ts
5473
5637
  var DEFAULT_INPUT_COST = 3 / 1e6;
@@ -6024,6 +6188,29 @@ function runGuards(tool, call, state, config, conversationContext, onGuardFired,
6024
6188
  if (config.inputValidation !== false && tool.preflight) {
6025
6189
  const check = tool.preflight(call.input);
6026
6190
  if (!check.valid) {
6191
+ if ("needsInput" in check && check.needsInput) {
6192
+ const event2 = {
6193
+ timestamp: now,
6194
+ toolName: tool.name,
6195
+ toolUseId: call.id,
6196
+ gate: "input_validation",
6197
+ verdict: "pass",
6198
+ tier: "safety",
6199
+ message: check.needsInput.description
6200
+ };
6201
+ fire("pass", "safety", "input_validation", false);
6202
+ return {
6203
+ blocked: false,
6204
+ injections: [],
6205
+ events: [event2],
6206
+ needsInput: check.needsInput
6207
+ };
6208
+ }
6209
+ if (!("error" in check)) {
6210
+ throw new Error(
6211
+ `Preflight returned a non-needsInput, non-error invalid result for tool ${tool.name}`
6212
+ );
6213
+ }
6027
6214
  const event = {
6028
6215
  timestamp: now,
6029
6216
  toolName: tool.name,
@@ -6945,6 +7132,26 @@ var QueryEngine = class {
6945
7132
  // their `finally` block so they DON'T clear the cache mid-turn — the
6946
7133
  // pending write may resume, and the cache should survive the pause.
6947
7134
  turnPaused = false;
7135
+ // [SPEC 9 v0.1.1 P9.2 / R3] Per-engine-instance cooldown set for
7136
+ // proactive insight markers. Same `(proactiveType, subjectKey)` tuple
7137
+ // emitted twice in one session ⇒ second emission is suppressed (host
7138
+ // strips the wrapper, renders body as plain text). Cooldown is
7139
+ // per-session, not cross-session — a fresh QueryEngine on the next
7140
+ // user session starts with an empty set and may re-emit the same
7141
+ // tuple. Drops automatically when the engine is GC'd.
7142
+ //
7143
+ // Storage shape: serialised key `${type}:${subjectKey}` for cheap
7144
+ // Set membership without an extra hash function.
7145
+ proactiveCooldown = /* @__PURE__ */ new Set();
7146
+ // [SPEC 9 v0.1.3 P9.4] In-flight `pending_input` state, keyed by `inputId`.
7147
+ // Set when the agent loop hits a tool whose preflight returned `needsInput`;
7148
+ // consulted by `resumeWithInput()` to look up which paused tool call to
7149
+ // re-feed with the user's submitted values. Cleared on resume (whether
7150
+ // success or abandonment via a fresh `submitMessage`). Multiple concurrent
7151
+ // entries are allowed in principle (LLM could call two `add_recipient`s
7152
+ // in one turn) but in practice we yield + return on the FIRST hit, so
7153
+ // the map is almost always 0 or 1 entries deep.
7154
+ pendingInputs = /* @__PURE__ */ new Map();
6948
7155
  constructor(config) {
6949
7156
  this.provider = config.provider;
6950
7157
  this.agent = config.agent;
@@ -7168,6 +7375,159 @@ var QueryEngine = class {
7168
7375
  }
7169
7376
  }
7170
7377
  }
7378
+ /**
7379
+ * [SPEC 9 v0.1.3 P9.4] Resume a turn that paused on `pending_input`.
7380
+ *
7381
+ * Called by the host after the user submitted the inline form. The
7382
+ * `pendingInput` argument is the EXACT payload that was yielded as
7383
+ * `pending_input` (host stored it in session storage); `values` is the
7384
+ * `{ fieldName: value }` map the user submitted, post-host-validation
7385
+ * against `pendingInput.schema`.
7386
+ *
7387
+ * Engine responsibilities (in order):
7388
+ * 1. Push the captured `assistantContent` (assistant blocks from the
7389
+ * paused turn — includes the tool_use that triggered this pause).
7390
+ * 2. Run the paused tool's `call()` with `values` as input. Re-runs
7391
+ * preflight first as a defense-in-depth gate (host SHOULD have
7392
+ * validated against the schema, but malformed input would otherwise
7393
+ * poison the conversation history with an orphan tool_use).
7394
+ * 3. Combine `completedResults` (read tool_results that already ran in
7395
+ * the same turn before the pause) with the new tool_result into ONE
7396
+ * user-role message. Anthropic's API requires every tool_use to have
7397
+ * a tool_result in the immediately-following user message — splitting
7398
+ * them would produce two consecutive user messages, which the API
7399
+ * rejects.
7400
+ * 4. Yield `tool_result` for the host's timeline + run `agentLoop` to
7401
+ * continue the turn (LLM gets the resumed result and narrates).
7402
+ *
7403
+ * Mirrors `resumeWithToolResult` for the user-confirm case but feeds
7404
+ * the values back as the tool's INPUT (the call() runs here for the
7405
+ * first time) instead of as the tool's OUTPUT (the call() ran on the
7406
+ * client side via sponsored-tx for pending_action).
7407
+ */
7408
+ async *resumeWithInput(pendingInput, values) {
7409
+ this.abortController = new AbortController();
7410
+ const signal = this.abortController.signal;
7411
+ if (pendingInput.assistantContent.length > 0) {
7412
+ this.messages.push({
7413
+ role: "assistant",
7414
+ content: pendingInput.assistantContent
7415
+ });
7416
+ }
7417
+ const tool = findTool(this.tools, pendingInput.toolName);
7418
+ let resumedToolResult;
7419
+ let toolResultEventPayload;
7420
+ if (!tool) {
7421
+ const errorPayload = {
7422
+ error: `Tool "${pendingInput.toolName}" not found on resume \u2014 engine may have been reconfigured between pause and resume.`,
7423
+ _hostBugMissingTool: true
7424
+ };
7425
+ resumedToolResult = {
7426
+ content: JSON.stringify(errorPayload),
7427
+ isError: true
7428
+ };
7429
+ toolResultEventPayload = { result: errorPayload, isError: true };
7430
+ } else {
7431
+ const preflightCheck = tool.preflight ? tool.preflight(values) : { valid: true };
7432
+ if (!preflightCheck.valid && "error" in preflightCheck) {
7433
+ const errorPayload = {
7434
+ error: `Form values failed re-validation: ${preflightCheck.error}`,
7435
+ _hostFormValidationFailed: true
7436
+ };
7437
+ resumedToolResult = {
7438
+ content: JSON.stringify(errorPayload),
7439
+ isError: true
7440
+ };
7441
+ toolResultEventPayload = { result: errorPayload, isError: true };
7442
+ } else if (!preflightCheck.valid && "needsInput" in preflightCheck) {
7443
+ const errorPayload = {
7444
+ error: "Tool requested another form on resume \u2014 multi-step forms are not supported in v0.1.3. Re-prompt the user with a fresh single-step request.",
7445
+ _multiStepFormUnsupported: true
7446
+ };
7447
+ resumedToolResult = {
7448
+ content: JSON.stringify(errorPayload),
7449
+ isError: true
7450
+ };
7451
+ toolResultEventPayload = { result: errorPayload, isError: true };
7452
+ } else {
7453
+ const parsed = tool.inputSchema.safeParse(values);
7454
+ if (!parsed.success) {
7455
+ const errorPayload = {
7456
+ error: `Invalid input on resume: ${parsed.error.issues.map((i) => i.message).join(", ")}`,
7457
+ _hostFormZodFailed: true
7458
+ };
7459
+ resumedToolResult = {
7460
+ content: JSON.stringify(errorPayload),
7461
+ isError: true
7462
+ };
7463
+ toolResultEventPayload = { result: errorPayload, isError: true };
7464
+ } else {
7465
+ const context = {
7466
+ agent: this.agent,
7467
+ mcpManager: this.mcpManager,
7468
+ walletAddress: this.walletAddress,
7469
+ suiRpcUrl: this.suiRpcUrl,
7470
+ serverPositions: this.serverPositions,
7471
+ positionFetcher: this.positionFetcher,
7472
+ env: this.env,
7473
+ signal,
7474
+ priceCache: this.priceCache,
7475
+ permissionConfig: this.permissionConfig,
7476
+ sessionSpendUsd: this.sessionSpendUsd,
7477
+ blockvisionApiKey: this.blockvisionApiKey,
7478
+ portfolioCache: this.portfolioCache
7479
+ };
7480
+ try {
7481
+ const callResult = await tool.call(parsed.data, context);
7482
+ resumedToolResult = {
7483
+ content: JSON.stringify(callResult.data),
7484
+ isError: false
7485
+ };
7486
+ toolResultEventPayload = { result: callResult.data, isError: false };
7487
+ } catch (err) {
7488
+ const message = err instanceof Error ? err.message : "Tool execution failed";
7489
+ const errorPayload = { error: message };
7490
+ resumedToolResult = {
7491
+ content: JSON.stringify(errorPayload),
7492
+ isError: true
7493
+ };
7494
+ toolResultEventPayload = { result: errorPayload, isError: true };
7495
+ }
7496
+ }
7497
+ }
7498
+ }
7499
+ const userMessageBlocks = [
7500
+ ...pendingInput.completedResults.map((r) => ({
7501
+ type: "tool_result",
7502
+ toolUseId: r.toolUseId,
7503
+ content: r.content,
7504
+ isError: r.isError
7505
+ })),
7506
+ {
7507
+ type: "tool_result",
7508
+ toolUseId: pendingInput.toolUseId,
7509
+ content: resumedToolResult.content,
7510
+ isError: resumedToolResult.isError
7511
+ }
7512
+ ];
7513
+ this.messages.push({ role: "user", content: userMessageBlocks });
7514
+ yield {
7515
+ type: "tool_result",
7516
+ toolName: pendingInput.toolName,
7517
+ toolUseId: pendingInput.toolUseId,
7518
+ result: toolResultEventPayload.result,
7519
+ isError: toolResultEventPayload.isError
7520
+ };
7521
+ this.pendingInputs.delete(pendingInput.inputId);
7522
+ this.turnPaused = false;
7523
+ try {
7524
+ yield* this.agentLoop(null, signal, false);
7525
+ } finally {
7526
+ if (!this.turnPaused) {
7527
+ this.turnReadCache.clear();
7528
+ }
7529
+ }
7530
+ }
7171
7531
  /**
7172
7532
  * [v1.5] Auto-run configured read tools after a successful write,
7173
7533
  * push their results into the conversation, and yield `tool_result`
@@ -7338,6 +7698,25 @@ var QueryEngine = class {
7338
7698
  }
7339
7699
  loadMessages(messages) {
7340
7700
  this.messages = [...messages];
7701
+ this.rehydrateProactiveCooldown();
7702
+ }
7703
+ /**
7704
+ * [SPEC 9 v0.1.1 P9.2 / R3] Walk loaded assistant text blocks and seed the
7705
+ * cooldown set with every parseable `(proactiveType, subjectKey)` tuple.
7706
+ * Called automatically from `loadMessages`. Idempotent — safe to call on
7707
+ * an already-populated set; duplicates are absorbed by Set semantics.
7708
+ */
7709
+ rehydrateProactiveCooldown() {
7710
+ for (const message of this.messages) {
7711
+ if (message.role !== "assistant") continue;
7712
+ const blocks = Array.isArray(message.content) ? message.content : [];
7713
+ for (const block of blocks) {
7714
+ if (block.type !== "text" || typeof block.text !== "string") continue;
7715
+ for (const { proactiveType, subjectKey } of extractAllProactiveMarkers(block.text)) {
7716
+ this.proactiveCooldown.add(`${proactiveType}:${subjectKey}`);
7717
+ }
7718
+ }
7719
+ }
7341
7720
  }
7342
7721
  /**
7343
7722
  * [v0.46.7] Run a read-only tool out-of-band, using the engine's tool
@@ -7745,6 +8124,45 @@ ${recipeCtx}`;
7745
8124
  });
7746
8125
  continue;
7747
8126
  }
8127
+ if (check.needsInput) {
8128
+ const inputId = randomUUID();
8129
+ const pendingInput = {
8130
+ inputId,
8131
+ toolName: call.name,
8132
+ toolUseId: call.id,
8133
+ schema: check.needsInput.schema,
8134
+ description: check.needsInput.description,
8135
+ assistantContent: acc.assistantBlocks,
8136
+ completedResults: toolResultBlocks.map((b) => ({
8137
+ toolUseId: b.toolUseId,
8138
+ content: b.content,
8139
+ isError: b.isError ?? false
8140
+ }))
8141
+ };
8142
+ this.pendingInputs.set(inputId, pendingInput);
8143
+ this.turnPaused = true;
8144
+ getTelemetrySink().counter("engine.pending_input_emitted", {
8145
+ tool: call.name
8146
+ });
8147
+ yield {
8148
+ type: "pending_input",
8149
+ inputId,
8150
+ toolName: call.name,
8151
+ toolUseId: call.id,
8152
+ schema: check.needsInput.schema,
8153
+ description: check.needsInput.description,
8154
+ // [SPEC 9 v0.1.3 P9.4] Round-trip fields on the wire so
8155
+ // stateless hosts (audric — request-scoped engines) can
8156
+ // persist + echo back on resume. In-process hosts (CLI,
8157
+ // long-lived engine instances) can ignore — the engine
8158
+ // also stashes the state on `this.pendingInputs[inputId]`
8159
+ // for in-memory recall.
8160
+ assistantContent: pendingInput.assistantContent,
8161
+ completedResults: pendingInput.completedResults
8162
+ };
8163
+ recordTurnOutcome("pending_input");
8164
+ return;
8165
+ }
7748
8166
  if (check.injections.length > 0) {
7749
8167
  call._guardInjections = check.injections;
7750
8168
  }
@@ -8081,6 +8499,32 @@ ${recipeCtx}`;
8081
8499
  yield { type: "text_delta", text: event.text };
8082
8500
  break;
8083
8501
  }
8502
+ case "text_done": {
8503
+ if (event.proactiveMarker) {
8504
+ const { proactiveType, subjectKey, body, markerCount } = event.proactiveMarker;
8505
+ const dedupKey = `${proactiveType}:${subjectKey}`;
8506
+ const suppressed = this.proactiveCooldown.has(dedupKey);
8507
+ if (!suppressed) this.proactiveCooldown.add(dedupKey);
8508
+ const sink = getTelemetrySink();
8509
+ if (suppressed) {
8510
+ sink.counter("audric.harness.proactive_text_suppressed_count", { reason: "cooldown" }, 1);
8511
+ } else {
8512
+ sink.counter("audric.harness.proactive_text_emitted_count", { type: proactiveType }, 1);
8513
+ }
8514
+ if (markerCount > 1) {
8515
+ sink.counter("audric.harness.proactive_marker_violations_count", {}, 1);
8516
+ }
8517
+ yield {
8518
+ type: "proactive_text",
8519
+ proactiveType,
8520
+ subjectKey,
8521
+ body,
8522
+ suppressed,
8523
+ markerCount
8524
+ };
8525
+ }
8526
+ break;
8527
+ }
8084
8528
  case "tool_use_done": {
8085
8529
  if (acc.text) {
8086
8530
  acc.assistantBlocks.push({ type: "text", text: acc.text });
@@ -8680,7 +9124,7 @@ function classifyEffort(model, userMessage, matchedRecipe, sessionWriteCount) {
8680
9124
  }
8681
9125
 
8682
9126
  // src/eval-summary.ts
8683
- var MARKER_REGEX = /<eval_summary>([\s\S]*?)<\/eval_summary>/g;
9127
+ var MARKER_REGEX2 = /<eval_summary>([\s\S]*?)<\/eval_summary>/g;
8684
9128
  var VALID_STATUSES = /* @__PURE__ */ new Set([
8685
9129
  "good",
8686
9130
  "warning",
@@ -8690,7 +9134,7 @@ var VALID_STATUSES = /* @__PURE__ */ new Set([
8690
9134
  function parseEvalSummary(thinkingText) {
8691
9135
  if (!thinkingText.includes("<eval_summary>")) return null;
8692
9136
  const matches = [];
8693
- for (const match of thinkingText.matchAll(MARKER_REGEX)) {
9137
+ for (const match of thinkingText.matchAll(MARKER_REGEX2)) {
8694
9138
  matches.push(match[1] ?? "");
8695
9139
  }
8696
9140
  if (matches.length === 0) return null;
@@ -8791,7 +9235,10 @@ context is worth mentioning. ${brevityGuidance}
8791
9235
  - Would seem pushy or sales-y
8792
9236
 
8793
9237
  ${styleGuidance}
8794
- Format: One sentence maximum, after main response, separated by a line break.
9238
+ Format: One sentence maximum, AFTER your main response, separated by a line break, WRAPPED in a \`<proactive type="..." subjectKey="...">BODY</proactive>\` block. The host renders the wrapped block with the "\u2726 ADDED BY AUDRIC" lockup styling \u2014 without the wrapper the host shows only plain text and the engine's per-session cooldown won't deduplicate future repeats (so the same nudge re-fires every turn).
9239
+ Allowed types (closed list \u2014 anything else is dropped by the host): \`idle_balance\` (cash sitting idle that could earn yield), \`hf_warning\` (debt approaching liquidation), \`apy_drift\` (rate change on a position they hold), \`goal_progress\` (update on a saved goal).
9240
+ \`subjectKey\` is a stable identifier for the SPECIFIC subject (e.g. "USDC" for an idle-USDC insight, "1.45" for HF at that level, "tokyo-trip" for a saved goal). Same (type, subjectKey) won't fire twice in one session \u2014 pick the same key for the same subject so cooldown works.
9241
+ Example (post-answer suffix form): \`<proactive type="goal_progress" subjectKey="tokyo-trip">Your Tokyo goal is $80 behind pace.</proactive>\`
8795
9242
  Frame as observation, not advice: "Your Tokyo goal is $80 behind pace." \u2014 not "You should deposit more."`;
8796
9243
  }
8797
9244
  function buildSelfEvaluationInstruction() {
@@ -9223,6 +9670,7 @@ var AnthropicProvider = class {
9223
9670
  const stream = params.signal ? this.client.messages.stream(streamParams, { signal: params.signal }) : this.client.messages.stream(streamParams);
9224
9671
  const toolInputBuffers = /* @__PURE__ */ new Map();
9225
9672
  const thinkingBuffers = /* @__PURE__ */ new Map();
9673
+ const textBuffers = /* @__PURE__ */ new Map();
9226
9674
  let outputTokensFromStart = 0;
9227
9675
  try {
9228
9676
  for await (const event of stream) {
@@ -9264,12 +9712,16 @@ var AnthropicProvider = class {
9264
9712
  thinkingBuffers.set(event.index, { type: "thinking", text: "", signature: "" });
9265
9713
  } else if (block.type === "redacted_thinking") {
9266
9714
  thinkingBuffers.set(event.index, { type: "redacted_thinking", data: block.data ?? "" });
9715
+ } else if (block.type === "text") {
9716
+ textBuffers.set(event.index, { text: "" });
9267
9717
  }
9268
9718
  break;
9269
9719
  }
9270
9720
  case "content_block_delta": {
9271
9721
  const delta = event.delta;
9272
9722
  if (delta.type === "text_delta") {
9723
+ const buf = textBuffers.get(event.index);
9724
+ if (buf) buf.text += delta.text ?? "";
9273
9725
  yield { type: "text_delta", text: delta.text };
9274
9726
  } else if (delta.type === "input_json_delta") {
9275
9727
  const buf = toolInputBuffers.get(event.index);
@@ -9323,6 +9775,15 @@ var AnthropicProvider = class {
9323
9775
  yield { type: "redacted_thinking", data: thinkBuf.data };
9324
9776
  thinkingBuffers.delete(event.index);
9325
9777
  }
9778
+ const textBuf = textBuffers.get(event.index);
9779
+ if (textBuf) {
9780
+ const proactiveMarker = parseProactiveMarker(textBuf.text);
9781
+ yield {
9782
+ type: "text_done",
9783
+ ...proactiveMarker ? { proactiveMarker } : {}
9784
+ };
9785
+ textBuffers.delete(event.index);
9786
+ }
9326
9787
  break;
9327
9788
  }
9328
9789
  case "message_delta": {
@@ -9531,6 +9992,6 @@ function sanitizeAnthropicMessages(messages) {
9531
9992
  return merged;
9532
9993
  }
9533
9994
 
9534
- export { AnthropicProvider, BalanceTracker, CANVAS_TEMPLATES, ContextBudget, CostTracker, DEFAULT_GUARD_CONFIG, DEFAULT_LEASE_SEC, DEFAULT_PERMISSION_CONFIG, DEFAULT_POLL_BUDGET_MS, DEFAULT_POLL_INTERVAL_MS, DEFAULT_SYSTEM_PROMPT, DEFAULT_TOOL_TTL_MS, EFFORT_THINKING_BUDGET_CAPS, EarlyToolDispatcher, InMemoryDefiCacheStore, InMemoryFetchLock, InMemoryNaviCacheStore, InMemoryWalletCacheStore, InvalidAddressError, MAX_BUNDLE_OPS, McpClientManager, McpResponseCache, MemorySessionStore, NAVI_ADDR_TTL_SEC, NAVI_MCP_CONFIG, NAVI_MCP_URL, NAVI_RATES_TTL_SEC, NAVI_SERVER_NAME, NaviTools, PERMISSION_PRESETS, QueryEngine, READ_TOOLS, REGENERATABLE_READ_TOOLS, RecipeRegistry, RetryTracker, SUINS_NAME_REGEX, SUI_ADDRESS_REGEX, SUI_ADDRESS_STRICT_REGEX, SuinsNotRegisteredError, SuinsRpcError, TOOL_FLAGS, TOOL_MODIFIABLE_FIELDS, TOOL_TTL_MS, TxMutex, VALID_PAIRS, WRITE_TOOLS, _resetNaviCircuitBreaker, activitySummaryTool, adaptAllMcpTools, adaptAllServerTools, adaptMcpTool, applyToolFlags, awaitOrFetch, balanceCheckTool, borrowTool, budgetToolResult, buildCachedSystemPrompt, buildMcpTools, buildProactivenessInstructions, buildProfileContext, buildSelfEvaluationInstruction, buildStateContext, buildTool, bundleShortestTtl, checkValidPair, claimRewardsTool, clampThinkingForEffort, classifyEffort, clearPortfolioCache, clearPortfolioCacheFor, clearPriceMapCache, compactMessages, composeBundleFromToolResults, computeRegenerateFields, createGuardRunnerState, engineToSSE, estimateTokens, explainTxTool, extractConversationText, extractMcpText, fetchAddressDefiPortfolio, fetchAddressPortfolio, fetchAudricHistory, fetchAudricPortfolio, fetchAvailableRewards, fetchBalance, fetchHealthFactor, fetchPositions, fetchProtocolStats, fetchRates, fetchSavings, fetchTokenPrices, fetchWalletCoins, findTool, getAudricApiBase, getDefaultTools, getDefiCacheStore, getFetchLock, getMcpManager, getModifiableFields, getNaviCacheStore, getTelemetrySink, getToolFlags, getWalletAddress, getWalletCacheStore, guardArtifactPreview, guardStaleData, harnessShapeForEffort, hasNaviMcp, healthCheckTool, isBundleableTool, loadRecipes, looksLikeSuiNs, microcompact, mppServicesTool, naviKey, normalizeAddressInput, parseEvalSummary, parseMcpJson, parseRecipe, parseSSE, payApiTool, portfolioAnalysisTool, protocolDeepDiveTool, ratesInfoTool, regenerateBundle, registerEngineTools, renderCanvasTool, repayDebtTool, requireAgent, resetDefiCacheStore, resetFetchLock, resetNaviCacheStore, resetTelemetrySink, resetWalletCacheStore, resolveAddressToSuinsViaRpc, resolvePermissionTier, resolveSuinsTool, resolveSuinsViaRpc, resolveUsdValue, runGuards, runTools, saveContactTool, saveDepositTool, savingsInfoTool, sendTransferTool, serializeSSE, setDefiCacheStore, setFetchLock, setNaviCacheStore, setTelemetrySink, setWalletCacheStore, spendingAnalyticsTool, swapExecuteTool, swapQuoteTool, tokenPricesTool, toolNameToOperation, toolsToDefinitions, transactionHistoryTool, transformBalance, transformHealthFactor, transformPositions, transformRates, transformRewards, transformSavings, updateGuardStateAfterToolResult, updateTodoTool, validateHistory, voloStakeTool, voloStatsTool, voloUnstakeTool, webSearchTool, withdrawTool, yieldSummaryTool };
9995
+ export { AnthropicProvider, BalanceTracker, CANVAS_TEMPLATES, ContextBudget, CostTracker, DEFAULT_GUARD_CONFIG, DEFAULT_LEASE_SEC, DEFAULT_PERMISSION_CONFIG, DEFAULT_POLL_BUDGET_MS, DEFAULT_POLL_INTERVAL_MS, DEFAULT_SYSTEM_PROMPT, DEFAULT_TOOL_TTL_MS, EFFORT_THINKING_BUDGET_CAPS, EarlyToolDispatcher, InMemoryDefiCacheStore, InMemoryFetchLock, InMemoryNaviCacheStore, InMemoryWalletCacheStore, InvalidAddressError, MAX_BUNDLE_OPS, McpClientManager, McpResponseCache, MemorySessionStore, NAVI_ADDR_TTL_SEC, NAVI_MCP_CONFIG, NAVI_MCP_URL, NAVI_RATES_TTL_SEC, NAVI_SERVER_NAME, NaviTools, PERMISSION_PRESETS, QueryEngine, READ_TOOLS, REGENERATABLE_READ_TOOLS, RecipeRegistry, RetryTracker, SUINS_NAME_REGEX, SUI_ADDRESS_REGEX, SUI_ADDRESS_STRICT_REGEX, SuinsNotRegisteredError, SuinsRpcError, TOOL_FLAGS, TOOL_MODIFIABLE_FIELDS, TOOL_TTL_MS, TxMutex, VALID_PAIRS, WRITE_TOOLS, _resetNaviCircuitBreaker, activitySummaryTool, adaptAllMcpTools, adaptAllServerTools, adaptMcpTool, addRecipientTool, applyToolFlags, awaitOrFetch, balanceCheckTool, borrowTool, budgetToolResult, buildCachedSystemPrompt, buildMcpTools, buildProactivenessInstructions, buildProfileContext, buildSelfEvaluationInstruction, buildStateContext, buildTool, bundleShortestTtl, checkValidPair, claimRewardsTool, clampThinkingForEffort, classifyEffort, clearPortfolioCache, clearPortfolioCacheFor, clearPriceMapCache, compactMessages, composeBundleFromToolResults, computeRegenerateFields, createGuardRunnerState, engineToSSE, estimateTokens, explainTxTool, extractAllProactiveMarkers, extractConversationText, extractMcpText, fetchAddressDefiPortfolio, fetchAddressPortfolio, fetchAudricHistory, fetchAudricPortfolio, fetchAvailableRewards, fetchBalance, fetchHealthFactor, fetchPositions, fetchProtocolStats, fetchRates, fetchSavings, fetchTokenPrices, fetchWalletCoins, findTool, getAudricApiBase, getDefaultTools, getDefiCacheStore, getFetchLock, getMcpManager, getModifiableFields, getNaviCacheStore, getTelemetrySink, getToolFlags, getWalletAddress, getWalletCacheStore, guardArtifactPreview, guardStaleData, harnessShapeForEffort, hasNaviMcp, healthCheckTool, isBundleableTool, loadRecipes, looksLikeSuiNs, microcompact, mppServicesTool, naviKey, normalizeAddressInput, parseEvalSummary, parseMcpJson, parseProactiveMarker, parseRecipe, parseSSE, payApiTool, portfolioAnalysisTool, protocolDeepDiveTool, ratesInfoTool, regenerateBundle, registerEngineTools, renderCanvasTool, repayDebtTool, requireAgent, resetDefiCacheStore, resetFetchLock, resetNaviCacheStore, resetTelemetrySink, resetWalletCacheStore, resolveAddressToSuinsViaRpc, resolvePermissionTier, resolveSuinsTool, resolveSuinsViaRpc, resolveUsdValue, runGuards, runTools, saveContactTool, saveDepositTool, savingsInfoTool, sendTransferTool, serializeSSE, setDefiCacheStore, setFetchLock, setNaviCacheStore, setTelemetrySink, setWalletCacheStore, spendingAnalyticsTool, stripProactiveMarkers, swapExecuteTool, swapQuoteTool, tokenPricesTool, toolNameToOperation, toolsToDefinitions, transactionHistoryTool, transformBalance, transformHealthFactor, transformPositions, transformRates, transformRewards, transformSavings, updateGuardStateAfterToolResult, updateTodoTool, validateHistory, voloStakeTool, voloStatsTool, voloUnstakeTool, webSearchTool, withdrawTool, yieldSummaryTool };
9535
9996
  //# sourceMappingURL=index.js.map
9536
9997
  //# sourceMappingURL=index.js.map