melchizedek-agents 0.16.2 → 0.18.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/.env.example +40 -3
- package/AGENT_SETUP.md +4 -2
- package/CHANGELOG.md +246 -0
- package/DOCUMENTATION.md +311 -15
- package/QUICKSTART.md +2 -2
- package/README.md +125 -149
- package/config/agents/examples/harness.yaml +135 -0
- package/config/agents/examples/model_zoo.yaml +23 -9
- package/config/agents/examples/pipeline.yaml +120 -0
- package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
- package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
- package/config/agents/examples/weather.yaml +59 -0
- package/config/agents/syndicateSchema.yaml +125 -1
- package/db/hardening.sql +4 -3
- package/db/migrations/0011_erase_expired.sql +198 -0
- package/db/migrations/0012_audit_log.sql +98 -0
- package/dist/lib/a2a/app.d.ts +27 -0
- package/dist/lib/a2a/app.js +101 -5
- package/dist/lib/a2a/executor.d.ts +34 -4
- package/dist/lib/a2a/executor.js +109 -9
- package/dist/lib/compile.d.ts +41 -3
- package/dist/lib/compile.js +162 -17
- package/dist/lib/config.d.ts +24 -0
- package/dist/lib/config.js +24 -0
- package/dist/lib/doctor.js +17 -4
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/loadEnv.d.ts +5 -0
- package/dist/lib/loadEnv.js +7 -0
- package/dist/lib/loadSyndicate.d.ts +50 -0
- package/dist/lib/loadSyndicate.js +19 -3
- package/dist/lib/memory/erase.d.ts +5 -0
- package/dist/lib/memory/erase.js +5 -0
- package/dist/lib/memory/providers.d.ts +27 -0
- package/dist/lib/memory/providers.js +28 -0
- package/dist/lib/models/capabilities.js +10 -0
- package/dist/lib/models/endpoints.d.ts +1 -1
- package/dist/lib/models/endpoints.js +4 -1
- package/dist/lib/models/fallback.d.ts +41 -0
- package/dist/lib/models/fallback.js +105 -0
- package/dist/lib/models/gateway.d.ts +2 -2
- package/dist/lib/models/gateway.js +4 -4
- package/dist/lib/models/kimiLlm.d.ts +123 -0
- package/dist/lib/models/kimiLlm.js +171 -0
- package/dist/lib/models/providerMap.d.ts +2 -2
- package/dist/lib/models/providerMap.js +4 -1
- package/dist/lib/models/registry.d.ts +1 -1
- package/dist/lib/models/registry.js +11 -3
- package/dist/lib/net/redirects.d.ts +14 -0
- package/dist/lib/net/redirects.js +94 -0
- package/dist/lib/observability/audit.d.ts +21 -0
- package/dist/lib/observability/audit.js +39 -0
- package/dist/lib/observability/metrics.d.ts +6 -0
- package/dist/lib/observability/tracer.d.ts +14 -1
- package/dist/lib/observability/tracer.js +16 -2
- package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
- package/dist/lib/persistence/supabaseProvider.js +2 -31
- package/dist/lib/runtime/questions.d.ts +55 -0
- package/dist/lib/runtime/questions.js +107 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +45 -5
- package/dist/lib/runtime/syndicateTurn.js +165 -17
- package/dist/lib/storage/postgres/index.d.ts +9 -0
- package/dist/lib/storage/postgres/index.js +13 -0
- package/dist/lib/storage/rlsStatus.d.ts +45 -0
- package/dist/lib/storage/rlsStatus.js +69 -0
- package/dist/lib/syndicateSchema.d.ts +177 -0
- package/dist/lib/syndicateSchema.js +343 -3
- package/dist/lib/toolRegistry.js +9 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +26 -0
- package/dist/lib/tools/mcpToolFactory.js +61 -4
- package/dist/lib/tools/openapiTools.d.ts +52 -0
- package/dist/lib/tools/openapiTools.js +225 -0
- package/dist/lib/tools/skillToolset.d.ts +135 -0
- package/dist/lib/tools/skillToolset.js +247 -0
- package/dist/lib/tools/urlContextTool.d.ts +29 -0
- package/dist/lib/tools/urlContextTool.js +39 -0
- package/dist/lib/workflow.d.ts +72 -0
- package/dist/lib/workflow.js +180 -0
- package/dist/lib/workflowConfig.d.ts +95 -0
- package/dist/lib/workflowConfig.js +93 -0
- package/dist/scripts/a2a_server.js +51 -8
- package/dist/scripts/db.d.ts +9 -0
- package/dist/scripts/db.js +30 -15
- package/dist/scripts/syndicate_chat.js +39 -5
- package/package.json +6 -3
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-author.md +8 -4
- package/skills/briefs/melchizedek-memory.md +1 -1
- package/skills/briefs/melchizedek-models.md +1 -1
- package/skills/briefs/melchizedek-scribe.md +4 -4
- package/skills/briefs/melchizedek-serve.md +1 -1
- package/skills/briefs/melchizedek.md +4 -1
- package/skills/melchizedek/SKILL.md +4 -1
- package/skills/melchizedek-author/SKILL.md +7 -3
- package/skills/melchizedek-memory/SKILL.md +1 -1
- package/skills/melchizedek-models/SKILL.md +1 -1
- package/skills/melchizedek-scribe/SKILL.md +3 -3
- package/skills/melchizedek-serve/SKILL.md +1 -1
- /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
- /package/skills/melchizedek-scribe/{templates → assets}/brief.md +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/kimiLlm.ts — Moonshot AI (Kimi) provider for the ADK LLMRegistry.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* Model optionality is a primary driver of this framework: the agent YAML
|
|
6
|
+
* declares `model`, and the registry routes it to the right provider.
|
|
7
|
+
* Kimi K3 (July 2026) is the strongest open-weight model on the public
|
|
8
|
+
* coding and research boards, and Moonshot serves it first-party with
|
|
9
|
+
* tool calling, strict structured output and vision, so it earns a
|
|
10
|
+
* direct adapter rather than only the gateway path.
|
|
11
|
+
*
|
|
12
|
+
* WHY it subclasses the chat-completions base (lib/models/openAiCompatibleLlm.ts):
|
|
13
|
+
* Moonshot's API is OpenAI Chat Completions at https://api.moonshot.ai/v1
|
|
14
|
+
* (it also serves Responses on the same base and Anthropic Messages at
|
|
15
|
+
* /anthropic; neither adds a capability the chat dialect lacks here).
|
|
16
|
+
* The base provides tool calling, strict `json_schema` structured output
|
|
17
|
+
* (Moonshot documents `strict: true`), SSE streaming with usage, image
|
|
18
|
+
* parts as base64 data URIs (Moonshot rejects public image URLs, which is
|
|
19
|
+
* what the base never sends), `reasoning_content` surfaced as THINKING
|
|
20
|
+
* output, retries, and the llm.request span.
|
|
21
|
+
*
|
|
22
|
+
* HOW TO ENABLE:
|
|
23
|
+
* 1. Create a key at https://platform.moonshot.ai (international; the
|
|
24
|
+
* .cn console serves mainland China) and put it in .env as
|
|
25
|
+
* MOONSHOT_API_KEY.
|
|
26
|
+
* 2. Set model: "kimi-k3" (or any kimi-* id) in your YAML.
|
|
27
|
+
* registerAvailableProviders() registers this adapter when the key is set.
|
|
28
|
+
* Optional: MOONSHOT_BASE_URL for a proxy that speaks the same dialect.
|
|
29
|
+
*
|
|
30
|
+
* THE FAMILY (platform.moonshot.ai › Model list, October 2026; USD per 1M tokens):
|
|
31
|
+
*
|
|
32
|
+
* model context in / cache hit / out use it for
|
|
33
|
+
* ──────────────────────────────────────────────────────────────────────
|
|
34
|
+
* kimi-k3 1M $3.00 / $0.30 / $15 DEFAULT — the flagship: 2.8T MoE, vision,
|
|
35
|
+
* always-on thinking, effort low|high|max
|
|
36
|
+
* kimi-k2.6 256K $0.95 / $0.16 / $4 the cheaper general tier: vision, thinking
|
|
37
|
+
* switchable; fragile on complex schemas
|
|
38
|
+
* kimi-k2.7-code 256K $0.95 / $0.19 / $4 coding; thinking cannot be turned off
|
|
39
|
+
* kimi-k2.7-code-highspeed 256K $1.90 / $0.38 / $8 the same model at ~180 tokens/s
|
|
40
|
+
*
|
|
41
|
+
* CHOOSING A MODEL (cost is a design property; see the model-routing skill):
|
|
42
|
+
* K3 is priced like a mid-tier closed model, not like an open-weight
|
|
43
|
+
* bargain: $3 / $15 is Claude Sonnet 4.6's price, and Claude Sonnet 5.5
|
|
44
|
+
* ($2 / $10, cache reads $0.20) undercuts it on every token while sitting
|
|
45
|
+
* above it on the coding boards. Two things make K3 dearer than its list
|
|
46
|
+
* price: thinking is always on and billed as output, and Moonshot's
|
|
47
|
+
* default effort is max — hence the pinned DEFAULT_KIMI_REASONING_EFFORT
|
|
48
|
+
* and the zoo agent at "low". What K3 buys is the strongest open-weight
|
|
49
|
+
* model (a self-host option) and a 1M context at a flat rate. For cost,
|
|
50
|
+
* reach for kimi-k2.6 (a quarter of K3's price, vision and tools) on
|
|
51
|
+
* subagent and bulk work, or serve K3 through the gateway: OpenRouter's
|
|
52
|
+
* endpoints range from about $0.88 to $3.45 per 1M input, the cheap ones
|
|
53
|
+
* quantized, with fidelity as the trade.
|
|
54
|
+
*
|
|
55
|
+
* There is no small or "flash" K3; K2.6 is the light tier. K3's weights
|
|
56
|
+
* are open (moonshotai/Kimi-K3 on Hugging Face), so the id is also served
|
|
57
|
+
* by OpenRouter, Together, Fireworks, Vercel AI Gateway and others; with
|
|
58
|
+
* no direct key, MODEL_GATEWAY serves it as `moonshotai/kimi-k3`
|
|
59
|
+
* (lib/models/gateway.ts). kimi-k2.5 and the moonshot-v1 ids were retired
|
|
60
|
+
* on 2026-08-31 and are not routed here on purpose.
|
|
61
|
+
*
|
|
62
|
+
* REASONING:
|
|
63
|
+
* K3 always thinks; the lever is the top-level `reasoning_effort`
|
|
64
|
+
* (low | high | max, Moonshot's default max). Requests pin
|
|
65
|
+
* DEFAULT_KIMI_REASONING_EFFORT (lib/config.ts) unless the agent sets
|
|
66
|
+
* generateContentConfig.reasoningEffort; "none" is mapped to "low" on
|
|
67
|
+
* K3, which cannot switch thinking off. K2.x models take no
|
|
68
|
+
* reasoning_effort: a `thinking: { type }` switch instead, so on those
|
|
69
|
+
* ids reasoningEffort "none" or thinkingBudget 0 sends `disabled` and
|
|
70
|
+
* any effort value is dropped from the body (K2.7 Code rejects
|
|
71
|
+
* disabled thinking; Moonshot's own error says so).
|
|
72
|
+
*
|
|
73
|
+
* LIMITATIONS:
|
|
74
|
+
* - web_search: Moonshot's model-side `$web_search` built-in retires on
|
|
75
|
+
* 2026-10-20 and its replacement is a separate REST API
|
|
76
|
+
* (POST /v1/tools/search, billed per call), not a tool the model
|
|
77
|
+
* enables in the request. The sentinel is therefore dropped with a
|
|
78
|
+
* warning, like on Ollama; `web_extract` works, and `MOONSHOT_API_KEY`
|
|
79
|
+
* would fund a client-side search tool over that REST API if one is
|
|
80
|
+
* added (lib/tools/).
|
|
81
|
+
* - Thinking with tools: Moonshot asks that K3's `reasoning_content` be
|
|
82
|
+
* passed back with the assistant message on a tool loop. This base
|
|
83
|
+
* keeps scratchpads out of history, so K3 re-reasons each step
|
|
84
|
+
* (degraded, stated in lib/models/capabilities.ts); if Moonshot ever
|
|
85
|
+
* enforces it, the turn ends with MOONSHOT_HTTP_ERROR naming the field.
|
|
86
|
+
* - Every cell of its capability row is asserted against the request body
|
|
87
|
+
* the adapter sends (tests/capabilityMatrix.test.ts), as for every other
|
|
88
|
+
* provider. Verified live on 2026-10-03 through `npm run demo:models`:
|
|
89
|
+
* kimi-k3 at low effort answered the zoo's explainer prompt in 17.9 s,
|
|
90
|
+
* 440 tokens in, 486 out, 262 of them thinking, the scratchpad surfaced
|
|
91
|
+
* from reasoning_content; tool loops and search were not exercised.
|
|
92
|
+
*/
|
|
93
|
+
import { LLMRegistry } from '@google/adk';
|
|
94
|
+
import { DEFAULT_KIMI_REASONING_EFFORT } from '../config.js';
|
|
95
|
+
import { OpenAiCompatibleLlm } from './openAiCompatibleLlm.js';
|
|
96
|
+
export const MOONSHOT_BASE_URL = 'https://api.moonshot.ai/v1';
|
|
97
|
+
/** The flagship takes `reasoning_effort`; the K2 generation takes a `thinking` switch. */
|
|
98
|
+
export function isKimiK3(model) {
|
|
99
|
+
return /^kimi-k3\b/.test(model);
|
|
100
|
+
}
|
|
101
|
+
// ── KimiLlm ───────────────────────────────────────────────────────────────────
|
|
102
|
+
export class KimiLlm extends OpenAiCompatibleLlm {
|
|
103
|
+
/** Any model: "kimi-*" in a YAML config routes here after registration. */
|
|
104
|
+
static supportedModels = [/^kimi-.+/];
|
|
105
|
+
apiKey;
|
|
106
|
+
baseUrl;
|
|
107
|
+
constructor({ model, apiKey, baseUrl }) {
|
|
108
|
+
super({ model });
|
|
109
|
+
this.apiKey = apiKey;
|
|
110
|
+
this.baseUrl = (baseUrl || process.env.MOONSHOT_BASE_URL || MOONSHOT_BASE_URL).replace(/\/+$/, '');
|
|
111
|
+
}
|
|
112
|
+
providerId() {
|
|
113
|
+
return 'moonshot';
|
|
114
|
+
}
|
|
115
|
+
endpointUrl() {
|
|
116
|
+
return `${this.baseUrl}/chat/completions`;
|
|
117
|
+
}
|
|
118
|
+
key() {
|
|
119
|
+
return this.apiKey ?? process.env.MOONSHOT_API_KEY;
|
|
120
|
+
}
|
|
121
|
+
headers() {
|
|
122
|
+
return { Authorization: `Bearer ${this.key() ?? ''}` };
|
|
123
|
+
}
|
|
124
|
+
missingRequirement() {
|
|
125
|
+
if (this.key())
|
|
126
|
+
return undefined;
|
|
127
|
+
return {
|
|
128
|
+
errorCode: 'MOONSHOT_MISSING_KEY',
|
|
129
|
+
errorMessage: 'MOONSHOT_API_KEY is not set in environment.',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
httpError(status, detail) {
|
|
133
|
+
const hint = status === 401
|
|
134
|
+
? ' Check MOONSHOT_API_KEY (platform.moonshot.ai).'
|
|
135
|
+
: status === 404
|
|
136
|
+
? ` Is "${this.model}" a current Kimi id? kimi-k2.5 and moonshot-v1-* were retired on 2026-08-31.`
|
|
137
|
+
: '';
|
|
138
|
+
return {
|
|
139
|
+
errorCode: 'MOONSHOT_HTTP_ERROR',
|
|
140
|
+
errorMessage: `Moonshot returned ${status}: ${detail.slice(0, 4000)}.${hint}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The reasoning controls, per generation (see the header). The base has
|
|
145
|
+
* already put `reasoning_effort` in the body when the agent set one;
|
|
146
|
+
* these fields are merged after it, so an `undefined` here removes it
|
|
147
|
+
* (JSON.stringify drops undefined keys).
|
|
148
|
+
*/
|
|
149
|
+
extraBodyFields(llmRequest) {
|
|
150
|
+
const cfg = llmRequest.config ?? {};
|
|
151
|
+
const effort = cfg.reasoningEffort;
|
|
152
|
+
if (isKimiK3(this.model)) {
|
|
153
|
+
if (effort === undefined)
|
|
154
|
+
return { reasoning_effort: DEFAULT_KIMI_REASONING_EFFORT };
|
|
155
|
+
if (effort === 'none')
|
|
156
|
+
return { reasoning_effort: 'low' }; // K3 cannot switch thinking off
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
const budget = cfg.thinkingConfig?.thinkingBudget;
|
|
160
|
+
const off = effort === 'none' || budget === 0;
|
|
161
|
+
return {
|
|
162
|
+
reasoning_effort: undefined,
|
|
163
|
+
...(off ? { thinking: { type: 'disabled' } } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ── Registration helper ───────────────────────────────────────────────────────
|
|
168
|
+
/** Registers KimiLlm with the ADK LLMRegistry (called when MOONSHOT_API_KEY is set). */
|
|
169
|
+
export function registerKimiLlm() {
|
|
170
|
+
LLMRegistry.register(KimiLlm);
|
|
171
|
+
}
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* Model-name conventions (see DOCUMENTATION.md §7):
|
|
11
11
|
* claude-* → anthropic gpt-* / o<digit>* → openai
|
|
12
12
|
* grok-* → xai ollama/<model> → ollama (local, keyless)
|
|
13
|
-
* everything else
|
|
13
|
+
* kimi-* → moonshot everything else → gemini (the ADK-native default)
|
|
14
14
|
*/
|
|
15
|
-
export type ProviderId = 'gemini' | 'anthropic' | 'openai' | 'xai' | 'ollama';
|
|
15
|
+
export type ProviderId = 'gemini' | 'anthropic' | 'openai' | 'xai' | 'moonshot' | 'ollama';
|
|
16
16
|
interface ProviderInfo {
|
|
17
17
|
/** Env var holding the API key; null = keyless (local). */
|
|
18
18
|
keyEnv: string | null;
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
* Model-name conventions (see DOCUMENTATION.md §7):
|
|
11
11
|
* claude-* → anthropic gpt-* / o<digit>* → openai
|
|
12
12
|
* grok-* → xai ollama/<model> → ollama (local, keyless)
|
|
13
|
-
* everything else
|
|
13
|
+
* kimi-* → moonshot everything else → gemini (the ADK-native default)
|
|
14
14
|
*/
|
|
15
15
|
export const PROVIDERS = {
|
|
16
16
|
gemini: { keyEnv: 'GOOGLE_GENAI_API_KEY', label: 'Google Gemini' },
|
|
17
17
|
anthropic: { keyEnv: 'ANTHROPIC_API_KEY', label: 'Anthropic Claude' },
|
|
18
18
|
openai: { keyEnv: 'OPENAI_API_KEY', label: 'OpenAI GPT' },
|
|
19
19
|
xai: { keyEnv: 'XAI_API_KEY', label: 'xAI Grok' },
|
|
20
|
+
moonshot: { keyEnv: 'MOONSHOT_API_KEY', label: 'Moonshot Kimi' },
|
|
20
21
|
ollama: { keyEnv: null, label: 'Ollama (local)' },
|
|
21
22
|
};
|
|
22
23
|
/** Maps a model id to its provider. Unknown ids default to gemini — the
|
|
@@ -30,6 +31,8 @@ export function providerForModel(model) {
|
|
|
30
31
|
return 'openai';
|
|
31
32
|
if (/^grok-/.test(model))
|
|
32
33
|
return 'xai';
|
|
34
|
+
if (/^kimi-/.test(model))
|
|
35
|
+
return 'moonshot';
|
|
33
36
|
return 'gemini';
|
|
34
37
|
}
|
|
35
38
|
/** True when the provider's key is present (always true for local Ollama). */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* registry then string-matches supportedModels patterns:
|
|
12
12
|
* claude-* → ClaudeLlm gpt-* / o<digit>* → GptLlm
|
|
13
13
|
* grok-* → GrokLlm ollama/<model> → OllamaLlm
|
|
14
|
-
* gemini-*
|
|
14
|
+
* kimi-* → KimiLlm gemini-* → TracedGemini (ADK's Gemini + llm.request spans)
|
|
15
15
|
*
|
|
16
16
|
* 2. resolveModel() — instance factory for BYOK paths (scripts/
|
|
17
17
|
* a2a_server.ts), where a per-request API key from an HTTP header must
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* registry then string-matches supportedModels patterns:
|
|
12
12
|
* claude-* → ClaudeLlm gpt-* / o<digit>* → GptLlm
|
|
13
13
|
* grok-* → GrokLlm ollama/<model> → OllamaLlm
|
|
14
|
-
* gemini-*
|
|
14
|
+
* kimi-* → KimiLlm gemini-* → TracedGemini (ADK's Gemini + llm.request spans)
|
|
15
15
|
*
|
|
16
16
|
* 2. resolveModel() — instance factory for BYOK paths (scripts/
|
|
17
17
|
* a2a_server.ts), where a per-request API key from an HTTP header must
|
|
@@ -26,11 +26,12 @@ import { Gemini, LLMRegistry } from '@google/adk';
|
|
|
26
26
|
import { endpointFromEnv, endpointProblems, mergeEndpoint, platformModel, providerReady } from './endpoints.js';
|
|
27
27
|
import { currentTurnSignal } from '../runtime/turnControl.js';
|
|
28
28
|
import { errorStatus, retryUntilFirstYield } from './retry.js';
|
|
29
|
-
import { DEFAULT_CLAUDE_MODEL, DEFAULT_GEMINI_MODEL, DEFAULT_GPT_MODEL, DEFAULT_GROK_MODEL, DEFAULT_OLLAMA_MODEL, } from '../config.js';
|
|
29
|
+
import { DEFAULT_CLAUDE_MODEL, DEFAULT_GEMINI_MODEL, DEFAULT_GPT_MODEL, DEFAULT_GROK_MODEL, DEFAULT_KIMI_MODEL, DEFAULT_OLLAMA_MODEL, } from '../config.js';
|
|
30
30
|
import { traceLlmGeneration, setLlmSpanAttribute, } from '../observability/tracer.js';
|
|
31
31
|
import { ClaudeLlm, registerClaudeLlm } from './claudeLlm.js';
|
|
32
32
|
import { GptLlm, registerGptLlm } from './gptLlm.js';
|
|
33
33
|
import { GrokLlm, registerGrokLlm } from './grokLlm.js';
|
|
34
|
+
import { KimiLlm, registerKimiLlm } from './kimiLlm.js';
|
|
34
35
|
import { OllamaLlm, registerOllamaLlm } from './ollamaLlm.js';
|
|
35
36
|
import { GatewayLlm, registerGatewayLlm } from './gatewayLlm.js';
|
|
36
37
|
import { gatewayConfig, gatewayProblem, gatewayUsable, planTransport, } from './gateway.js';
|
|
@@ -144,6 +145,7 @@ const REGISTRARS = {
|
|
|
144
145
|
anthropic: registerClaudeLlm,
|
|
145
146
|
openai: registerGptLlm,
|
|
146
147
|
xai: registerGrokLlm,
|
|
148
|
+
moonshot: registerKimiLlm,
|
|
147
149
|
ollama: registerOllamaLlm,
|
|
148
150
|
};
|
|
149
151
|
/**
|
|
@@ -159,6 +161,7 @@ const PATTERNS = {
|
|
|
159
161
|
anthropic: ClaudeLlm.supportedModels,
|
|
160
162
|
openai: GptLlm.supportedModels,
|
|
161
163
|
xai: GrokLlm.supportedModels,
|
|
164
|
+
moonshot: KimiLlm.supportedModels,
|
|
162
165
|
};
|
|
163
166
|
let providersRegistered = false;
|
|
164
167
|
/**
|
|
@@ -209,6 +212,8 @@ function modelHint(provider) {
|
|
|
209
212
|
return 'gpt-*/o*';
|
|
210
213
|
case 'xai':
|
|
211
214
|
return 'grok-*';
|
|
215
|
+
case 'moonshot':
|
|
216
|
+
return 'kimi-*';
|
|
212
217
|
case 'ollama':
|
|
213
218
|
return 'ollama/*';
|
|
214
219
|
case 'gemini':
|
|
@@ -220,6 +225,7 @@ const DEFAULT_MODEL_FOR = {
|
|
|
220
225
|
anthropic: DEFAULT_CLAUDE_MODEL,
|
|
221
226
|
openai: DEFAULT_GPT_MODEL,
|
|
222
227
|
xai: DEFAULT_GROK_MODEL,
|
|
228
|
+
moonshot: DEFAULT_KIMI_MODEL,
|
|
223
229
|
ollama: DEFAULT_OLLAMA_MODEL,
|
|
224
230
|
};
|
|
225
231
|
/**
|
|
@@ -257,13 +263,15 @@ export function resolveModel(model, options = {}) {
|
|
|
257
263
|
return new GptLlm({ model: resolved, apiKey, endpoint });
|
|
258
264
|
case 'xai':
|
|
259
265
|
return new GrokLlm({ model: resolved, apiKey });
|
|
266
|
+
case 'moonshot':
|
|
267
|
+
return new KimiLlm({ model: resolved, apiKey, baseUrl: endpoint?.baseURL });
|
|
260
268
|
case 'gemini':
|
|
261
269
|
return new TracedGemini({ model: resolved, apiKey, endpoint });
|
|
262
270
|
}
|
|
263
271
|
}
|
|
264
272
|
function normalizeProvider(provider) {
|
|
265
273
|
const p = provider?.toLowerCase();
|
|
266
|
-
if (p === 'ollama' || p === 'anthropic' || p === 'openai' || p === 'xai') {
|
|
274
|
+
if (p === 'ollama' || p === 'anthropic' || p === 'openai' || p === 'xai' || p === 'moonshot') {
|
|
267
275
|
return p;
|
|
268
276
|
}
|
|
269
277
|
return 'gemini';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const MAX_REDIRECTS = 5;
|
|
2
|
+
export interface RedirectPolicy {
|
|
3
|
+
/** Why the request may not follow a redirect to `url`, or null to follow it. */
|
|
4
|
+
hopProblem(url: URL, crossOrigin: boolean): Promise<string | null>;
|
|
5
|
+
}
|
|
6
|
+
type Fetch = typeof globalThis.fetch;
|
|
7
|
+
/**
|
|
8
|
+
* `fetch(input, init)` with redirects followed manually under `policy`.
|
|
9
|
+
* Throws when a hop is refused or the chain is too long.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fetchWithRedirectPolicy(input: string | URL | Request, init: RequestInit | undefined, policy: RedirectPolicy, fetchImpl: Fetch): Promise<Response>;
|
|
12
|
+
/** Run `fn` with every globalThis.fetch it makes following redirects under `policy`. */
|
|
13
|
+
export declare function withRedirectGuard<T>(policy: RedirectPolicy, fn: () => Promise<T>): Promise<T>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/net/redirects.ts — follow HTTP redirects one hop at a time, each hop
|
|
3
|
+
* held to a policy, for code that calls `globalThis.fetch` itself.
|
|
4
|
+
*
|
|
5
|
+
* WHY: fetch follows redirects by default, so a guard that vets only the URL
|
|
6
|
+
* a request starts at is bypassed by any server that answers 302: an allowed
|
|
7
|
+
* public API with an open redirect can send a call to 169.254.169.254, and
|
|
8
|
+
* fetch forwards every header but Authorization and Cookie (an API key in a
|
|
9
|
+
* custom header crosses origins). web_extract follows redirects manually for
|
|
10
|
+
* this reason; ADK's RestApiTool (the OpenAPI tools) calls globalThis.fetch
|
|
11
|
+
* with no hook to pass a fetch of our own.
|
|
12
|
+
*
|
|
13
|
+
* HOW: `withRedirectGuard(policy, fn)` runs `fn` in an AsyncLocalStorage
|
|
14
|
+
* context. A wrapper installed on globalThis.fetch (once, and again if
|
|
15
|
+
* something replaced it) applies the guard only inside such a context and
|
|
16
|
+
* calls the original fetch untouched everywhere else, so no other request in
|
|
17
|
+
* the process changes behaviour and concurrent calls never see each other's
|
|
18
|
+
* policy. Inside the context every request uses `redirect: 'manual'`; each
|
|
19
|
+
* hop's URL goes to `policy.hopProblem` before it is fetched, a hop to
|
|
20
|
+
* another origin keeps only content-negotiation headers, and a chain longer
|
|
21
|
+
* than MAX_REDIRECTS is an error. 303, and 301/302 after a POST, continue as
|
|
22
|
+
* a GET without a body, as browsers do.
|
|
23
|
+
*/
|
|
24
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
25
|
+
export const MAX_REDIRECTS = 5;
|
|
26
|
+
/** The headers a request keeps when a redirect moves it to another origin. */
|
|
27
|
+
const CROSS_ORIGIN_HEADERS = new Set(['accept', 'accept-language', 'content-language', 'content-type', 'user-agent']);
|
|
28
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
29
|
+
/**
|
|
30
|
+
* `fetch(input, init)` with redirects followed manually under `policy`.
|
|
31
|
+
* Throws when a hop is refused or the chain is too long.
|
|
32
|
+
*/
|
|
33
|
+
export async function fetchWithRedirectPolicy(input, init, policy, fetchImpl) {
|
|
34
|
+
let url = new URL(input instanceof Request ? input.url : String(input));
|
|
35
|
+
let method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
36
|
+
let headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
37
|
+
let body = init?.body;
|
|
38
|
+
for (let hop = 0;; hop++) {
|
|
39
|
+
const res = await fetchImpl(url, { ...init, method, headers, body, redirect: 'manual' });
|
|
40
|
+
const location = res.headers.get('location');
|
|
41
|
+
if (!REDIRECT_STATUSES.has(res.status) || !location)
|
|
42
|
+
return res;
|
|
43
|
+
await res.body?.cancel().catch(() => { });
|
|
44
|
+
if (hop >= MAX_REDIRECTS)
|
|
45
|
+
throw new Error(`more than ${MAX_REDIRECTS} redirects from ${url.host}`);
|
|
46
|
+
let next;
|
|
47
|
+
try {
|
|
48
|
+
next = new URL(location, url);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new Error(`redirect to an unparseable URL from ${url.host}`);
|
|
52
|
+
}
|
|
53
|
+
const crossOrigin = next.origin !== url.origin;
|
|
54
|
+
const problem = await policy.hopProblem(next, crossOrigin);
|
|
55
|
+
if (problem)
|
|
56
|
+
throw new Error(`redirect refused: ${problem}`);
|
|
57
|
+
if (res.status === 303 || ((res.status === 301 || res.status === 302) && method === 'POST')) {
|
|
58
|
+
if (method !== 'HEAD')
|
|
59
|
+
method = 'GET';
|
|
60
|
+
body = undefined;
|
|
61
|
+
headers.delete('content-type');
|
|
62
|
+
headers.delete('content-length');
|
|
63
|
+
}
|
|
64
|
+
if (crossOrigin) {
|
|
65
|
+
const kept = new Headers();
|
|
66
|
+
headers.forEach((value, name) => {
|
|
67
|
+
if (CROSS_ORIGIN_HEADERS.has(name.toLowerCase()))
|
|
68
|
+
kept.set(name, value);
|
|
69
|
+
});
|
|
70
|
+
headers = kept;
|
|
71
|
+
}
|
|
72
|
+
url = next;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const guardContext = new AsyncLocalStorage();
|
|
76
|
+
const WRAPPED = Symbol.for('melchizedek.redirectGuardedFetch');
|
|
77
|
+
/** Wrap whatever globalThis.fetch is now, unless it is already our wrapper. */
|
|
78
|
+
function ensureInstalled() {
|
|
79
|
+
const current = globalThis.fetch;
|
|
80
|
+
if (current[WRAPPED])
|
|
81
|
+
return;
|
|
82
|
+
const inner = current;
|
|
83
|
+
const wrapped = ((input, init) => {
|
|
84
|
+
const policy = guardContext.getStore();
|
|
85
|
+
return policy ? fetchWithRedirectPolicy(input, init, policy, inner) : inner(input, init);
|
|
86
|
+
});
|
|
87
|
+
wrapped[WRAPPED] = true;
|
|
88
|
+
globalThis.fetch = wrapped;
|
|
89
|
+
}
|
|
90
|
+
/** Run `fn` with every globalThis.fetch it makes following redirects under `policy`. */
|
|
91
|
+
export function withRedirectGuard(policy, fn) {
|
|
92
|
+
ensureInstalled();
|
|
93
|
+
return guardContext.run(policy, fn);
|
|
94
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
export type AuditEventName = 'auth.failure' | 'task.end' | 'memory.erase';
|
|
3
|
+
export interface AuditEvent {
|
|
4
|
+
event: AuditEventName;
|
|
5
|
+
/** denied | ok | the task's final status. */
|
|
6
|
+
outcome: string;
|
|
7
|
+
/** The authenticated caller's name ('shared-secret', a caller token's name, 'jwt'). */
|
|
8
|
+
caller?: string;
|
|
9
|
+
/** SHA-256 prefix of the scope key, never the key. */
|
|
10
|
+
scopeHash?: string;
|
|
11
|
+
sourceIp?: string;
|
|
12
|
+
agentId?: string;
|
|
13
|
+
taskId?: string;
|
|
14
|
+
/** Small, content-free facts: a reason code, counts, a path. */
|
|
15
|
+
detail?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export type AuditSink = (event: AuditEvent) => void;
|
|
18
|
+
/** The scope's SHA-256 prefix, as the task log writes it. */
|
|
19
|
+
export declare function scopeHashOf(scopeKey: string | undefined): string | undefined;
|
|
20
|
+
/** Appends events to melchizedek_audit through the storage pool. */
|
|
21
|
+
export declare function postgresAuditSink(pool: Pick<Pool, 'query'>, warn?: (m: string) => void): AuditSink;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/observability/audit.ts — the audit trail: who did what, when, from
|
|
3
|
+
* where (ADR 0042).
|
|
4
|
+
*
|
|
5
|
+
* The ledger (adk_turns) records what a turn said. An auditor asks a
|
|
6
|
+
* different question: which caller, authenticated how, from which address,
|
|
7
|
+
* did what, and was it allowed. Those events are written to
|
|
8
|
+
* melchizedek_audit (db/migrations/0012), which a trigger keeps
|
|
9
|
+
* append-only. An event never carries conversation content or a scope
|
|
10
|
+
* key: a scope is a SHA-256 prefix, as in the task log.
|
|
11
|
+
*
|
|
12
|
+
* The sink is fire-and-forget: an audit write never delays or fails a
|
|
13
|
+
* request. A failed write is logged once per outage, and the event is
|
|
14
|
+
* written to stderr as one JSON line instead, so it is not lost silently.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
/** The scope's SHA-256 prefix, as the task log writes it. */
|
|
18
|
+
export function scopeHashOf(scopeKey) {
|
|
19
|
+
return scopeKey ? createHash('sha256').update(scopeKey).digest('hex').slice(0, 12) : undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Appends events to melchizedek_audit through the storage pool. */
|
|
22
|
+
export function postgresAuditSink(pool, warn = (m) => console.warn(m)) {
|
|
23
|
+
let failing = false;
|
|
24
|
+
return (e) => {
|
|
25
|
+
pool
|
|
26
|
+
.query('INSERT INTO melchizedek_audit (event, outcome, caller, scope_hash, source_ip, agent_id, task_id, detail) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)', [e.event, e.outcome, e.caller ?? null, e.scopeHash ?? null, e.sourceIp ?? null, e.agentId ?? null, e.taskId ?? null, JSON.stringify(e.detail ?? {})])
|
|
27
|
+
.then(() => {
|
|
28
|
+
if (failing)
|
|
29
|
+
warn('[audit] writes to melchizedek_audit succeed again.');
|
|
30
|
+
failing = false;
|
|
31
|
+
})
|
|
32
|
+
.catch((err) => {
|
|
33
|
+
if (!failing)
|
|
34
|
+
warn(`[audit] could not write to melchizedek_audit (${err instanceof Error ? err.message : String(err)}); events go to stderr until it recovers.`);
|
|
35
|
+
failing = true;
|
|
36
|
+
console.error(JSON.stringify({ audit: { ...e, at: new Date().toISOString() } }));
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
/** One finished (or refused) task, as the executor reports it. */
|
|
12
12
|
export interface TaskRecord {
|
|
13
|
+
/** The A2A task id: joins this record to the task store and the audit trail. */
|
|
14
|
+
taskId?: string;
|
|
15
|
+
/** The turn's trace id: joins this record to its spans and its ledger row. */
|
|
16
|
+
traceId?: string;
|
|
17
|
+
/** The trace id of the caller's `traceparent`, when it sent one: the turn's root span links to it. */
|
|
18
|
+
callerTraceId?: string;
|
|
13
19
|
/** The agent id the task was sent to; '' is the default syndicate. */
|
|
14
20
|
agentId: string;
|
|
15
21
|
syndicate: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SpanContext } from '@opentelemetry/api';
|
|
1
2
|
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
2
3
|
import type { Event, LlmResponse } from '@google/adk';
|
|
3
4
|
/**
|
|
@@ -67,6 +68,14 @@ export interface TraceMetadata {
|
|
|
67
68
|
traceId: string;
|
|
68
69
|
spanId: string;
|
|
69
70
|
}) => void;
|
|
71
|
+
/**
|
|
72
|
+
* A caller's W3C `traceparent`. The turn's root span LINKS to the caller's
|
|
73
|
+
* span (and records `caller.trace_id`) instead of joining its trace: a turn's
|
|
74
|
+
* trace id keys its ledger attribution, its in-process stats and erasure,
|
|
75
|
+
* so it must stay unique to the turn and never be chosen by a caller.
|
|
76
|
+
* Ignored unless well formed (validTraceparent).
|
|
77
|
+
*/
|
|
78
|
+
traceparent?: string;
|
|
70
79
|
/**
|
|
71
80
|
* Identity of the turn. These are what make a stored turn joinable to
|
|
72
81
|
* its session row (`adk_sessions.id` = app:user:session) and to the ADK
|
|
@@ -103,9 +112,13 @@ export declare function serverToolEvents(ev: any): Array<{
|
|
|
103
112
|
name: 'ToolCall' | 'ToolResponse';
|
|
104
113
|
attributes: Record<string, string | boolean>;
|
|
105
114
|
}>;
|
|
115
|
+
/** A well-formed version-00 W3C traceparent, or undefined. Never throws. */
|
|
116
|
+
export declare function validTraceparent(value: unknown): string | undefined;
|
|
117
|
+
/** The caller's span a turn links to, from its `traceparent`; undefined when absent or malformed. */
|
|
118
|
+
export declare function callerSpanContext(traceparent: string | undefined): SpanContext | undefined;
|
|
106
119
|
export declare function traceAgentRun(stream: AsyncIterableIterator<Event>, metadata: TraceMetadata): AsyncGenerator<Event, void, void>;
|
|
107
120
|
export interface LlmCallMeta {
|
|
108
|
-
/** Provider id, e.g. 'anthropic', 'openai', 'xai', 'ollama', 'gemini'. */
|
|
121
|
+
/** Provider id, e.g. 'anthropic', 'openai', 'xai', 'moonshot', 'ollama', 'gemini'. */
|
|
109
122
|
provider: string;
|
|
110
123
|
/** The model id as declared in the agent YAML (e.g. 'ollama/qwen3:8b'). */
|
|
111
124
|
model: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { trace, context } from '@opentelemetry/api';
|
|
1
|
+
import { trace, context, defaultTextMapGetter, ROOT_CONTEXT } from '@opentelemetry/api';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { TELEMETRY_SCHEMA_VERSION, engineVersion } from './lineage.js';
|
|
4
4
|
import { FilteringSpanExporter, otlpContentMode } from './otlpFilter.js';
|
|
@@ -345,9 +345,23 @@ export function serverToolEvents(ev) {
|
|
|
345
345
|
}
|
|
346
346
|
return out;
|
|
347
347
|
}
|
|
348
|
+
const TRACEPARENT = /^00-(?!0{32})[0-9a-f]{32}-(?!0{16})[0-9a-f]{16}-[0-9a-f]{2}$/;
|
|
349
|
+
/** A well-formed version-00 W3C traceparent, or undefined. Never throws. */
|
|
350
|
+
export function validTraceparent(value) {
|
|
351
|
+
return typeof value === 'string' && TRACEPARENT.test(value.trim().toLowerCase()) ? value.trim().toLowerCase() : undefined;
|
|
352
|
+
}
|
|
353
|
+
/** The caller's span a turn links to, from its `traceparent`; undefined when absent or malformed. */
|
|
354
|
+
export function callerSpanContext(traceparent) {
|
|
355
|
+
const tp = validTraceparent(traceparent);
|
|
356
|
+
if (!tp)
|
|
357
|
+
return undefined;
|
|
358
|
+
const ctx = new core.W3CTraceContextPropagator().extract(ROOT_CONTEXT, { traceparent: tp }, defaultTextMapGetter);
|
|
359
|
+
return trace.getSpanContext(ctx);
|
|
360
|
+
}
|
|
348
361
|
export async function* traceAgentRun(stream, metadata) {
|
|
349
362
|
initializeTracing();
|
|
350
|
-
const
|
|
363
|
+
const caller = callerSpanContext(metadata.traceparent);
|
|
364
|
+
const span = tracer.startSpan(`Syndicate Execution: ${metadata.syndicateName}`, caller ? { links: [{ context: caller }], attributes: { 'caller.trace_id': caller.traceId } } : undefined);
|
|
351
365
|
turnContexts.set(span.spanContext().traceId, {
|
|
352
366
|
sessionId: metadata.sessionId,
|
|
353
367
|
userId: metadata.userId,
|
|
@@ -9,12 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { BaseSessionService, BaseMemoryService } from '@google/adk';
|
|
11
11
|
import type { Embedder, MemoryExtractor } from '../memory/providers.ts';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
applied: boolean;
|
|
15
|
-
/** Human-readable explanation for logs. */
|
|
16
|
-
detail: string;
|
|
17
|
-
}
|
|
12
|
+
import type { RlsHardeningStatus } from '../storage/rlsStatus.ts';
|
|
13
|
+
export type { RlsHardeningStatus } from '../storage/rlsStatus.ts';
|
|
18
14
|
export interface PersistenceServices {
|
|
19
15
|
sessionService: BaseSessionService;
|
|
20
16
|
memoryService: BaseMemoryService | undefined;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Service construction (SupabaseSessionService, SupabaseVectorMemoryService)
|
|
9
9
|
*/
|
|
10
10
|
import { isPlaceholderValue } from '../loadEnv.js';
|
|
11
|
+
import { evaluateRlsRows } from '../storage/rlsStatus.js';
|
|
11
12
|
/**
|
|
12
13
|
* Returns true if all required Supabase credentials are present in the
|
|
13
14
|
* environment. Does NOT throw — callers use this to decide whether to
|
|
@@ -59,37 +60,7 @@ export async function createSupabaseServices(options) {
|
|
|
59
60
|
detail: 'db/hardening.sql has not been applied (status function missing)',
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
const unprotected = ['adk_memory_facts', 'adk_sessions'].filter((t) => !rows.some((r) => r.table_name === t && r.rls_enabled));
|
|
64
|
-
if (unprotected.length > 0) {
|
|
65
|
-
return {
|
|
66
|
-
applied: false,
|
|
67
|
-
detail: `RLS is disabled on: ${unprotected.join(', ')}`,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
// adk_agent_registry is optional (only deployments that boot
|
|
71
|
-
// `registry:<id>` create it) but NOT advisory: an unprotected registry
|
|
72
|
-
// is agent takeover via the anon key, so a present-and-open table
|
|
73
|
-
// fails the check outright. Absent table => no row => nothing to say.
|
|
74
|
-
const registryRow = rows.find((r) => r.table_name === 'adk_agent_registry');
|
|
75
|
-
if (registryRow && !registryRow.rls_enabled) {
|
|
76
|
-
return {
|
|
77
|
-
applied: false,
|
|
78
|
-
detail: 'RLS is disabled on: adk_agent_registry (agent definitions are ' +
|
|
79
|
-
'writable with the anon key) — re-run db/hardening.sql',
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
// Advisory only — adk_telemetry is optional (db/telemetry.sql). If it
|
|
83
|
-
// exists but hardening.sql wasn't re-run afterwards, say so without
|
|
84
|
-
// failing the check (the mandatory tables are protected).
|
|
85
|
-
const telemetryRow = rows.find((r) => r.table_name === 'adk_telemetry');
|
|
86
|
-
if (telemetryRow && !telemetryRow.rls_enabled) {
|
|
87
|
-
return {
|
|
88
|
-
applied: true,
|
|
89
|
-
detail: 'RLS enabled on adk_memory_facts and adk_sessions — but NOT on adk_telemetry; re-run db/hardening.sql',
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
return { applied: true, detail: 'RLS enabled on adk_memory_facts and adk_sessions' };
|
|
63
|
+
return evaluateRlsRows((data ?? []));
|
|
93
64
|
}
|
|
94
65
|
catch (err) {
|
|
95
66
|
const msg = err instanceof Error ? err.message : String(err);
|