melchizedek-agents 1.6.0 → 1.8.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/CHANGELOG.md +113 -0
- package/DOCUMENTATION.md +16 -7
- package/config/agents/syndicate.schema.json +63 -0
- package/config/agents/syndicateSchema.yaml +50 -0
- package/dist/lib/a2a/executor.js +21 -0
- package/dist/lib/compile.d.ts +4 -3
- package/dist/lib/compile.js +8 -3
- package/dist/lib/doctor.d.ts +10 -0
- package/dist/lib/doctor.js +33 -4
- package/dist/lib/index.d.ts +9 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/loadSyndicate.d.ts +13 -0
- package/dist/lib/loadSyndicate.js +14 -5
- package/dist/lib/observability/supabaseSpanExporter.js +2 -1
- package/dist/lib/observability/tracer.d.ts +2 -2
- package/dist/lib/observability/tracer.js +2 -2
- package/dist/lib/project.d.ts +116 -0
- package/dist/lib/project.js +274 -0
- package/dist/lib/runtime/native/checkpoint.d.ts +2 -1
- package/dist/lib/runtime/native/checkpoint.js +6 -2
- package/dist/lib/runtime/native/interrupts.js +9 -2
- package/dist/lib/runtime/nativeTurn.d.ts +2 -1
- package/dist/lib/runtime/nativeTurn.js +5 -1
- package/dist/lib/runtime/syndicateTurn.d.ts +11 -1
- package/dist/lib/runtime/syndicateTurn.js +383 -5
- package/dist/lib/sandbox/sandbox.d.ts +121 -0
- package/dist/lib/sandbox/sandbox.js +276 -0
- package/dist/lib/sandbox/worker.d.ts +29 -0
- package/dist/lib/sandbox/worker.js +280 -0
- package/dist/lib/script/config.d.ts +35 -0
- package/dist/lib/script/config.js +17 -0
- package/dist/lib/script/journal.d.ts +137 -0
- package/dist/lib/script/journal.js +193 -0
- package/dist/lib/script/run.d.ts +138 -0
- package/dist/lib/script/run.js +364 -0
- package/dist/lib/syndicateSchema.d.ts +20 -0
- package/dist/lib/syndicateSchema.js +98 -6
- package/dist/lib/toolRegistry.js +4 -0
- package/dist/lib/tools/runCodeTool.d.ts +50 -0
- package/dist/lib/tools/runCodeTool.js +59 -0
- package/dist/scripts/a2a_server.js +3 -3
- package/dist/scripts/doctor.js +2 -1
- package/dist/scripts/mcp_server.js +2 -1
- package/package.json +6 -1
|
@@ -5,6 +5,7 @@ import { createClient } from '@supabase/supabase-js';
|
|
|
5
5
|
import { hasSupabaseCredentials } from './persistence/supabaseProvider.js';
|
|
6
6
|
import { validateSyndicateConfig } from './syndicateSchema.js';
|
|
7
7
|
import { v1SpellingsOf } from './agentDialect.js';
|
|
8
|
+
import { resolveAgentsDir } from './project.js';
|
|
8
9
|
// ── Variable Injection (private) ──────────────────────────
|
|
9
10
|
const TOKEN_RE = /\{\{(\w+)\}\}/g;
|
|
10
11
|
const FULL_TOKEN_RE = /^\{\{(\w+)\}\}$/;
|
|
@@ -87,7 +88,12 @@ function resolveAndValidate(raw, label, options) {
|
|
|
87
88
|
...yamlVars,
|
|
88
89
|
...bindings,
|
|
89
90
|
};
|
|
90
|
-
|
|
91
|
+
// A script's source is code, not a template: `{{` is JavaScript there, and the script reads `variables` itself (ADR 0137 item 5).
|
|
92
|
+
const scriptSource = isPlainObject(raw) && isPlainObject(raw.script) && typeof raw.script.source === 'string' ? raw.script.source : undefined;
|
|
93
|
+
let interpolated = isPlainObject(raw) ? interpolateDeep(raw, merged) : raw;
|
|
94
|
+
if (scriptSource !== undefined && isPlainObject(interpolated) && isPlainObject(interpolated.script)) {
|
|
95
|
+
interpolated = { ...interpolated, script: { ...interpolated.script, source: scriptSource } };
|
|
96
|
+
}
|
|
91
97
|
warnV1Spellings(interpolated, label, options.onWarning);
|
|
92
98
|
const unresolved = findUnresolvedTokens(interpolated);
|
|
93
99
|
if (unresolved.length > 0) {
|
|
@@ -96,6 +102,11 @@ function resolveAndValidate(raw, label, options) {
|
|
|
96
102
|
const valid = validateSyndicateConfig(bundle !== undefined && isPlainObject(interpolated) ? { ...interpolated, bundled_references: bundle } : interpolated, label);
|
|
97
103
|
const { variables: _stripped, ...clean } = valid;
|
|
98
104
|
let config = clean;
|
|
105
|
+
// The script reads the bound variables (ADR 0137 item 6): the YAML's, then the caller's bindings, as {{token}} resolves them.
|
|
106
|
+
if (config.script) {
|
|
107
|
+
const yamlBound = isPlainObject(interpolated) && isPlainObject(interpolated.variables) ? interpolated.variables : {};
|
|
108
|
+
config = { ...config, script: { ...config.script, variables: { ...defaultBindings(), ...yamlBound, ...bindings } } };
|
|
109
|
+
}
|
|
99
110
|
if (overrides) {
|
|
100
111
|
config = applyOverrides(config, overrides);
|
|
101
112
|
}
|
|
@@ -280,10 +291,8 @@ export function readSyndicateFile(filename, options = {}) {
|
|
|
280
291
|
// Security: confine reads to the agents root. `filename` can originate from
|
|
281
292
|
// a network-controlled path segment (see a2a_server dynamic routes), so we
|
|
282
293
|
// must reject any value that resolves outside that root (e.g. "../../.env").
|
|
283
|
-
// The root itself is trusted configuration: option → env → cwd default.
|
|
284
|
-
const agentsDir =
|
|
285
|
-
process.env.MELCHIZEDEK_AGENTS_DIR ??
|
|
286
|
-
path.join(process.cwd(), 'config', 'agents'));
|
|
294
|
+
// The root itself is trusted configuration: option → env → project manifest → cwd default (lib/project.ts).
|
|
295
|
+
const agentsDir = resolveAgentsDir(options.agentsDir);
|
|
287
296
|
let filePath = path.resolve(agentsDir, filename);
|
|
288
297
|
if (filePath !== agentsDir && !filePath.startsWith(agentsDir + path.sep)) {
|
|
289
298
|
throw new Error(`Invalid syndicate path: '${filename}' resolves outside the agents directory`);
|
|
@@ -81,7 +81,8 @@ export function payloadDecision(policy, traceId, facts) {
|
|
|
81
81
|
}
|
|
82
82
|
// ── Span → row mapping (pure; tested offline) ───────────────────────────────
|
|
83
83
|
const ROOT_PREFIX = 'Syndicate Execution: ';
|
|
84
|
-
|
|
84
|
+
// A script's own spans (ADR 0137 item 16) carry its run, its calls' kinds, names, sequence and status, never an argument or a result.
|
|
85
|
+
const EXPORTED_SPAN_NAMES = /^(llm\.request$|Syndicate Execution: |script\.(invoke|call) )/;
|
|
85
86
|
function attrsOf(span) {
|
|
86
87
|
return span.attributes;
|
|
87
88
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Span, SpanContext, SpanOptions } from '@opentelemetry/api';
|
|
1
|
+
import type { Context, Span, SpanContext, SpanOptions } from '@opentelemetry/api';
|
|
2
2
|
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
3
3
|
import type { LlmResponse } from '../models/genaiMapping.ts';
|
|
4
4
|
import type { TurnEvent as Event } from '../runtime/events.ts';
|
|
@@ -67,7 +67,7 @@ export declare function initializeTracing(): void;
|
|
|
67
67
|
* the engine opens inside a turn goes through here or checks turnUntraced
|
|
68
68
|
* itself.
|
|
69
69
|
*/
|
|
70
|
-
export declare function startEngineSpan(tracerName: string, name: string, options?: SpanOptions): Span;
|
|
70
|
+
export declare function startEngineSpan(tracerName: string, name: string, options?: SpanOptions, parent?: Context): Span;
|
|
71
71
|
export interface TraceMetadata {
|
|
72
72
|
syndicateName: string;
|
|
73
73
|
bindings?: Record<string, any>;
|
|
@@ -345,11 +345,11 @@ function nonRecordingSpan() {
|
|
|
345
345
|
* the engine opens inside a turn goes through here or checks turnUntraced
|
|
346
346
|
* itself.
|
|
347
347
|
*/
|
|
348
|
-
export function startEngineSpan(tracerName, name, options) {
|
|
348
|
+
export function startEngineSpan(tracerName, name, options, parent) {
|
|
349
349
|
if (turnUntraced())
|
|
350
350
|
return nonRecordingSpan();
|
|
351
351
|
initializeTracing();
|
|
352
|
-
return trace.getTracer(tracerName).startSpan(name, options);
|
|
352
|
+
return trace.getTracer(tracerName).startSpan(name, options, parent);
|
|
353
353
|
}
|
|
354
354
|
/** Turn identity by trace id, so llm.request children can carry it too. */
|
|
355
355
|
const turnContexts = new Map();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/project.ts — the project manifest, `melchizedek.json` (ADR 0138).
|
|
3
|
+
*
|
|
4
|
+
* A project is a folder, usually a git repository, with a `melchizedek.json`
|
|
5
|
+
* at its root. The manifest records what the engine cannot infer from the
|
|
6
|
+
* files: which folder holds the syndicates, which engine version the project
|
|
7
|
+
* is written for, and how each file ships. It holds names, never values: an
|
|
8
|
+
* `env` entry is a variable name, and no field may carry a secret.
|
|
9
|
+
*
|
|
10
|
+
* `findProject()` walks up from a directory to the nearest manifest and stops
|
|
11
|
+
* at the repository root (the first directory holding `.git`) or the home
|
|
12
|
+
* directory, so a manifest outside the repository never steers it.
|
|
13
|
+
* `resolveAgentsDir()` is the one agents-directory chain every loader and bin
|
|
14
|
+
* uses: explicit option → MELCHIZEDEK_AGENTS_DIR → the manifest's `agents` →
|
|
15
|
+
* <cwd>/config/agents. A deployed server sets the variable, so the manifest
|
|
16
|
+
* only changes what a bare command run inside a project finds.
|
|
17
|
+
*
|
|
18
|
+
* Imports only node built-ins, zod and lib/paths.ts.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
export declare const PROJECT_MANIFEST = "melchizedek.json";
|
|
22
|
+
export declare const MANIFEST_VERSION = 1;
|
|
23
|
+
export declare const projectManifestSchema: z.ZodObject<{
|
|
24
|
+
manifest: z.ZodNumber;
|
|
25
|
+
engine: z.ZodString;
|
|
26
|
+
agents: z.ZodString;
|
|
27
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
|
+
tests: z.ZodOptional<z.ZodString>;
|
|
29
|
+
env: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
30
|
+
syndicates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
31
|
+
deliverable: z.ZodOptional<z.ZodEnum<{
|
|
32
|
+
library: "library";
|
|
33
|
+
mcp: "mcp";
|
|
34
|
+
service: "service";
|
|
35
|
+
}>>;
|
|
36
|
+
publish: z.ZodOptional<z.ZodObject<{
|
|
37
|
+
kind: z.ZodLiteral<"registry">;
|
|
38
|
+
id: z.ZodString;
|
|
39
|
+
}, z.core.$loose>>;
|
|
40
|
+
}, z.core.$loose>>>;
|
|
41
|
+
registry: z.ZodOptional<z.ZodObject<{
|
|
42
|
+
kind: z.ZodLiteral<"supabase">;
|
|
43
|
+
url_env: z.ZodOptional<z.ZodString>;
|
|
44
|
+
restart: z.ZodOptional<z.ZodString>;
|
|
45
|
+
}, z.core.$loose>>;
|
|
46
|
+
production: z.ZodOptional<z.ZodObject<{
|
|
47
|
+
ledger: z.ZodOptional<z.ZodObject<{
|
|
48
|
+
kind: z.ZodLiteral<"postgres">;
|
|
49
|
+
url_env: z.ZodString;
|
|
50
|
+
}, z.core.$loose>>;
|
|
51
|
+
}, z.core.$loose>>;
|
|
52
|
+
tools: z.ZodOptional<z.ZodObject<{
|
|
53
|
+
remote: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
54
|
+
mcp_env: z.ZodString;
|
|
55
|
+
}, z.core.$loose>>>;
|
|
56
|
+
}, z.core.$loose>>;
|
|
57
|
+
}, z.core.$loose>;
|
|
58
|
+
export type ProjectManifest = z.infer<typeof projectManifestSchema>;
|
|
59
|
+
export interface Project {
|
|
60
|
+
/** The folder holding `melchizedek.json`. */
|
|
61
|
+
root: string;
|
|
62
|
+
/** Absolute path of the manifest. */
|
|
63
|
+
file: string;
|
|
64
|
+
manifest: ProjectManifest;
|
|
65
|
+
/** `agents`, resolved under the root. */
|
|
66
|
+
agentsDir: string;
|
|
67
|
+
/** `tests`, resolved under the root (default `tests/agents`). */
|
|
68
|
+
testsDir: string;
|
|
69
|
+
}
|
|
70
|
+
/** Thrown for a manifest that does not parse or validate; names the file and fields, never a value. */
|
|
71
|
+
export declare class ProjectManifestError extends Error {
|
|
72
|
+
readonly file: string;
|
|
73
|
+
readonly issues: string[];
|
|
74
|
+
constructor(file: string, issues: string[]);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The nearest folder at or above `from` holding a manifest, without crossing
|
|
78
|
+
* the repository root or the home directory. Undefined when there is none.
|
|
79
|
+
*/
|
|
80
|
+
export declare function findProjectRoot(from?: string): string | undefined;
|
|
81
|
+
/** Parse and validate a manifest's text. Throws `ProjectManifestError`. */
|
|
82
|
+
export declare function parseProjectManifest(text: string, file?: string): ProjectManifest;
|
|
83
|
+
/** Load the project rooted at `root` (the folder holding the manifest). Throws `ProjectManifestError`. */
|
|
84
|
+
export declare function loadProject(root: string): Project;
|
|
85
|
+
/** The project at or above `from`, or undefined when there is none. Throws on a broken manifest. */
|
|
86
|
+
export declare function findProject(from?: string): Project | undefined;
|
|
87
|
+
/** Forget cached manifest lookups (tests, and an embedder that edits the manifest in-process). */
|
|
88
|
+
export declare function clearProjectCache(): void;
|
|
89
|
+
/**
|
|
90
|
+
* The agents directory: `explicit` → MELCHIZEDEK_AGENTS_DIR → the nearest
|
|
91
|
+
* manifest's `agents` → <cwd>/config/agents. Always absolute.
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolveAgentsDir(explicit?: string, env?: NodeJS.ProcessEnv): string;
|
|
94
|
+
/** True when `relFromRoot` (a `/`-separated path relative to the project root) matches an `ignore` pattern. */
|
|
95
|
+
export declare function isIgnored(project: Pick<Project, 'manifest'>, relFromRoot: string): boolean;
|
|
96
|
+
/** The syndicate files directly in the agents folder, as names relative to it, minus `ignore`. */
|
|
97
|
+
export declare function listProjectSyndicates(project: Project): string[];
|
|
98
|
+
/** The installed engine's package version (no commit suffix). */
|
|
99
|
+
export declare function enginePackageVersion(): string;
|
|
100
|
+
export interface ProjectCheck {
|
|
101
|
+
root: string;
|
|
102
|
+
agentsDir: string;
|
|
103
|
+
/** The manifest's engine pin, and the engine running now. */
|
|
104
|
+
engine: {
|
|
105
|
+
pinned: string;
|
|
106
|
+
running: string;
|
|
107
|
+
matches: boolean;
|
|
108
|
+
};
|
|
109
|
+
syndicates: string[];
|
|
110
|
+
/** Stop `melchizedek-doctor --check`. */
|
|
111
|
+
problems: string[];
|
|
112
|
+
/** Shown, never fatal. */
|
|
113
|
+
warnings: string[];
|
|
114
|
+
}
|
|
115
|
+
/** What is wrong with a project: engine pin, missing folders, stale ignores, unset env names. Never reads a value. */
|
|
116
|
+
export declare function checkProject(project: Project, running?: string, env?: NodeJS.ProcessEnv): ProjectCheck;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/project.ts — the project manifest, `melchizedek.json` (ADR 0138).
|
|
3
|
+
*
|
|
4
|
+
* A project is a folder, usually a git repository, with a `melchizedek.json`
|
|
5
|
+
* at its root. The manifest records what the engine cannot infer from the
|
|
6
|
+
* files: which folder holds the syndicates, which engine version the project
|
|
7
|
+
* is written for, and how each file ships. It holds names, never values: an
|
|
8
|
+
* `env` entry is a variable name, and no field may carry a secret.
|
|
9
|
+
*
|
|
10
|
+
* `findProject()` walks up from a directory to the nearest manifest and stops
|
|
11
|
+
* at the repository root (the first directory holding `.git`) or the home
|
|
12
|
+
* directory, so a manifest outside the repository never steers it.
|
|
13
|
+
* `resolveAgentsDir()` is the one agents-directory chain every loader and bin
|
|
14
|
+
* uses: explicit option → MELCHIZEDEK_AGENTS_DIR → the manifest's `agents` →
|
|
15
|
+
* <cwd>/config/agents. A deployed server sets the variable, so the manifest
|
|
16
|
+
* only changes what a bare command run inside a project finds.
|
|
17
|
+
*
|
|
18
|
+
* Imports only node built-ins, zod and lib/paths.ts.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { enginePackageRoot } from './paths.js';
|
|
25
|
+
export const PROJECT_MANIFEST = 'melchizedek.json';
|
|
26
|
+
export const MANIFEST_VERSION = 1;
|
|
27
|
+
/** A path relative to the project root: no absolute paths, no `..` segments. */
|
|
28
|
+
const relPath = z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.refine((p) => !/^([a-zA-Z]:)?[\\/]/.test(p) && !p.split(/[\\/]/).includes('..'), 'must be relative to the project root and stay inside it');
|
|
32
|
+
const envName = z.string().regex(/^[A-Z][A-Z0-9_]{0,63}$/, 'must be an environment variable name');
|
|
33
|
+
const syndicateRecord = z
|
|
34
|
+
.object({
|
|
35
|
+
deliverable: z.enum(['service', 'library', 'mcp']).optional(),
|
|
36
|
+
publish: z.object({ kind: z.literal('registry'), id: z.string().min(1) }).passthrough().optional(),
|
|
37
|
+
})
|
|
38
|
+
.passthrough();
|
|
39
|
+
export const projectManifestSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
manifest: z.number().int().min(1),
|
|
42
|
+
engine: z.string().regex(/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, 'must be an exact version, e.g. 1.8.0'),
|
|
43
|
+
agents: relPath,
|
|
44
|
+
ignore: z.array(z.string().min(1)).optional(),
|
|
45
|
+
tests: relPath.optional(),
|
|
46
|
+
env: z.array(envName).optional(),
|
|
47
|
+
syndicates: z.record(z.string(), syndicateRecord).optional(),
|
|
48
|
+
registry: z
|
|
49
|
+
.object({ kind: z.literal('supabase'), url_env: envName.optional(), restart: z.string().optional() })
|
|
50
|
+
.passthrough()
|
|
51
|
+
.optional(),
|
|
52
|
+
production: z
|
|
53
|
+
.object({ ledger: z.object({ kind: z.literal('postgres'), url_env: envName }).passthrough().optional() })
|
|
54
|
+
.passthrough()
|
|
55
|
+
.optional(),
|
|
56
|
+
tools: z
|
|
57
|
+
.object({ remote: z.record(z.string(), z.object({ mcp_env: envName }).passthrough()).optional() })
|
|
58
|
+
.passthrough()
|
|
59
|
+
.optional(),
|
|
60
|
+
})
|
|
61
|
+
// Unknown keys are kept: a newer writer's fields survive an older reader.
|
|
62
|
+
.passthrough();
|
|
63
|
+
/** Thrown for a manifest that does not parse or validate; names the file and fields, never a value. */
|
|
64
|
+
export class ProjectManifestError extends Error {
|
|
65
|
+
file;
|
|
66
|
+
issues;
|
|
67
|
+
constructor(file, issues) {
|
|
68
|
+
super(`${file}: ${issues.join('; ')}`);
|
|
69
|
+
this.file = file;
|
|
70
|
+
this.issues = issues;
|
|
71
|
+
this.name = 'ProjectManifestError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The nearest folder at or above `from` holding a manifest, without crossing
|
|
76
|
+
* the repository root or the home directory. Undefined when there is none.
|
|
77
|
+
*/
|
|
78
|
+
export function findProjectRoot(from = process.cwd()) {
|
|
79
|
+
const home = resolve(homedir());
|
|
80
|
+
let dir = resolve(from);
|
|
81
|
+
for (;;) {
|
|
82
|
+
if (existsSync(join(dir, PROJECT_MANIFEST)))
|
|
83
|
+
return dir;
|
|
84
|
+
if (existsSync(join(dir, '.git')) || dir === home)
|
|
85
|
+
return undefined;
|
|
86
|
+
const parent = dirname(dir);
|
|
87
|
+
if (parent === dir)
|
|
88
|
+
return undefined;
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Parse and validate a manifest's text. Throws `ProjectManifestError`. */
|
|
93
|
+
export function parseProjectManifest(text, file = PROJECT_MANIFEST) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = JSON.parse(text);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new ProjectManifestError(file, ['not valid JSON']);
|
|
100
|
+
}
|
|
101
|
+
const parsed = projectManifestSchema.safeParse(raw);
|
|
102
|
+
if (!parsed.success) {
|
|
103
|
+
throw new ProjectManifestError(file, parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`));
|
|
104
|
+
}
|
|
105
|
+
if (parsed.data.manifest > MANIFEST_VERSION) {
|
|
106
|
+
throw new ProjectManifestError(file, [`manifest: version ${parsed.data.manifest} is newer than this engine reads (${MANIFEST_VERSION}); upgrade melchizedek-agents`]);
|
|
107
|
+
}
|
|
108
|
+
return parsed.data;
|
|
109
|
+
}
|
|
110
|
+
/** A path under the root, or an error when it would leave it. */
|
|
111
|
+
function under(root, rel, field, file) {
|
|
112
|
+
const abs = resolve(root, rel);
|
|
113
|
+
if (abs !== root && !abs.startsWith(root + sep))
|
|
114
|
+
throw new ProjectManifestError(file, [`${field}: resolves outside the project root`]);
|
|
115
|
+
return abs;
|
|
116
|
+
}
|
|
117
|
+
/** Load the project rooted at `root` (the folder holding the manifest). Throws `ProjectManifestError`. */
|
|
118
|
+
export function loadProject(root) {
|
|
119
|
+
const r = resolve(root);
|
|
120
|
+
const file = join(r, PROJECT_MANIFEST);
|
|
121
|
+
const manifest = parseProjectManifest(readFileSync(file, 'utf-8'), file);
|
|
122
|
+
return {
|
|
123
|
+
root: r,
|
|
124
|
+
file,
|
|
125
|
+
manifest,
|
|
126
|
+
agentsDir: under(r, manifest.agents, 'agents', file),
|
|
127
|
+
testsDir: under(r, manifest.tests ?? join('tests', 'agents'), 'tests', file),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** The project at or above `from`, or undefined when there is none. Throws on a broken manifest. */
|
|
131
|
+
export function findProject(from = process.cwd()) {
|
|
132
|
+
const root = findProjectRoot(from);
|
|
133
|
+
return root ? loadProject(root) : undefined;
|
|
134
|
+
}
|
|
135
|
+
// The agents-directory chain runs on every syndicate read (an A2A route
|
|
136
|
+
// reads per request), so the manifest lookup is cached per working
|
|
137
|
+
// directory. A broken manifest is cached as "none" and reported by
|
|
138
|
+
// `melchizedek-doctor`, never by a request path.
|
|
139
|
+
const agentsDirCache = new Map();
|
|
140
|
+
/** Forget cached manifest lookups (tests, and an embedder that edits the manifest in-process). */
|
|
141
|
+
export function clearProjectCache() {
|
|
142
|
+
agentsDirCache.clear();
|
|
143
|
+
}
|
|
144
|
+
function manifestAgentsDir(cwd) {
|
|
145
|
+
if (!agentsDirCache.has(cwd)) {
|
|
146
|
+
let dir = null;
|
|
147
|
+
try {
|
|
148
|
+
dir = findProject(cwd)?.agentsDir ?? null;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
dir = null;
|
|
152
|
+
}
|
|
153
|
+
agentsDirCache.set(cwd, dir);
|
|
154
|
+
}
|
|
155
|
+
return agentsDirCache.get(cwd) ?? undefined;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The agents directory: `explicit` → MELCHIZEDEK_AGENTS_DIR → the nearest
|
|
159
|
+
* manifest's `agents` → <cwd>/config/agents. Always absolute.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveAgentsDir(explicit, env = process.env) {
|
|
162
|
+
const fromEnv = env.MELCHIZEDEK_AGENTS_DIR?.trim();
|
|
163
|
+
const cwd = process.cwd();
|
|
164
|
+
return resolve(explicit ?? (fromEnv || undefined) ?? manifestAgentsDir(cwd) ?? join(cwd, 'config', 'agents'));
|
|
165
|
+
}
|
|
166
|
+
/** Glob to RegExp for `*` (one segment), `**` (any depth) and `?`, on `/`-separated paths. */
|
|
167
|
+
function globToRegExp(glob) {
|
|
168
|
+
let re = '';
|
|
169
|
+
for (let i = 0; i < glob.length; i++) {
|
|
170
|
+
const c = glob[i];
|
|
171
|
+
if (c === '*') {
|
|
172
|
+
if (glob[i + 1] === '*') {
|
|
173
|
+
i++;
|
|
174
|
+
if (glob[i + 1] === '/') {
|
|
175
|
+
i++;
|
|
176
|
+
re += '(?:.*/)?';
|
|
177
|
+
}
|
|
178
|
+
else
|
|
179
|
+
re += '.*';
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
re += '[^/]*';
|
|
183
|
+
}
|
|
184
|
+
else if (c === '?')
|
|
185
|
+
re += '[^/]';
|
|
186
|
+
else
|
|
187
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
188
|
+
}
|
|
189
|
+
return new RegExp(`^${re}$`);
|
|
190
|
+
}
|
|
191
|
+
/** True when `relFromRoot` (a `/`-separated path relative to the project root) matches an `ignore` pattern. */
|
|
192
|
+
export function isIgnored(project, relFromRoot) {
|
|
193
|
+
const p = relFromRoot.split(sep).join('/');
|
|
194
|
+
return (project.manifest.ignore ?? []).some((g) => globToRegExp(g).test(p));
|
|
195
|
+
}
|
|
196
|
+
/** The syndicate files directly in the agents folder, as names relative to it, minus `ignore`. */
|
|
197
|
+
export function listProjectSyndicates(project) {
|
|
198
|
+
if (!existsSync(project.agentsDir))
|
|
199
|
+
return [];
|
|
200
|
+
const rel = relative(project.root, project.agentsDir);
|
|
201
|
+
return readdirSync(project.agentsDir)
|
|
202
|
+
.filter((f) => (f.endsWith('.yaml') || f.endsWith('.yml')) && f !== 'syndicateSchema.yaml')
|
|
203
|
+
.filter((f) => statSync(join(project.agentsDir, f)).isFile())
|
|
204
|
+
.filter((f) => !isIgnored(project, rel ? `${rel}/${f}` : f))
|
|
205
|
+
.sort();
|
|
206
|
+
}
|
|
207
|
+
/** The installed engine's package version (no commit suffix). */
|
|
208
|
+
export function enginePackageVersion() {
|
|
209
|
+
try {
|
|
210
|
+
const root = enginePackageRoot();
|
|
211
|
+
if (root)
|
|
212
|
+
return String(JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).version);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* fall through */
|
|
216
|
+
}
|
|
217
|
+
return '0.0.0';
|
|
218
|
+
}
|
|
219
|
+
/** What is wrong with a project: engine pin, missing folders, stale ignores, unset env names. Never reads a value. */
|
|
220
|
+
export function checkProject(project, running = enginePackageVersion(), env = process.env) {
|
|
221
|
+
const problems = [];
|
|
222
|
+
const warnings = [];
|
|
223
|
+
const pinned = project.manifest.engine;
|
|
224
|
+
const matches = pinned === running;
|
|
225
|
+
if (!matches)
|
|
226
|
+
problems.push(`engine: the project pins melchizedek-agents ${pinned}; ${running} is running`);
|
|
227
|
+
if (!existsSync(project.agentsDir))
|
|
228
|
+
problems.push(`agents: ${project.manifest.agents} does not exist`);
|
|
229
|
+
if (project.manifest.tests && !existsSync(project.testsDir))
|
|
230
|
+
problems.push(`tests: ${project.manifest.tests} does not exist`);
|
|
231
|
+
for (const g of project.manifest.ignore ?? []) {
|
|
232
|
+
if (!anyPathMatches(project.root, globToRegExp(g)))
|
|
233
|
+
warnings.push(`ignore: "${g}" matches nothing`);
|
|
234
|
+
}
|
|
235
|
+
for (const name of project.manifest.env ?? [])
|
|
236
|
+
if (!env[name]?.trim())
|
|
237
|
+
warnings.push(`env: ${name} is not set`);
|
|
238
|
+
const syndicates = listProjectSyndicates(project);
|
|
239
|
+
for (const f of Object.keys(project.manifest.syndicates ?? {})) {
|
|
240
|
+
if (!syndicates.includes(f))
|
|
241
|
+
warnings.push(`syndicates: ${f} is not in ${project.manifest.agents}`);
|
|
242
|
+
}
|
|
243
|
+
return { root: project.root, agentsDir: project.agentsDir, engine: { pinned, running, matches }, syndicates, problems, warnings };
|
|
244
|
+
}
|
|
245
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', 'venv', '.venv', '__pycache__', 'dist']);
|
|
246
|
+
/** Whether any path under root (depth-limited, skipping vendor folders) matches. */
|
|
247
|
+
function anyPathMatches(root, re, rel = '', depth = 0) {
|
|
248
|
+
if (depth > 6)
|
|
249
|
+
return false;
|
|
250
|
+
let entries;
|
|
251
|
+
try {
|
|
252
|
+
entries = readdirSync(join(root, rel));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
for (const e of entries) {
|
|
258
|
+
if (SKIP_DIRS.has(e))
|
|
259
|
+
continue;
|
|
260
|
+
const p = rel ? `${rel}/${e}` : e;
|
|
261
|
+
if (re.test(p))
|
|
262
|
+
return true;
|
|
263
|
+
let dir = false;
|
|
264
|
+
try {
|
|
265
|
+
dir = statSync(join(root, p)).isDirectory();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
/* unreadable entry */
|
|
269
|
+
}
|
|
270
|
+
if (dir && anyPathMatches(root, re, p, depth + 1))
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* runner, unchanged, continues where the snapshot stood. runDurableTurn
|
|
16
16
|
* is runSyndicateTurn with that store and a sink.
|
|
17
17
|
*
|
|
18
|
-
* A STEP BOUNDARY is an append that carries function responses
|
|
18
|
+
* A STEP BOUNDARY is an append that carries function responses, or a
|
|
19
|
+
* script's journal event (ADR 0137 item 8), after which
|
|
19
20
|
* every function call any session of the run holds has its response (calls
|
|
20
21
|
* listed in their event's longRunningToolIds, which wait on a person, aside).
|
|
21
22
|
* So a parent waiting on a delegated subagent is never at a boundary, the
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* runner, unchanged, continues where the snapshot stood. runDurableTurn
|
|
16
16
|
* is runSyndicateTurn with that store and a sink.
|
|
17
17
|
*
|
|
18
|
-
* A STEP BOUNDARY is an append that carries function responses
|
|
18
|
+
* A STEP BOUNDARY is an append that carries function responses, or a
|
|
19
|
+
* script's journal event (ADR 0137 item 8), after which
|
|
19
20
|
* every function call any session of the run holds has its response (calls
|
|
20
21
|
* listed in their event's longRunningToolIds, which wait on a person, aside).
|
|
21
22
|
* So a parent waiting on a delegated subagent is never at a boundary, the
|
|
@@ -75,6 +76,7 @@
|
|
|
75
76
|
*/
|
|
76
77
|
import { createHash, randomUUID } from 'node:crypto';
|
|
77
78
|
import { getFunctionCalls, getFunctionResponses, parseTurnEvents } from '../events.js';
|
|
79
|
+
import { JOURNAL_EVERY_CALL, isJournalEvent } from '../../script/journal.js';
|
|
78
80
|
import { InProcessSessionService } from '../sessions.js';
|
|
79
81
|
import { runSyndicateTurn } from '../syndicateTurn.js';
|
|
80
82
|
// ── Shapes ───────────────────────────────────────────────────────────────────
|
|
@@ -127,6 +129,8 @@ export function usableCheckpoint(cp, runId, parts) {
|
|
|
127
129
|
/** Content compared as stored JSON. */
|
|
128
130
|
const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
129
131
|
class CheckpointingStore {
|
|
132
|
+
/** A script run stores each call's journal event as it ends, and each is a step boundary (ADR 0137 item 8). */
|
|
133
|
+
[JOURNAL_EVERY_CALL] = true;
|
|
130
134
|
inner;
|
|
131
135
|
opts;
|
|
132
136
|
openingParts;
|
|
@@ -250,7 +254,7 @@ class CheckpointingStore {
|
|
|
250
254
|
if (stored.author !== 'user' && this.firstAgentSeq === undefined)
|
|
251
255
|
this.firstAgentSeq = order;
|
|
252
256
|
this.track(stored);
|
|
253
|
-
if (getFunctionResponses(stored).length > 0 && this.openCalls.size === 0)
|
|
257
|
+
if ((getFunctionResponses(stored).length > 0 || isJournalEvent(stored)) && this.openCalls.size === 0)
|
|
254
258
|
await this.boundary();
|
|
255
259
|
return stored;
|
|
256
260
|
}
|
|
@@ -163,6 +163,7 @@ import { approvalRequestOf, pendingApproval } from '../approvals.js';
|
|
|
163
163
|
import { consentFrom, pendingConsent } from '../credentials.js';
|
|
164
164
|
import { getFunctionCalls, getFunctionResponses } from '../events.js';
|
|
165
165
|
import { ASK_USER, pendingQuestion } from '../questions.js';
|
|
166
|
+
import { isJournalEvent, scriptPauseIn } from '../../script/journal.js';
|
|
166
167
|
import { childAppName, entryAppName, legacyChild, legacyEntry } from './delegate.js';
|
|
167
168
|
import { REQUEST_CONFIRMATION_CALL, REQUEST_CREDENTIAL_CALL, REQUEST_INPUT_CALL, isSegmentPrefix } from './history.js';
|
|
168
169
|
import { isToolset } from './request.js';
|
|
@@ -516,6 +517,12 @@ async function conversationPause(key, child, name, id, depth, seen = new Set())
|
|
|
516
517
|
...(routed.consent ? { consent: { ...routed.consent, path } } : {}),
|
|
517
518
|
};
|
|
518
519
|
}
|
|
520
|
+
// A nested script syndicate's own pause (ADR 0137 item 15): its askUser or a gated tool() call, authored by the script.
|
|
521
|
+
const scripted = scriptPauseIn(child.events, id);
|
|
522
|
+
if (scripted) {
|
|
523
|
+
const path = scripted.asker === name ? [name] : down([scripted.asker]);
|
|
524
|
+
return { path, ...(scripted.approval ? { approval: { ...scripted.approval, path } } : {}), ...(scripted.question ? { question: { ...scripted.question, path } } : {}) };
|
|
525
|
+
}
|
|
519
526
|
const approval = pendingApproval(child.events);
|
|
520
527
|
if (approval && approval.agent && approval.agent !== 'user' && approval.agent !== name && wanted(approval.id)) {
|
|
521
528
|
const path = down([approval.agent]);
|
|
@@ -543,8 +550,8 @@ async function conversationPause(key, child, name, id, depth, seen = new Set())
|
|
|
543
550
|
...(first.consent ? { consent: { ...first.consent, path } } : {}),
|
|
544
551
|
};
|
|
545
552
|
}
|
|
546
|
-
/** A walk's session: its node events carry a dotted node path (`<workflow>.<node>`). */
|
|
547
|
-
const isWalk = (session) => session.events.some((e) => typeof e.nodeInfo?.path === 'string' && e.nodeInfo.path.includes('.'));
|
|
553
|
+
/** A walk's session: its node events carry a dotted node path (`<workflow>.<node>`); a script's journal events do not count. */
|
|
554
|
+
const isWalk = (session) => session.events.some((e) => typeof e.nodeInfo?.path === 'string' && e.nodeInfo.path.includes('.') && !isJournalEvent(e));
|
|
548
555
|
/**
|
|
549
556
|
* The pause a nested workflow's walk holds in its child session (ADR 0111):
|
|
550
557
|
* the workflow's own pause record, the last event, authored by the
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* feature (UnsupportedOnRuntimeError): a caller's `transformAgent` (it
|
|
19
19
|
* transformed ADK agents, and ADK left in 1.0.0), and an `ask_user` tool on
|
|
20
20
|
* a workflow's agent (the schema refuses it; a config that skipped
|
|
21
|
-
* validation is refused here, ADR 0095)
|
|
21
|
+
* validation is refused here, ADR 0095), and a script or a `run_code`
|
|
22
|
+
* tool while MELCHIZEDEK_CODE_SANDBOX is off (CodeSandboxOffError, ADR 0137). A workflow syndicate runs on the
|
|
22
23
|
* engine's scheduler (lib/workflow/turn.ts, ADR 0095). An answer to an
|
|
23
24
|
* approval resumes the call (lib/runtime/native/interrupts.ts), an answer
|
|
24
25
|
* to a question resumes the agent that asked (lib/runtime/questions.ts, ADR
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* feature (UnsupportedOnRuntimeError): a caller's `transformAgent` (it
|
|
19
19
|
* transformed ADK agents, and ADK left in 1.0.0), and an `ask_user` tool on
|
|
20
20
|
* a workflow's agent (the schema refuses it; a config that skipped
|
|
21
|
-
* validation is refused here, ADR 0095)
|
|
21
|
+
* validation is refused here, ADR 0095), and a script or a `run_code`
|
|
22
|
+
* tool while MELCHIZEDEK_CODE_SANDBOX is off (CodeSandboxOffError, ADR 0137). A workflow syndicate runs on the
|
|
22
23
|
* engine's scheduler (lib/workflow/turn.ts, ADR 0095). An answer to an
|
|
23
24
|
* approval resumes the call (lib/runtime/native/interrupts.ts), an answer
|
|
24
25
|
* to a question resumes the agent that asked (lib/runtime/questions.ts, ADR
|
|
@@ -30,6 +31,7 @@ import { createTurnEvent } from './events.js';
|
|
|
30
31
|
import { runAgentLoop } from './native/agentLoop.js';
|
|
31
32
|
import { ASK_USER } from './questions.js';
|
|
32
33
|
import { unsupportedOnNative } from './runtimeFlag.js';
|
|
34
|
+
import { refuseWithoutSandbox } from '../sandbox/sandbox.js';
|
|
33
35
|
// ── What native refuses before a turn starts ─────────────────────────────────
|
|
34
36
|
/**
|
|
35
37
|
* Throws UnsupportedOnRuntimeError for a syndicate or a call the runtime
|
|
@@ -39,6 +41,8 @@ export function refuseOnNative(config, call) {
|
|
|
39
41
|
const where = config.syndicate_name || config.orchestrator?.name || 'syndicate';
|
|
40
42
|
if (call.transformAgent)
|
|
41
43
|
throw unsupportedOnNative('transformAgent (it transformed ADK agents; the ADK runtime was removed in 1.0.0)', where);
|
|
44
|
+
// A script, or an agent listing run_code, runs code: only once the operator turned the sandbox on (ADR 0137 item 4).
|
|
45
|
+
refuseWithoutSandbox(config);
|
|
42
46
|
if (call.isWorkflow) {
|
|
43
47
|
// A pause inside an agent node cannot be resumed by the native walk (lib/workflow/resume.ts).
|
|
44
48
|
const asking = [config.orchestrator, ...(config.subagents ?? [])].find((agent) => (agent?.tools ?? []).includes(ASK_USER));
|
|
@@ -64,6 +64,8 @@ export { ASK_USER, pendingQuestion, questionAnswerPart } from './questions.ts';
|
|
|
64
64
|
import type { PendingApproval } from './approvals.ts';
|
|
65
65
|
export { approvalResponsePart, describeApproval, pendingApproval } from './approvals.ts';
|
|
66
66
|
export type { PendingApproval } from './approvals.ts';
|
|
67
|
+
import type { CodeRun } from '../tools/runCodeTool.ts';
|
|
68
|
+
export type { CodeRun } from '../tools/runCodeTool.ts';
|
|
67
69
|
import type { AgentBranch, TurnStopReason } from './turnControl.ts';
|
|
68
70
|
export type { AgentBranch } from './turnControl.ts';
|
|
69
71
|
/** One part of the user's message. Text is the common case; the genai part
|
|
@@ -222,8 +224,10 @@ export interface DrainedRun {
|
|
|
222
224
|
code: string;
|
|
223
225
|
message: string;
|
|
224
226
|
}>;
|
|
227
|
+
/** Every run_code call that got its result, with the agent that made it (ADR 0137). */
|
|
228
|
+
codeRuns: CodeRun[];
|
|
225
229
|
}
|
|
226
|
-
export type TurnStage = 'classify' | 'dispatch' | 'delegate' | 'workflow';
|
|
230
|
+
export type TurnStage = 'classify' | 'dispatch' | 'delegate' | 'workflow' | 'script';
|
|
227
231
|
export interface RouteDecision extends RouteResolution {
|
|
228
232
|
/** `approval`: the turn resumed the route that asked for an approval (ADR 0028).
|
|
229
233
|
* `answer`: the message answered the route's open `ask_user` question.
|
|
@@ -272,6 +276,12 @@ export interface SyndicateTurnResult {
|
|
|
272
276
|
usage: TurnUsage;
|
|
273
277
|
/** Set when a control (cancel, deadline, step limit) stopped the turn. */
|
|
274
278
|
stopReason?: TurnStopReason;
|
|
279
|
+
/**
|
|
280
|
+
* The run_code runs of agents whose YAML sets `run_code: { attach: true }`
|
|
281
|
+
* (ADR 0137 item 3), in order: the reply's `code_run` data parts. Absent
|
|
282
|
+
* when there are none.
|
|
283
|
+
*/
|
|
284
|
+
codeRuns?: CodeRun[];
|
|
275
285
|
}
|
|
276
286
|
/** A turn's spend: model calls and the tokens the providers reported. */
|
|
277
287
|
export interface TurnUsage {
|