melchizedek-agents 1.15.0 → 1.15.2
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 +29 -0
- package/db/migrations/0024_image_model_pricing.sql +28 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +5 -2
- package/dist/lib/runtime/native/agentLoop.js +27 -2
- package/dist/lib/runtime/native/request.d.ts +14 -1
- package/dist/lib/runtime/native/request.js +40 -1
- package/dist/lib/tools/generateImageTool.d.ts +6 -0
- package/dist/lib/tools/generateImageTool.js +29 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ 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.2 — 2026-11-06
|
|
8
|
+
|
|
9
|
+
**Fixed: a turn's usage and cost left out `generate_image`.** The tool called
|
|
10
|
+
Gemini's image model outside the tracer, so its tokens never reached the
|
|
11
|
+
turn's usage or the ledger's `cost_usd`. The call is now an `llm.request`
|
|
12
|
+
through `traceLlmGeneration`, like every other model call: its tokens count in
|
|
13
|
+
the turn's usage, it counts against `max_steps`, and a cancelled turn stops it
|
|
14
|
+
before the request. Migration 0024 adds the price row for
|
|
15
|
+
`gemini-3.1-flash-image-preview` ($0.50 in, $60.00 out at the image rate,
|
|
16
|
+
$3.00 thinking, from Google's page for `gemini-3.1-flash-image`;
|
|
17
|
+
`verified_at` NULL, since the preview id is not listed). A typical image costs
|
|
18
|
+
about $0.08. Apply it with `npx melchizedek-db apply` before upgrading; without it,
|
|
19
|
+
a turn that made an image prices NULL.
|
|
20
|
+
|
|
21
|
+
## 1.15.1 — 2026-11-06
|
|
22
|
+
|
|
23
|
+
**Fixed: a conversation stuck on Anthropic after an approval** (ADR 0155).
|
|
24
|
+
When one model step made several tool calls and one of them required
|
|
25
|
+
approval, the calls beside it ran but their results were never stored. After
|
|
26
|
+
the approval the history held the step's calls with only the approved one
|
|
27
|
+
answered, and Anthropic refused every later request with `tool_use ids were
|
|
28
|
+
found without tool_result blocks immediately after`. The loop now stores the
|
|
29
|
+
other calls' results before the approval request. The request builder also
|
|
30
|
+
gives any call the history leaves unanswered an error result: an approval
|
|
31
|
+
the person moved on from by typing a new message, a run that ended between a call and its
|
|
32
|
+
result, and sessions stored before this release, which recover without
|
|
33
|
+
editing. Gemini, which accepted the unanswered calls, sees the same history
|
|
34
|
+
plus those results.
|
|
35
|
+
|
|
7
36
|
## 1.15.0 — 2026-11-02
|
|
8
37
|
|
|
9
38
|
**Breaking — read before upgrading**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 0024_image_model_pricing — a price row for the image model generate_image
|
|
3
|
+
-- calls (extends ADR 0136)
|
|
4
|
+
-- ============================================================================
|
|
5
|
+
-- Rows only, in 0017's column layout (USD per million tokens). generate_image
|
|
6
|
+
-- (lib/tools/generateImageTool.ts) now traces its call as an llm.request, so
|
|
7
|
+
-- the turn's usage holds gemini-3.1-flash-image-preview; without a row the
|
|
8
|
+
-- whole turn prices NULL.
|
|
9
|
+
--
|
|
10
|
+
-- gemini-3.1-flash-image-preview: Google's Gemini API pricing page
|
|
11
|
+
-- (aistudio.google.com/docs/pricing, checked 2026-11-06) lists the model as
|
|
12
|
+
-- gemini-3.1-flash-image, standard tier: $0.50 in, $3.00 text and thinking
|
|
13
|
+
-- out, $60.00 image out. The preview id is not on the page, so verified_at
|
|
14
|
+
-- stays NULL (0015's convention). A flat row has one output rate: it holds
|
|
15
|
+
-- the image rate, since nearly all of the call's output is the image, so the
|
|
16
|
+
-- few text tokens beside it are overcounted. Thinking bills at $3.00.
|
|
17
|
+
--
|
|
18
|
+
-- New rows never overwrite a row that exists: an operator's own rates
|
|
19
|
+
-- survive. Nothing secret, nothing personal. Idempotent; safe to re-run.
|
|
20
|
+
-- ============================================================================
|
|
21
|
+
|
|
22
|
+
INSERT INTO model_pricing (model, input_per_mtok, output_per_mtok, thinking_per_mtok, cache_read_per_mtok, cache_write_per_mtok, verified_at, source, note) VALUES
|
|
23
|
+
('gemini-3.1-flash-image-preview', 0.50, 60.00, 3.00, NULL, NULL, NULL, 'aistudio.google.com/docs/pricing (as gemini-3.1-flash-image), checked 2026-11-06', 'output at the image rate; text output ($3.00) is overcounted; preview id not on the page')
|
|
24
|
+
ON CONFLICT (model) DO NOTHING;
|
|
25
|
+
|
|
26
|
+
INSERT INTO melchizedek_schema_version (version, name)
|
|
27
|
+
VALUES (24, '0024_image_model_pricing')
|
|
28
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -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 } : {}),
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
* The contract is an own Tool (lib/tools/tool.ts); `generateImageTool` is the
|
|
15
15
|
* own Tool the registry lists, made from it by asTool.
|
|
16
16
|
*/
|
|
17
|
+
import { GoogleGenAI } from '@google/genai';
|
|
17
18
|
import { z } from 'zod';
|
|
19
|
+
export declare const IMAGE_MODEL = "gemini-3.1-flash-image-preview";
|
|
20
|
+
/** Makes the Gemini client; a test swaps it for a stub. */
|
|
21
|
+
export declare const imageClient: {
|
|
22
|
+
create: (apiKey: string) => Pick<GoogleGenAI, 'models'>;
|
|
23
|
+
};
|
|
18
24
|
export declare const generateImageContract: import("./toolContract.ts").DefinedTool<z.ZodObject<{
|
|
19
25
|
prompt: z.ZodString;
|
|
20
26
|
style: z.ZodOptional<z.ZodString>;
|
|
@@ -21,7 +21,12 @@ import { z } from 'zod';
|
|
|
21
21
|
import { asTool, defineTool } from './toolContract.js';
|
|
22
22
|
import { outputsDir } from '../paths.js';
|
|
23
23
|
import { credentialValue } from '../models/credentialSource.js';
|
|
24
|
-
|
|
24
|
+
import { traceLlmGeneration } from '../observability/tracer.js';
|
|
25
|
+
export const IMAGE_MODEL = 'gemini-3.1-flash-image-preview';
|
|
26
|
+
/** Makes the Gemini client; a test swaps it for a stub. */
|
|
27
|
+
export const imageClient = {
|
|
28
|
+
create: (apiKey) => new GoogleGenAI({ apiKey }),
|
|
29
|
+
};
|
|
25
30
|
export const generateImageContract = defineTool({
|
|
26
31
|
name: 'generate_image',
|
|
27
32
|
description: 'Generates a real image from a structured payload and saves it to the outputs/ directory. ' +
|
|
@@ -42,7 +47,7 @@ export const generateImageContract = defineTool({
|
|
|
42
47
|
}
|
|
43
48
|
// Sizes and which options are set, never their values: the prompt carries the person's words.
|
|
44
49
|
console.log(`[ImageTool] Parameters - prompt ${prompt.length} chars; set: ${[style && 'style', aspect_ratio && 'aspect_ratio', color_palette && 'color_palette'].filter(Boolean).join(', ') || 'none'}`);
|
|
45
|
-
const ai =
|
|
50
|
+
const ai = imageClient.create(apiKey);
|
|
46
51
|
// Build a rich prompt that bakes in the structured parameters.
|
|
47
52
|
const parts = [prompt];
|
|
48
53
|
if (style)
|
|
@@ -54,13 +59,28 @@ export const generateImageContract = defineTool({
|
|
|
54
59
|
const fullPrompt = parts.join('. ');
|
|
55
60
|
try {
|
|
56
61
|
console.log(`[ImageTool] Sending generateContent request to Gemini API (Model: ${IMAGE_MODEL})...`);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Traced as one llm.request like every other model call, so the image
|
|
63
|
+
// model's tokens reach the turn's usage and the ledger prices them
|
|
64
|
+
// (model_pricing); it also counts against the turn's step budget and stops on cancel.
|
|
65
|
+
let response;
|
|
66
|
+
async function* call() {
|
|
67
|
+
response = await ai.models.generateContent({
|
|
68
|
+
model: IMAGE_MODEL,
|
|
69
|
+
contents: [{ role: 'user', parts: [{ text: fullPrompt }] }],
|
|
70
|
+
config: {
|
|
71
|
+
responseModalities: ['IMAGE', 'TEXT'],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
yield { usageMetadata: response.usageMetadata };
|
|
75
|
+
}
|
|
76
|
+
const request = () => ({ model: IMAGE_MODEL, messages: [{ role: 'user', parts: [{ type: 'text', text: fullPrompt }] }] });
|
|
77
|
+
for await (const r of traceLlmGeneration({ provider: 'gemini', model: IMAGE_MODEL, request }, call())) {
|
|
78
|
+
// A refused call (the step budget, a cancelled turn) never reaches the API.
|
|
79
|
+
if (r.errorCode)
|
|
80
|
+
return `[generate_image ERROR] Model: ${IMAGE_MODEL} - ${r.errorMessage || r.errorCode}`;
|
|
81
|
+
}
|
|
82
|
+
if (!response)
|
|
83
|
+
return `No image data returned. Model used: ${IMAGE_MODEL}`;
|
|
64
84
|
console.log("[ImageTool] Received response from Gemini API.");
|
|
65
85
|
const candidates = response.candidates ?? [];
|
|
66
86
|
console.log(`[ImageTool] Candidates found: ${candidates.length}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "melchizedek-agents",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.2",
|
|
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",
|