@t2000/engine 1.18.0 → 1.19.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,70 @@ 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 an unsolicited 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 wrap your ENTIRE response in a \`<proactive type="..." subjectKey="...">BODY</proactive>\` block.
5572
+ - The host renders proactive blocks with a distinct "\u2726 ADDED BY AUDRIC" lockup so the user knows this is your suggestion, not an answer to a question.
5573
+ - 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).
5574
+ - \`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.
5575
+ - Cap: at most ONE proactive block per turn. Do NOT mix proactive insights with answers to user questions \u2014 if the user asked a question, answer it; emit a proactive block only when there's no user question OR when the question is unrelated to the insight.
5576
+ - 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.`;
5577
+
5578
+ // src/proactive-marker.ts
5579
+ var VALID_TYPES = /* @__PURE__ */ new Set([
5580
+ "idle_balance",
5581
+ "hf_warning",
5582
+ "apy_drift",
5583
+ "goal_progress"
5584
+ ]);
5585
+ var MARKER_REGEX = /<proactive\s+([^>]+)>([\s\S]*?)<\/proactive>/g;
5586
+ var ATTR_TYPE_REGEX = /\btype\s*=\s*"([^"]+)"/;
5587
+ var ATTR_SUBJECT_KEY_REGEX = /\bsubjectKey\s*=\s*"([^"]+)"/;
5588
+ function parseProactiveMarker(text) {
5589
+ if (!text.includes("<proactive")) return null;
5590
+ const matches = [];
5591
+ for (const match of text.matchAll(MARKER_REGEX)) {
5592
+ matches.push({ attrs: match[1] ?? "", body: match[2] ?? "" });
5593
+ }
5594
+ if (matches.length === 0) return null;
5595
+ for (const { attrs, body } of matches) {
5596
+ const typeMatch = attrs.match(ATTR_TYPE_REGEX);
5597
+ const subjectKeyMatch = attrs.match(ATTR_SUBJECT_KEY_REGEX);
5598
+ if (!typeMatch || !subjectKeyMatch) continue;
5599
+ const proactiveType = typeMatch[1];
5600
+ const subjectKey = subjectKeyMatch[1].trim();
5601
+ if (!VALID_TYPES.has(proactiveType)) continue;
5602
+ if (subjectKey.length === 0) continue;
5603
+ const trimmedBody = body.trim();
5604
+ if (trimmedBody.length === 0) continue;
5605
+ return {
5606
+ proactiveType,
5607
+ subjectKey,
5608
+ body: trimmedBody,
5609
+ markerCount: matches.length
5610
+ };
5611
+ }
5612
+ return null;
5613
+ }
5614
+ function stripProactiveMarkers(text) {
5615
+ if (!text.includes("<proactive")) return text;
5616
+ return text.replace(MARKER_REGEX, (_match, _attrs, body) => body);
5617
+ }
5618
+ function extractAllProactiveMarkers(text) {
5619
+ if (!text.includes("<proactive")) return [];
5620
+ const out = [];
5621
+ for (const match of text.matchAll(MARKER_REGEX)) {
5622
+ const attrs = match[1] ?? "";
5623
+ const typeMatch = attrs.match(ATTR_TYPE_REGEX);
5624
+ const subjectKeyMatch = attrs.match(ATTR_SUBJECT_KEY_REGEX);
5625
+ if (!typeMatch || !subjectKeyMatch) continue;
5626
+ const subjectKey = subjectKeyMatch[1].trim();
5627
+ if (subjectKey.length === 0) continue;
5628
+ out.push({ proactiveType: typeMatch[1], subjectKey });
5629
+ }
5630
+ return out;
5631
+ }
5471
5632
 
5472
5633
  // src/cost.ts
5473
5634
  var DEFAULT_INPUT_COST = 3 / 1e6;
@@ -6024,6 +6185,29 @@ function runGuards(tool, call, state, config, conversationContext, onGuardFired,
6024
6185
  if (config.inputValidation !== false && tool.preflight) {
6025
6186
  const check = tool.preflight(call.input);
6026
6187
  if (!check.valid) {
6188
+ if ("needsInput" in check && check.needsInput) {
6189
+ const event2 = {
6190
+ timestamp: now,
6191
+ toolName: tool.name,
6192
+ toolUseId: call.id,
6193
+ gate: "input_validation",
6194
+ verdict: "pass",
6195
+ tier: "safety",
6196
+ message: check.needsInput.description
6197
+ };
6198
+ fire("pass", "safety", "input_validation", false);
6199
+ return {
6200
+ blocked: false,
6201
+ injections: [],
6202
+ events: [event2],
6203
+ needsInput: check.needsInput
6204
+ };
6205
+ }
6206
+ if (!("error" in check)) {
6207
+ throw new Error(
6208
+ `Preflight returned a non-needsInput, non-error invalid result for tool ${tool.name}`
6209
+ );
6210
+ }
6027
6211
  const event = {
6028
6212
  timestamp: now,
6029
6213
  toolName: tool.name,
@@ -6945,6 +7129,26 @@ var QueryEngine = class {
6945
7129
  // their `finally` block so they DON'T clear the cache mid-turn — the
6946
7130
  // pending write may resume, and the cache should survive the pause.
6947
7131
  turnPaused = false;
7132
+ // [SPEC 9 v0.1.1 P9.2 / R3] Per-engine-instance cooldown set for
7133
+ // proactive insight markers. Same `(proactiveType, subjectKey)` tuple
7134
+ // emitted twice in one session ⇒ second emission is suppressed (host
7135
+ // strips the wrapper, renders body as plain text). Cooldown is
7136
+ // per-session, not cross-session — a fresh QueryEngine on the next
7137
+ // user session starts with an empty set and may re-emit the same
7138
+ // tuple. Drops automatically when the engine is GC'd.
7139
+ //
7140
+ // Storage shape: serialised key `${type}:${subjectKey}` for cheap
7141
+ // Set membership without an extra hash function.
7142
+ proactiveCooldown = /* @__PURE__ */ new Set();
7143
+ // [SPEC 9 v0.1.3 P9.4] In-flight `pending_input` state, keyed by `inputId`.
7144
+ // Set when the agent loop hits a tool whose preflight returned `needsInput`;
7145
+ // consulted by `resumeWithInput()` to look up which paused tool call to
7146
+ // re-feed with the user's submitted values. Cleared on resume (whether
7147
+ // success or abandonment via a fresh `submitMessage`). Multiple concurrent
7148
+ // entries are allowed in principle (LLM could call two `add_recipient`s
7149
+ // in one turn) but in practice we yield + return on the FIRST hit, so
7150
+ // the map is almost always 0 or 1 entries deep.
7151
+ pendingInputs = /* @__PURE__ */ new Map();
6948
7152
  constructor(config) {
6949
7153
  this.provider = config.provider;
6950
7154
  this.agent = config.agent;
@@ -7168,6 +7372,159 @@ var QueryEngine = class {
7168
7372
  }
7169
7373
  }
7170
7374
  }
7375
+ /**
7376
+ * [SPEC 9 v0.1.3 P9.4] Resume a turn that paused on `pending_input`.
7377
+ *
7378
+ * Called by the host after the user submitted the inline form. The
7379
+ * `pendingInput` argument is the EXACT payload that was yielded as
7380
+ * `pending_input` (host stored it in session storage); `values` is the
7381
+ * `{ fieldName: value }` map the user submitted, post-host-validation
7382
+ * against `pendingInput.schema`.
7383
+ *
7384
+ * Engine responsibilities (in order):
7385
+ * 1. Push the captured `assistantContent` (assistant blocks from the
7386
+ * paused turn — includes the tool_use that triggered this pause).
7387
+ * 2. Run the paused tool's `call()` with `values` as input. Re-runs
7388
+ * preflight first as a defense-in-depth gate (host SHOULD have
7389
+ * validated against the schema, but malformed input would otherwise
7390
+ * poison the conversation history with an orphan tool_use).
7391
+ * 3. Combine `completedResults` (read tool_results that already ran in
7392
+ * the same turn before the pause) with the new tool_result into ONE
7393
+ * user-role message. Anthropic's API requires every tool_use to have
7394
+ * a tool_result in the immediately-following user message — splitting
7395
+ * them would produce two consecutive user messages, which the API
7396
+ * rejects.
7397
+ * 4. Yield `tool_result` for the host's timeline + run `agentLoop` to
7398
+ * continue the turn (LLM gets the resumed result and narrates).
7399
+ *
7400
+ * Mirrors `resumeWithToolResult` for the user-confirm case but feeds
7401
+ * the values back as the tool's INPUT (the call() runs here for the
7402
+ * first time) instead of as the tool's OUTPUT (the call() ran on the
7403
+ * client side via sponsored-tx for pending_action).
7404
+ */
7405
+ async *resumeWithInput(pendingInput, values) {
7406
+ this.abortController = new AbortController();
7407
+ const signal = this.abortController.signal;
7408
+ if (pendingInput.assistantContent.length > 0) {
7409
+ this.messages.push({
7410
+ role: "assistant",
7411
+ content: pendingInput.assistantContent
7412
+ });
7413
+ }
7414
+ const tool = findTool(this.tools, pendingInput.toolName);
7415
+ let resumedToolResult;
7416
+ let toolResultEventPayload;
7417
+ if (!tool) {
7418
+ const errorPayload = {
7419
+ error: `Tool "${pendingInput.toolName}" not found on resume \u2014 engine may have been reconfigured between pause and resume.`,
7420
+ _hostBugMissingTool: true
7421
+ };
7422
+ resumedToolResult = {
7423
+ content: JSON.stringify(errorPayload),
7424
+ isError: true
7425
+ };
7426
+ toolResultEventPayload = { result: errorPayload, isError: true };
7427
+ } else {
7428
+ const preflightCheck = tool.preflight ? tool.preflight(values) : { valid: true };
7429
+ if (!preflightCheck.valid && "error" in preflightCheck) {
7430
+ const errorPayload = {
7431
+ error: `Form values failed re-validation: ${preflightCheck.error}`,
7432
+ _hostFormValidationFailed: true
7433
+ };
7434
+ resumedToolResult = {
7435
+ content: JSON.stringify(errorPayload),
7436
+ isError: true
7437
+ };
7438
+ toolResultEventPayload = { result: errorPayload, isError: true };
7439
+ } else if (!preflightCheck.valid && "needsInput" in preflightCheck) {
7440
+ const errorPayload = {
7441
+ 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.",
7442
+ _multiStepFormUnsupported: true
7443
+ };
7444
+ resumedToolResult = {
7445
+ content: JSON.stringify(errorPayload),
7446
+ isError: true
7447
+ };
7448
+ toolResultEventPayload = { result: errorPayload, isError: true };
7449
+ } else {
7450
+ const parsed = tool.inputSchema.safeParse(values);
7451
+ if (!parsed.success) {
7452
+ const errorPayload = {
7453
+ error: `Invalid input on resume: ${parsed.error.issues.map((i) => i.message).join(", ")}`,
7454
+ _hostFormZodFailed: true
7455
+ };
7456
+ resumedToolResult = {
7457
+ content: JSON.stringify(errorPayload),
7458
+ isError: true
7459
+ };
7460
+ toolResultEventPayload = { result: errorPayload, isError: true };
7461
+ } else {
7462
+ const context = {
7463
+ agent: this.agent,
7464
+ mcpManager: this.mcpManager,
7465
+ walletAddress: this.walletAddress,
7466
+ suiRpcUrl: this.suiRpcUrl,
7467
+ serverPositions: this.serverPositions,
7468
+ positionFetcher: this.positionFetcher,
7469
+ env: this.env,
7470
+ signal,
7471
+ priceCache: this.priceCache,
7472
+ permissionConfig: this.permissionConfig,
7473
+ sessionSpendUsd: this.sessionSpendUsd,
7474
+ blockvisionApiKey: this.blockvisionApiKey,
7475
+ portfolioCache: this.portfolioCache
7476
+ };
7477
+ try {
7478
+ const callResult = await tool.call(parsed.data, context);
7479
+ resumedToolResult = {
7480
+ content: JSON.stringify(callResult.data),
7481
+ isError: false
7482
+ };
7483
+ toolResultEventPayload = { result: callResult.data, isError: false };
7484
+ } catch (err) {
7485
+ const message = err instanceof Error ? err.message : "Tool execution failed";
7486
+ const errorPayload = { error: message };
7487
+ resumedToolResult = {
7488
+ content: JSON.stringify(errorPayload),
7489
+ isError: true
7490
+ };
7491
+ toolResultEventPayload = { result: errorPayload, isError: true };
7492
+ }
7493
+ }
7494
+ }
7495
+ }
7496
+ const userMessageBlocks = [
7497
+ ...pendingInput.completedResults.map((r) => ({
7498
+ type: "tool_result",
7499
+ toolUseId: r.toolUseId,
7500
+ content: r.content,
7501
+ isError: r.isError
7502
+ })),
7503
+ {
7504
+ type: "tool_result",
7505
+ toolUseId: pendingInput.toolUseId,
7506
+ content: resumedToolResult.content,
7507
+ isError: resumedToolResult.isError
7508
+ }
7509
+ ];
7510
+ this.messages.push({ role: "user", content: userMessageBlocks });
7511
+ yield {
7512
+ type: "tool_result",
7513
+ toolName: pendingInput.toolName,
7514
+ toolUseId: pendingInput.toolUseId,
7515
+ result: toolResultEventPayload.result,
7516
+ isError: toolResultEventPayload.isError
7517
+ };
7518
+ this.pendingInputs.delete(pendingInput.inputId);
7519
+ this.turnPaused = false;
7520
+ try {
7521
+ yield* this.agentLoop(null, signal, false);
7522
+ } finally {
7523
+ if (!this.turnPaused) {
7524
+ this.turnReadCache.clear();
7525
+ }
7526
+ }
7527
+ }
7171
7528
  /**
7172
7529
  * [v1.5] Auto-run configured read tools after a successful write,
7173
7530
  * push their results into the conversation, and yield `tool_result`
@@ -7338,6 +7695,25 @@ var QueryEngine = class {
7338
7695
  }
7339
7696
  loadMessages(messages) {
7340
7697
  this.messages = [...messages];
7698
+ this.rehydrateProactiveCooldown();
7699
+ }
7700
+ /**
7701
+ * [SPEC 9 v0.1.1 P9.2 / R3] Walk loaded assistant text blocks and seed the
7702
+ * cooldown set with every parseable `(proactiveType, subjectKey)` tuple.
7703
+ * Called automatically from `loadMessages`. Idempotent — safe to call on
7704
+ * an already-populated set; duplicates are absorbed by Set semantics.
7705
+ */
7706
+ rehydrateProactiveCooldown() {
7707
+ for (const message of this.messages) {
7708
+ if (message.role !== "assistant") continue;
7709
+ const blocks = Array.isArray(message.content) ? message.content : [];
7710
+ for (const block of blocks) {
7711
+ if (block.type !== "text" || typeof block.text !== "string") continue;
7712
+ for (const { proactiveType, subjectKey } of extractAllProactiveMarkers(block.text)) {
7713
+ this.proactiveCooldown.add(`${proactiveType}:${subjectKey}`);
7714
+ }
7715
+ }
7716
+ }
7341
7717
  }
7342
7718
  /**
7343
7719
  * [v0.46.7] Run a read-only tool out-of-band, using the engine's tool
@@ -7745,6 +8121,45 @@ ${recipeCtx}`;
7745
8121
  });
7746
8122
  continue;
7747
8123
  }
8124
+ if (check.needsInput) {
8125
+ const inputId = randomUUID();
8126
+ const pendingInput = {
8127
+ inputId,
8128
+ toolName: call.name,
8129
+ toolUseId: call.id,
8130
+ schema: check.needsInput.schema,
8131
+ description: check.needsInput.description,
8132
+ assistantContent: acc.assistantBlocks,
8133
+ completedResults: toolResultBlocks.map((b) => ({
8134
+ toolUseId: b.toolUseId,
8135
+ content: b.content,
8136
+ isError: b.isError ?? false
8137
+ }))
8138
+ };
8139
+ this.pendingInputs.set(inputId, pendingInput);
8140
+ this.turnPaused = true;
8141
+ getTelemetrySink().counter("engine.pending_input_emitted", {
8142
+ tool: call.name
8143
+ });
8144
+ yield {
8145
+ type: "pending_input",
8146
+ inputId,
8147
+ toolName: call.name,
8148
+ toolUseId: call.id,
8149
+ schema: check.needsInput.schema,
8150
+ description: check.needsInput.description,
8151
+ // [SPEC 9 v0.1.3 P9.4] Round-trip fields on the wire so
8152
+ // stateless hosts (audric — request-scoped engines) can
8153
+ // persist + echo back on resume. In-process hosts (CLI,
8154
+ // long-lived engine instances) can ignore — the engine
8155
+ // also stashes the state on `this.pendingInputs[inputId]`
8156
+ // for in-memory recall.
8157
+ assistantContent: pendingInput.assistantContent,
8158
+ completedResults: pendingInput.completedResults
8159
+ };
8160
+ recordTurnOutcome("pending_input");
8161
+ return;
8162
+ }
7748
8163
  if (check.injections.length > 0) {
7749
8164
  call._guardInjections = check.injections;
7750
8165
  }
@@ -8081,6 +8496,32 @@ ${recipeCtx}`;
8081
8496
  yield { type: "text_delta", text: event.text };
8082
8497
  break;
8083
8498
  }
8499
+ case "text_done": {
8500
+ if (event.proactiveMarker) {
8501
+ const { proactiveType, subjectKey, body, markerCount } = event.proactiveMarker;
8502
+ const dedupKey = `${proactiveType}:${subjectKey}`;
8503
+ const suppressed = this.proactiveCooldown.has(dedupKey);
8504
+ if (!suppressed) this.proactiveCooldown.add(dedupKey);
8505
+ const sink = getTelemetrySink();
8506
+ if (suppressed) {
8507
+ sink.counter("audric.harness.proactive_text_suppressed_count", { reason: "cooldown" }, 1);
8508
+ } else {
8509
+ sink.counter("audric.harness.proactive_text_emitted_count", { type: proactiveType }, 1);
8510
+ }
8511
+ if (markerCount > 1) {
8512
+ sink.counter("audric.harness.proactive_marker_violations_count", {}, 1);
8513
+ }
8514
+ yield {
8515
+ type: "proactive_text",
8516
+ proactiveType,
8517
+ subjectKey,
8518
+ body,
8519
+ suppressed,
8520
+ markerCount
8521
+ };
8522
+ }
8523
+ break;
8524
+ }
8084
8525
  case "tool_use_done": {
8085
8526
  if (acc.text) {
8086
8527
  acc.assistantBlocks.push({ type: "text", text: acc.text });
@@ -8680,7 +9121,7 @@ function classifyEffort(model, userMessage, matchedRecipe, sessionWriteCount) {
8680
9121
  }
8681
9122
 
8682
9123
  // src/eval-summary.ts
8683
- var MARKER_REGEX = /<eval_summary>([\s\S]*?)<\/eval_summary>/g;
9124
+ var MARKER_REGEX2 = /<eval_summary>([\s\S]*?)<\/eval_summary>/g;
8684
9125
  var VALID_STATUSES = /* @__PURE__ */ new Set([
8685
9126
  "good",
8686
9127
  "warning",
@@ -8690,7 +9131,7 @@ var VALID_STATUSES = /* @__PURE__ */ new Set([
8690
9131
  function parseEvalSummary(thinkingText) {
8691
9132
  if (!thinkingText.includes("<eval_summary>")) return null;
8692
9133
  const matches = [];
8693
- for (const match of thinkingText.matchAll(MARKER_REGEX)) {
9134
+ for (const match of thinkingText.matchAll(MARKER_REGEX2)) {
8694
9135
  matches.push(match[1] ?? "");
8695
9136
  }
8696
9137
  if (matches.length === 0) return null;
@@ -9223,6 +9664,7 @@ var AnthropicProvider = class {
9223
9664
  const stream = params.signal ? this.client.messages.stream(streamParams, { signal: params.signal }) : this.client.messages.stream(streamParams);
9224
9665
  const toolInputBuffers = /* @__PURE__ */ new Map();
9225
9666
  const thinkingBuffers = /* @__PURE__ */ new Map();
9667
+ const textBuffers = /* @__PURE__ */ new Map();
9226
9668
  let outputTokensFromStart = 0;
9227
9669
  try {
9228
9670
  for await (const event of stream) {
@@ -9264,12 +9706,16 @@ var AnthropicProvider = class {
9264
9706
  thinkingBuffers.set(event.index, { type: "thinking", text: "", signature: "" });
9265
9707
  } else if (block.type === "redacted_thinking") {
9266
9708
  thinkingBuffers.set(event.index, { type: "redacted_thinking", data: block.data ?? "" });
9709
+ } else if (block.type === "text") {
9710
+ textBuffers.set(event.index, { text: "" });
9267
9711
  }
9268
9712
  break;
9269
9713
  }
9270
9714
  case "content_block_delta": {
9271
9715
  const delta = event.delta;
9272
9716
  if (delta.type === "text_delta") {
9717
+ const buf = textBuffers.get(event.index);
9718
+ if (buf) buf.text += delta.text ?? "";
9273
9719
  yield { type: "text_delta", text: delta.text };
9274
9720
  } else if (delta.type === "input_json_delta") {
9275
9721
  const buf = toolInputBuffers.get(event.index);
@@ -9323,6 +9769,15 @@ var AnthropicProvider = class {
9323
9769
  yield { type: "redacted_thinking", data: thinkBuf.data };
9324
9770
  thinkingBuffers.delete(event.index);
9325
9771
  }
9772
+ const textBuf = textBuffers.get(event.index);
9773
+ if (textBuf) {
9774
+ const proactiveMarker = parseProactiveMarker(textBuf.text);
9775
+ yield {
9776
+ type: "text_done",
9777
+ ...proactiveMarker ? { proactiveMarker } : {}
9778
+ };
9779
+ textBuffers.delete(event.index);
9780
+ }
9326
9781
  break;
9327
9782
  }
9328
9783
  case "message_delta": {
@@ -9531,6 +9986,6 @@ function sanitizeAnthropicMessages(messages) {
9531
9986
  return merged;
9532
9987
  }
9533
9988
 
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 };
9989
+ 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
9990
  //# sourceMappingURL=index.js.map
9536
9991
  //# sourceMappingURL=index.js.map