melchizedek-agents 1.15.0 → 1.15.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ Consumers of the package read this file; it records changes to the
4
4
  **published API surface** (the exports map in `package.json`, the bins,
5
5
  the starter pack and the templates), not the repo's full history.
6
6
 
7
+ ## 1.15.1 — 2026-11-06
8
+
9
+ **Fixed: a conversation stuck on Anthropic after an approval** (ADR 0155).
10
+ When one model step made several tool calls and one of them required
11
+ approval, the calls beside it ran but their results were never stored. After
12
+ the approval the history held the step's calls with only the approved one
13
+ answered, and Anthropic refused every later request with `tool_use ids were
14
+ found without tool_result blocks immediately after`. The loop now stores the
15
+ other calls' results before the approval request. The request builder also
16
+ gives any call the history leaves unanswered an error result: an approval
17
+ the person moved on from by typing a new message, a run that ended between a call and its
18
+ result, and sessions stored before this release, which recover without
19
+ editing. Gemini, which accepted the unanswered calls, sees the same history
20
+ plus those results.
21
+
7
22
  ## 1.15.0 — 2026-11-02
8
23
 
9
24
  **Breaking — read before upgrading**
@@ -47,8 +47,11 @@
47
47
  * mergeParallelFunctionResponseEvents). ADK runs the calls one after
48
48
  * another and lets a call read the state an earlier call of the same
49
49
  * step wrote; here each call reads the state as the step left it.
50
- * 3. An approval request ends the run: the response is NOT stored; in its
51
- * place the event ADK stores, a call to `adk_request_confirmation`
50
+ * 3. An approval request ends the run: the gated calls' answers are NOT
51
+ * stored. The step's other calls ran, and their answers are stored
52
+ * first, as their own event with no actions (answeredBesideGates): a
53
+ * provider other than Gemini refuses a call with no result. Then, in
54
+ * the gated calls' place, the event ADK stores, a call to `adk_request_confirmation`
52
55
  * (args: the original call and the confirmation; an `adk-` id listed in
53
56
  * longRunningToolIds), carrying the response's actions. The answer
54
57
  * resumes it: before each step, lib/runtime/native/interrupts.ts runs
@@ -47,8 +47,11 @@
47
47
  * mergeParallelFunctionResponseEvents). ADK runs the calls one after
48
48
  * another and lets a call read the state an earlier call of the same
49
49
  * step wrote; here each call reads the state as the step left it.
50
- * 3. An approval request ends the run: the response is NOT stored; in its
51
- * place the event ADK stores, a call to `adk_request_confirmation`
50
+ * 3. An approval request ends the run: the gated calls' answers are NOT
51
+ * stored. The step's other calls ran, and their answers are stored
52
+ * first, as their own event with no actions (answeredBesideGates): a
53
+ * provider other than Gemini refuses a call with no result. Then, in
54
+ * the gated calls' place, the event ADK stores, a call to `adk_request_confirmation`
52
55
  * (args: the original call and the confirmation; an `adk-` id listed in
53
56
  * longRunningToolIds), carrying the response's actions. The answer
54
57
  * resumes it: before each step, lib/runtime/native/interrupts.ts runs
@@ -515,6 +518,25 @@ function confirmationEvent(scope, modelEvent, response) {
515
518
  longRunningToolIds: parts.map((p) => p.functionCall?.id),
516
519
  });
517
520
  }
521
+ /**
522
+ * The answers of a step that paused on an approval, beside its gated calls:
523
+ * the response's parts without the gated calls' stand-ins, and without the
524
+ * actions, which the confirmation event carries. Undefined when every call
525
+ * waits on the approval. Every provider but Gemini refuses a call with no
526
+ * result, so a call that already ran keeps its result through the pause.
527
+ */
528
+ function answeredBesideGates(scope, response) {
529
+ const gated = new Set(Object.keys(response.actions.requestedToolConfirmations ?? {}));
530
+ const parts = (response.content?.parts ?? []).filter((p) => !(p.functionResponse?.id && gated.has(p.functionResponse.id)));
531
+ if (!parts.some((p) => p.functionResponse))
532
+ return undefined;
533
+ return createTurnEvent({
534
+ invocationId: scope.ctx.invocationId,
535
+ author: scope.agent.name,
536
+ branch: scope.ctx.branch,
537
+ content: { parts, role: response.content?.role ?? 'user' },
538
+ });
539
+ }
518
540
  /**
519
541
  * ADK's request-confirmation processor, before a step: the pinned calls the
520
542
  * latest user message approves or refuses run with the answer in their
@@ -801,6 +823,9 @@ async function* agentLoop(agent, ctx) {
801
823
  const authIds = auth ? [...(lastEvent.longRunningToolIds ?? [])] : [];
802
824
  const confirmation = confirmationEvent(scope, modelEvent, response);
803
825
  if (confirmation) {
826
+ const answered = answeredBesideGates(scope, response);
827
+ if (answered)
828
+ yield await store(answered);
804
829
  lastEvent = await store(confirmation);
805
830
  yield lastEvent;
806
831
  return { reason: 'paused', steps, lastEvent, pending: [...(lastEvent.longRunningToolIds ?? []), ...delegated] };
@@ -69,7 +69,7 @@
69
69
  * yielded by a toolset or passed in `extraTools`, is refused, naming 1.0.0
70
70
  * and defineTool, as registerTool refuses one (ADR 0107).
71
71
  */
72
- import type { ModelRequest } from '../../models/contract.ts';
72
+ import type { Message, ModelRequest } from '../../models/contract.ts';
73
73
  import type { MemoryService } from '../memoryService.ts';
74
74
  import type { TurnContent, TurnEvent } from '../events.ts';
75
75
  import type { Session } from '../sessions.ts';
@@ -230,4 +230,17 @@ export declare function isToolset(value: unknown): value is {
230
230
  * request: a required state key that is absent, a
231
231
  * Gemini-only tool on another model, a config field LlmAgent refuses.
232
232
  */
233
+ /** What a call with no stored result reads as: it never answered. */
234
+ export declare const UNANSWERED_CALL = "This call has no result: it never ran, or the run ended before it answered.";
235
+ /**
236
+ * Every assistant message's tool calls answered in the message right after
237
+ * it, as the contract's ToolMessage holds them (one result per call).
238
+ * A call the history leaves unanswered (an approval the person moved on
239
+ * from, a run that ended between a call and its result) gets an error
240
+ * result saying it never answered. Anthropic and OpenAI refuse a request
241
+ * that leaves a call unanswered, and every later request replays it, so
242
+ * without this one such call ends the conversation. Results already there
243
+ * stay as they are, in their order; the stand-ins follow them.
244
+ */
245
+ export declare function answerEveryCall(messages: Message[]): Message[];
233
246
  export declare function buildModelRequest(agent: NativeAgent, ctx: RequestContext): Promise<BuiltRequest>;
@@ -448,6 +448,45 @@ function placementOf(native, model, configTools) {
448
448
  * request: a required state key that is absent, a
449
449
  * Gemini-only tool on another model, a config field LlmAgent refuses.
450
450
  */
451
+ /** What a call with no stored result reads as: it never answered. */
452
+ export const UNANSWERED_CALL = 'This call has no result: it never ran, or the run ended before it answered.';
453
+ /**
454
+ * Every assistant message's tool calls answered in the message right after
455
+ * it, as the contract's ToolMessage holds them (one result per call).
456
+ * A call the history leaves unanswered (an approval the person moved on
457
+ * from, a run that ended between a call and its result) gets an error
458
+ * result saying it never answered. Anthropic and OpenAI refuse a request
459
+ * that leaves a call unanswered, and every later request replays it, so
460
+ * without this one such call ends the conversation. Results already there
461
+ * stay as they are, in their order; the stand-ins follow them.
462
+ */
463
+ export function answerEveryCall(messages) {
464
+ const out = [];
465
+ for (let i = 0; i < messages.length; i++) {
466
+ const message = messages[i];
467
+ out.push(message);
468
+ if (message.role !== 'assistant')
469
+ continue;
470
+ const calls = message.parts.filter((p) => p.type === 'toolCall');
471
+ if (calls.length === 0)
472
+ continue;
473
+ const next = messages[i + 1];
474
+ const results = next?.role === 'tool' ? next.parts : [];
475
+ const answered = new Set(results.map((r) => r.id));
476
+ const missing = calls
477
+ .filter((c) => !answered.has(c.id))
478
+ .map((c) => ({ type: 'toolResult', id: c.id, name: c.name, result: UNANSWERED_CALL, isError: true }));
479
+ if (missing.length === 0)
480
+ continue;
481
+ if (next?.role === 'tool') {
482
+ out.push({ role: 'tool', parts: [...next.parts, ...missing] });
483
+ i += 1;
484
+ }
485
+ else
486
+ out.push({ role: 'tool', parts: missing });
487
+ }
488
+ return out;
489
+ }
451
490
  export async function buildModelRequest(agent, ctx) {
452
491
  const model = agent.model;
453
492
  const cfg = { ...(agent.generateContentConfig ?? {}) };
@@ -621,7 +660,7 @@ export async function buildModelRequest(agent, ctx) {
621
660
  const reasoning = reasoningOf(cfg);
622
661
  const sampling = samplingOf(cfg);
623
662
  const systemPrompt = systemText(system);
624
- const messages = contentsToMessages(contents).messages;
663
+ const messages = answerEveryCall(contentsToMessages(contents).messages);
625
664
  const request = {
626
665
  model,
627
666
  ...(systemPrompt !== undefined ? { system: systemPrompt } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "melchizedek-agents",
3
- "version": "1.15.0",
3
+ "version": "1.15.1",
4
4
  "description": "A multi-model, multi-agent orchestration framework — YAML-defined syndicates on the engine's own agent loop, Supabase or Postgres session persistence, pgvector long-term memory. Companion repo to the lyceumagents.com curriculum.",
5
5
  "type": "module",
6
6
  "license": "MIT",