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
|
|
51
|
-
*
|
|
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
|
|
51
|
-
*
|
|
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.
|
|
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",
|