dna-sdk 0.5.0 → 0.7.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/dist/adapters/source-url.d.ts +3 -0
- package/dist/adapters/source-url.js +28 -1
- package/dist/emit/agentFramework.d.ts +16 -0
- package/dist/emit/agentFramework.js +127 -0
- package/dist/emit/index.d.ts +87 -0
- package/dist/emit/index.js +95 -0
- package/dist/extensions/helix/kinds/tool.kind.yaml +240 -0
- package/dist/extensions/helix.js +9 -98
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/kernel/errors.d.ts +18 -0
- package/dist/kernel/errors.js +27 -0
- package/dist/kernel/kind-registry.js +3 -1
- package/dist/kernel/models.d.ts +0 -191
- package/dist/kernel/models.js +0 -32
- package/dist/package-scope.d.ts +18 -0
- package/dist/package-scope.js +87 -0
- package/dist/prompts.d.ts +27 -4
- package/dist/prompts.js +21 -6
- package/dist/tools.d.ts +41 -0
- package/dist/tools.js +88 -0
- package/package.json +1 -1
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* file:// <path> → FilesystemSource (read/write on disk)
|
|
11
11
|
* fs:// <path> → alias of file://
|
|
12
12
|
* <plain path> → treated as file://<path>
|
|
13
|
+
* pkg://<pkg>[/sub] → FilesystemSource (READ-ONLY) over a scope embedded
|
|
14
|
+
* as PACKAGE DATA of <pkg> (sub defaults to .dna);
|
|
15
|
+
* travels with the app (tarball / Docker)
|
|
13
16
|
* postgresql:// … → PostgresSource (node-postgres)
|
|
14
17
|
* postgres:// … → alias of postgresql://
|
|
15
18
|
* sqlite:// <path> → NOT SUPPORTED in the TS runtime (Python-only; the
|
|
@@ -16,6 +16,22 @@ function schemeOf(url) {
|
|
|
16
16
|
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url);
|
|
17
17
|
return (m ? m[1] : "file").toLowerCase();
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Split `pkg://<package>[/<subpath>]` into `{ pkg, subpath }`. Parity with the
|
|
21
|
+
* python `_parse_pkg_url`: `pkg://app` → `{pkg:"app", subpath:""}`;
|
|
22
|
+
* `pkg://app/.dna` → `{pkg:"app", subpath:".dna"}`. The package is the netloc
|
|
23
|
+
* (a dotted `pkg://my.app` stays `my.app`).
|
|
24
|
+
*/
|
|
25
|
+
function parsePkgUrl(url) {
|
|
26
|
+
const m = /^pkg:\/\/([^/]*)(?:\/(.*))?$/.exec(url);
|
|
27
|
+
const pkg = m?.[1] ?? "";
|
|
28
|
+
if (!pkg) {
|
|
29
|
+
throw new UnsupportedSourceScheme(`pkg:// source URL is missing a package name — use ` +
|
|
30
|
+
`pkg://<package>[/<subpath>] (e.g. pkg://app or pkg://app/.dna). ` +
|
|
31
|
+
`Got: ${url}`);
|
|
32
|
+
}
|
|
33
|
+
return { pkg, subpath: m?.[2] ?? "" };
|
|
34
|
+
}
|
|
19
35
|
/**
|
|
20
36
|
* Build a source from a scheme URL (see module docstring).
|
|
21
37
|
*
|
|
@@ -29,6 +45,16 @@ export async function sourceFromUrl(url, opts = {}) {
|
|
|
29
45
|
const { FilesystemSource } = await import("./filesystem/source.js");
|
|
30
46
|
return new FilesystemSource(urlToFsPath(url) || url);
|
|
31
47
|
}
|
|
48
|
+
if (scheme === "pkg") {
|
|
49
|
+
// A scope embedded as PACKAGE DATA — resolve it from inside the installed
|
|
50
|
+
// package so it travels with the app (tarball / Docker), no path
|
|
51
|
+
// navigation and no manual copy. READ-ONLY: FilesystemSource has no write
|
|
52
|
+
// surface here (to write, use file:// or postgresql://).
|
|
53
|
+
const { FilesystemSource } = await import("./filesystem/source.js");
|
|
54
|
+
const { anchorScopesRoot, DEFAULT_SUBPATH } = await import("../package-scope.js");
|
|
55
|
+
const { pkg, subpath } = parsePkgUrl(url);
|
|
56
|
+
return new FilesystemSource(anchorScopesRoot(pkg, subpath || DEFAULT_SUBPATH));
|
|
57
|
+
}
|
|
32
58
|
if (base === "postgresql" || base === "postgres") {
|
|
33
59
|
const { PostgresSource } = await import("./postgres/source.js");
|
|
34
60
|
const src = new PostgresSource({ connectionString: url, schema: opts.schema });
|
|
@@ -41,7 +67,8 @@ export async function sourceFromUrl(url, opts = {}) {
|
|
|
41
67
|
`(filesystem) or postgresql:// here. Got: ${url}`);
|
|
42
68
|
}
|
|
43
69
|
throw new UnsupportedSourceScheme(`unsupported source URL scheme '${scheme}://' — the TS runtime ships ` +
|
|
44
|
-
`file:// (filesystem)
|
|
70
|
+
`file:// (filesystem), pkg:// (read-only package-data scope) and ` +
|
|
71
|
+
`postgresql:// adapters. Got: ${url}`);
|
|
45
72
|
}
|
|
46
73
|
/**
|
|
47
74
|
* The `file://` URL the SDK falls back to with no explicit config.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EmitContext, EmitResult, EmitterPort } from "./index.js";
|
|
2
|
+
/** `concierge-grounded` → `ConciergeGrounded`. */
|
|
3
|
+
export declare function camel(name: string): string;
|
|
4
|
+
/** Split a DNA model coordinate into agent-framework `{id, provider}`. */
|
|
5
|
+
export declare function splitModel(model: string | null, providerHint?: string | null): {
|
|
6
|
+
id: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
} | null;
|
|
9
|
+
export declare class AgentFrameworkEmitter implements EmitterPort {
|
|
10
|
+
readonly target = "agent-framework";
|
|
11
|
+
readonly fileExtension = "agent.yaml";
|
|
12
|
+
/** The PURE de-para: {@link EmitContext} → the PromptAgent object. Field
|
|
13
|
+
* order is intentional and preserved by js-yaml (insertion order). */
|
|
14
|
+
toPromptAgent(ctx: EmitContext): Record<string, unknown>;
|
|
15
|
+
emit(ctx: EmitContext): EmitResult;
|
|
16
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → Microsoft **agent-framework** emitter (TS twin of python
|
|
3
|
+
* `dna.emit.agent_framework`). Materializes an {@link EmitContext} into the
|
|
4
|
+
* declarative `PromptAgent` YAML that `agent-framework-declarative`'s
|
|
5
|
+
* `AgentFactory` loads.
|
|
6
|
+
*
|
|
7
|
+
* The de-para (DNA field → PromptAgent field):
|
|
8
|
+
* metadata.name -> name (CamelCased id)
|
|
9
|
+
* metadata.description -> description
|
|
10
|
+
* Soul + guardrails + instruction -> instructions (flat — kernel-composed)
|
|
11
|
+
* spec.model (or Genome default_llm)-> model.{id, provider}
|
|
12
|
+
* spec.tools[] (Tool Kind surfaces) -> tools[] (kind: function)
|
|
13
|
+
* spec.output_schema -> outputSchema (only when present)
|
|
14
|
+
*
|
|
15
|
+
* `toPromptAgent` is the PURE de-para and is parity-critical: it must build the
|
|
16
|
+
* SAME object the Python `to_prompt_agent` builds from the same context.
|
|
17
|
+
*/
|
|
18
|
+
import yaml from "js-yaml";
|
|
19
|
+
/** DNA provider token → agent-framework `model.provider` value. Unknown tokens
|
|
20
|
+
* pass through unchanged so a future provider needs no code change. */
|
|
21
|
+
const PROVIDER_MAP = {
|
|
22
|
+
azure: "AzureOpenAI",
|
|
23
|
+
azureopenai: "AzureOpenAI",
|
|
24
|
+
azure_openai: "AzureOpenAI",
|
|
25
|
+
openai: "OpenAI",
|
|
26
|
+
anthropic: "Anthropic",
|
|
27
|
+
foundry: "AzureAIFoundry",
|
|
28
|
+
azureaifoundry: "AzureAIFoundry",
|
|
29
|
+
};
|
|
30
|
+
/** Bare model with no provider token and no `--provider` → AzureOpenAI (the
|
|
31
|
+
* provider the spike proved). Documented default, never silently wrong. */
|
|
32
|
+
const DEFAULT_PROVIDER = "AzureOpenAI";
|
|
33
|
+
/** `concierge-grounded` → `ConciergeGrounded`. */
|
|
34
|
+
export function camel(name) {
|
|
35
|
+
return String(name)
|
|
36
|
+
.replace(/_/g, "-")
|
|
37
|
+
.split("-")
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
40
|
+
.join("");
|
|
41
|
+
}
|
|
42
|
+
/** Split a DNA model coordinate into agent-framework `{id, provider}`. */
|
|
43
|
+
export function splitModel(model, providerHint) {
|
|
44
|
+
if (!model)
|
|
45
|
+
return null;
|
|
46
|
+
let token = null;
|
|
47
|
+
let ident = model;
|
|
48
|
+
for (const sep of [":", "/"]) {
|
|
49
|
+
const i = model.indexOf(sep);
|
|
50
|
+
if (i >= 0) {
|
|
51
|
+
token = model.slice(0, i);
|
|
52
|
+
ident = model.slice(i + 1);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let provider;
|
|
57
|
+
if (providerHint)
|
|
58
|
+
provider = providerHint;
|
|
59
|
+
else if (token)
|
|
60
|
+
provider = PROVIDER_MAP[token.trim().toLowerCase()] ?? token;
|
|
61
|
+
else
|
|
62
|
+
provider = DEFAULT_PROVIDER;
|
|
63
|
+
return { id: ident.trim(), provider };
|
|
64
|
+
}
|
|
65
|
+
function emitTools(tools) {
|
|
66
|
+
return tools.map((t) => {
|
|
67
|
+
const entry = {
|
|
68
|
+
name: t.name,
|
|
69
|
+
kind: "function", // AgentSchema function-tool kind (NOT `type`)
|
|
70
|
+
description: t.description ?? "",
|
|
71
|
+
};
|
|
72
|
+
if (t.parameters && Object.keys(t.parameters).length > 0) {
|
|
73
|
+
entry.parameters = t.parameters;
|
|
74
|
+
}
|
|
75
|
+
return entry;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export class AgentFrameworkEmitter {
|
|
79
|
+
target = "agent-framework";
|
|
80
|
+
fileExtension = "agent.yaml";
|
|
81
|
+
/** The PURE de-para: {@link EmitContext} → the PromptAgent object. Field
|
|
82
|
+
* order is intentional and preserved by js-yaml (insertion order). */
|
|
83
|
+
toPromptAgent(ctx) {
|
|
84
|
+
const providerHint = ctx.options?.provider ?? null;
|
|
85
|
+
const doc = { kind: "Prompt", name: camel(ctx.name) };
|
|
86
|
+
if (ctx.description)
|
|
87
|
+
doc.description = ctx.description;
|
|
88
|
+
const model = splitModel(ctx.model, providerHint);
|
|
89
|
+
if (model)
|
|
90
|
+
doc.model = model;
|
|
91
|
+
if (ctx.tools.length > 0)
|
|
92
|
+
doc.tools = emitTools(ctx.tools);
|
|
93
|
+
doc.instructions = ctx.instructions; // verbatim — the byte-equal gate
|
|
94
|
+
if (ctx.outputSchema)
|
|
95
|
+
doc.outputSchema = ctx.outputSchema;
|
|
96
|
+
return doc;
|
|
97
|
+
}
|
|
98
|
+
emit(ctx) {
|
|
99
|
+
const promptAgent = this.toPromptAgent(ctx);
|
|
100
|
+
const artifact = yaml.dump(promptAgent, { sortKeys: false, lineWidth: -1 });
|
|
101
|
+
const losses = [
|
|
102
|
+
"composition structure — Soul reuse + wired Guardrails flatten to one " +
|
|
103
|
+
"`instructions` string (no `soul:`/`guardrails:` slot in a PromptAgent)",
|
|
104
|
+
"tenant overlay — a per-tenant persona without a fork has no PromptAgent field",
|
|
105
|
+
"eval-as-contract — prompt invariants (EvalCases) have no PromptAgent slot",
|
|
106
|
+
];
|
|
107
|
+
if (ctx.model === null) {
|
|
108
|
+
losses.push("model unbound in DNA and none supplied — emitted PromptAgent has no " +
|
|
109
|
+
"`model:` block; pass provider/model or set spec.model / Genome default_llm");
|
|
110
|
+
}
|
|
111
|
+
const mapping = {
|
|
112
|
+
"metadata.name": "name (CamelCase)",
|
|
113
|
+
"metadata.description": "description",
|
|
114
|
+
"buildPrompt (Soul+guardrails+instruction)": "instructions",
|
|
115
|
+
"spec.model / Genome.default_llm": "model.{id,provider}",
|
|
116
|
+
"spec.tools[] (Tool Kind)": "tools[] (kind: function)",
|
|
117
|
+
"spec.output_schema": "outputSchema",
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
artifact,
|
|
121
|
+
target: this.target,
|
|
122
|
+
filename: `${ctx.name}.${this.fileExtension}`,
|
|
123
|
+
losses,
|
|
124
|
+
mapping,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `emit` — the vendor-neutral EMITTER layer (TS twin of python `dna.emit`).
|
|
3
|
+
*
|
|
4
|
+
* DNA authors an agent ONCE — a persona (Soul), an instruction (Agent), wired
|
|
5
|
+
* Guardrails, and Tools — as declarative Kinds. This module materializes that
|
|
6
|
+
* one neutral definition into the NATIVE artifact each runtime framework
|
|
7
|
+
* consumes. The concrete first step of "DNA as the Terraform of agents": author
|
|
8
|
+
* once, emit per runtime, swap runtimes without rewriting the agent.
|
|
9
|
+
*
|
|
10
|
+
* Parity: the pure de-para (`toPromptAgent` in `agentFramework.ts`) mirrors the
|
|
11
|
+
* Python emitter field-for-field; the serialization (js-yaml vs PyYAML) is a
|
|
12
|
+
* rendering detail, so the parity contract is the emitted OBJECT, not the bytes.
|
|
13
|
+
*
|
|
14
|
+
* Shape: {@link EmitContext} (runtime-agnostic view of a composed agent) →
|
|
15
|
+
* {@link EmitterPort} (a target) → {@link EmitResult} (artifact + honest
|
|
16
|
+
* `losses`). {@link registerEmitter}/{@link getEmitter}/{@link availableTargets}
|
|
17
|
+
* are the pluggable registry — a new target is a class + one register call.
|
|
18
|
+
*/
|
|
19
|
+
import type { ManifestInstance } from "../kernel/instance.js";
|
|
20
|
+
/** Runtime-agnostic view of ONE composed DNA agent. */
|
|
21
|
+
export interface EmitContext {
|
|
22
|
+
/** The DNA agent slug (`metadata.name`). */
|
|
23
|
+
name: string;
|
|
24
|
+
/** `metadata.description`, or "". */
|
|
25
|
+
description: string;
|
|
26
|
+
/** DNA-composed system prompt (Soul + guardrails + instruction, flat). The
|
|
27
|
+
* byte-equal gate: an emitter MUST carry it verbatim. */
|
|
28
|
+
instructions: string;
|
|
29
|
+
/** Raw DNA model coordinate (`openai:gpt-4o-mini` / `azure/gpt-4o` / bare), or
|
|
30
|
+
* null when the DNA leaves the model unbound. */
|
|
31
|
+
model: string | null;
|
|
32
|
+
/** Resolved tool surfaces (`spec.tools` → Tool Kind). */
|
|
33
|
+
tools: EmitTool[];
|
|
34
|
+
/** Optional response JSON Schema (`spec.output_schema`), or null. */
|
|
35
|
+
outputSchema: Record<string, unknown> | null;
|
|
36
|
+
/** Scope the agent was composed from (provenance). */
|
|
37
|
+
scope: string | null;
|
|
38
|
+
/** Per-emitter hints (e.g. a `--provider` override). */
|
|
39
|
+
options: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
export interface EmitTool {
|
|
42
|
+
name: string;
|
|
43
|
+
description: string;
|
|
44
|
+
parameters: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
/** The emitted native artifact + an honest account of the de-para. */
|
|
47
|
+
export interface EmitResult {
|
|
48
|
+
/** The serialized native artifact. */
|
|
49
|
+
artifact: string;
|
|
50
|
+
/** The target runtime id. */
|
|
51
|
+
target: string;
|
|
52
|
+
/** Suggested filename. */
|
|
53
|
+
filename: string;
|
|
54
|
+
/** DNA axes with NO slot in this target — what did NOT survive the emit. */
|
|
55
|
+
losses: string[];
|
|
56
|
+
/** Field-level de-para (`dnaField -> targetField`). */
|
|
57
|
+
mapping: Record<string, string>;
|
|
58
|
+
}
|
|
59
|
+
/** A runtime emitter — pure: reads an {@link EmitContext}, returns an
|
|
60
|
+
* {@link EmitResult}. No kernel I/O, no network. */
|
|
61
|
+
export interface EmitterPort {
|
|
62
|
+
readonly target: string;
|
|
63
|
+
readonly fileExtension: string;
|
|
64
|
+
emit(ctx: EmitContext): EmitResult;
|
|
65
|
+
}
|
|
66
|
+
export declare class EmitError extends Error {
|
|
67
|
+
}
|
|
68
|
+
export declare class UnknownTarget extends EmitError {
|
|
69
|
+
readonly target: string;
|
|
70
|
+
readonly available: string[];
|
|
71
|
+
constructor(target: string, available: string[]);
|
|
72
|
+
}
|
|
73
|
+
/** Register an emitter under its `target` (last registration wins). */
|
|
74
|
+
export declare function registerEmitter(emitter: EmitterPort): EmitterPort;
|
|
75
|
+
/** Look up a registered emitter or throw {@link UnknownTarget}. */
|
|
76
|
+
export declare function getEmitter(target: string): Promise<EmitterPort>;
|
|
77
|
+
/** Sorted list of registered target ids. */
|
|
78
|
+
export declare function availableTargets(): Promise<string[]>;
|
|
79
|
+
export interface BuildEmitContextOpts {
|
|
80
|
+
model?: string | null;
|
|
81
|
+
provider?: string | null;
|
|
82
|
+
}
|
|
83
|
+
/** Compose a DNA agent through the kernel and project it to an
|
|
84
|
+
* {@link EmitContext}. `mi` is a live ManifestInstance. */
|
|
85
|
+
export declare function buildEmitContext(mi: ManifestInstance, agent: string, opts?: BuildEmitContextOpts): Promise<EmitContext>;
|
|
86
|
+
/** Compose `agent` from `mi` and emit it for `target`. */
|
|
87
|
+
export declare function emitAgent(mi: ManifestInstance, agent: string, target: string, opts?: BuildEmitContextOpts): Promise<EmitResult>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ToolLibrary } from "../tools.js";
|
|
2
|
+
export class EmitError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export class UnknownTarget extends EmitError {
|
|
5
|
+
target;
|
|
6
|
+
available;
|
|
7
|
+
constructor(target, available) {
|
|
8
|
+
super(`no emitter registered for target '${target}'; available: ` +
|
|
9
|
+
(available.join(", ") || "(none)"));
|
|
10
|
+
this.target = target;
|
|
11
|
+
this.available = available;
|
|
12
|
+
this.name = "UnknownTarget";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// ── registry ──────────────────────────────────────────────────────────────
|
|
16
|
+
const EMITTER_REGISTRY = new Map();
|
|
17
|
+
let builtinsWired = false;
|
|
18
|
+
async function ensureBuiltins() {
|
|
19
|
+
if (builtinsWired)
|
|
20
|
+
return;
|
|
21
|
+
builtinsWired = true;
|
|
22
|
+
const { AgentFrameworkEmitter } = await import("./agentFramework.js");
|
|
23
|
+
const e = new AgentFrameworkEmitter();
|
|
24
|
+
if (!EMITTER_REGISTRY.has(e.target))
|
|
25
|
+
EMITTER_REGISTRY.set(e.target, e);
|
|
26
|
+
}
|
|
27
|
+
/** Register an emitter under its `target` (last registration wins). */
|
|
28
|
+
export function registerEmitter(emitter) {
|
|
29
|
+
EMITTER_REGISTRY.set(emitter.target, emitter);
|
|
30
|
+
return emitter;
|
|
31
|
+
}
|
|
32
|
+
/** Look up a registered emitter or throw {@link UnknownTarget}. */
|
|
33
|
+
export async function getEmitter(target) {
|
|
34
|
+
await ensureBuiltins();
|
|
35
|
+
const e = EMITTER_REGISTRY.get(target);
|
|
36
|
+
if (!e)
|
|
37
|
+
throw new UnknownTarget(target, await availableTargets());
|
|
38
|
+
return e;
|
|
39
|
+
}
|
|
40
|
+
/** Sorted list of registered target ids. */
|
|
41
|
+
export async function availableTargets() {
|
|
42
|
+
await ensureBuiltins();
|
|
43
|
+
return [...EMITTER_REGISTRY.keys()].sort();
|
|
44
|
+
}
|
|
45
|
+
/** Compose a DNA agent through the kernel and project it to an
|
|
46
|
+
* {@link EmitContext}. `mi` is a live ManifestInstance. */
|
|
47
|
+
export async function buildEmitContext(mi, agent, opts = {}) {
|
|
48
|
+
const doc = mi.findAgent(agent);
|
|
49
|
+
if (!doc) {
|
|
50
|
+
throw new EmitError(`agent '${agent}' not found in scope '${mi.scope ?? "?"}'`);
|
|
51
|
+
}
|
|
52
|
+
const spec = (doc.spec ?? {});
|
|
53
|
+
const meta = (doc.metadata ?? {});
|
|
54
|
+
const description = meta.description ?? "";
|
|
55
|
+
const instructions = await mi.buildPrompt({ agent });
|
|
56
|
+
let resolvedModel = opts.model ?? spec.model ?? null;
|
|
57
|
+
if (!resolvedModel) {
|
|
58
|
+
const root = mi.root;
|
|
59
|
+
const rootSpec = (root?.spec ?? {});
|
|
60
|
+
resolvedModel = rootSpec.default_llm ?? null;
|
|
61
|
+
}
|
|
62
|
+
const tools = resolveTools(mi, spec);
|
|
63
|
+
const outputSchema = spec.output_schema ?? null;
|
|
64
|
+
return {
|
|
65
|
+
name: doc.name ?? agent,
|
|
66
|
+
description,
|
|
67
|
+
instructions,
|
|
68
|
+
model: resolvedModel,
|
|
69
|
+
tools,
|
|
70
|
+
outputSchema,
|
|
71
|
+
scope: mi.scope ?? null,
|
|
72
|
+
options: opts.provider ? { provider: opts.provider } : {},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function resolveTools(mi, spec) {
|
|
76
|
+
const names = spec.tools;
|
|
77
|
+
if (!names || names.length === 0)
|
|
78
|
+
return [];
|
|
79
|
+
const lib = new ToolLibrary(mi);
|
|
80
|
+
return names.map((name) => {
|
|
81
|
+
const surface = lib.get(name); // throws ToolNotFound (fail-loud)
|
|
82
|
+
return {
|
|
83
|
+
name,
|
|
84
|
+
description: surface.description,
|
|
85
|
+
parameters: { ...surface.parameters },
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// ── high-level surface ──────────────────────────────────────────────────────
|
|
90
|
+
/** Compose `agent` from `mi` and emit it for `target`. */
|
|
91
|
+
export async function emitAgent(mi, agent, target, opts = {}) {
|
|
92
|
+
const emitter = await getEmitter(target);
|
|
93
|
+
const ctx = await buildEmitContext(mi, agent, opts);
|
|
94
|
+
return emitter.emit(ctx);
|
|
95
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Tool — a declarative, invocable capability an agent can call (record plane).
|
|
2
|
+
#
|
|
3
|
+
# "Tools as data" (f-dna-tools-as-data). The DNA already governs persona,
|
|
4
|
+
# instruction and guardrails declaratively; a Tool moves the AGENT-FACING
|
|
5
|
+
# surface of a tool into the same declarative plane — the ``description`` the
|
|
6
|
+
# model reads to decide whether to call it (``metadata.description``) and the
|
|
7
|
+
# ``parameters`` JSON Schema of its arguments (``spec.input_schema``). One
|
|
8
|
+
# source of truth, versioned, testable, and overridable per tenant, served
|
|
9
|
+
# identically to a Python backend (`@tool`) and a TypeScript frontend
|
|
10
|
+
# (CopilotKit `useCopilotAction`) via ``dna.load_tools`` / ``loadTools``.
|
|
11
|
+
#
|
|
12
|
+
# F3 migration (s-tool-kind-descriptor): this WAS a hand-written ``ToolKind``
|
|
13
|
+
# class (helix/__init__.py + helix.ts) with a ``TypedTool``/``ToolSpec``
|
|
14
|
+
# model. It is now a ``kinds/tool.kind.yaml`` descriptor — a record Kind
|
|
15
|
+
# expressed as data, per the repo's own ratchet (record Kinds are
|
|
16
|
+
# descriptors, never classes). The invocation surface (type/endpoint/mcp/
|
|
17
|
+
# python/shell + input/output schema + auth + read_only/requires_confirmation)
|
|
18
|
+
# is preserved verbatim; only the IMPLEMENTATION moved class → descriptor and
|
|
19
|
+
# the plane moved composition → record (a Tool is not a prompt target and
|
|
20
|
+
# never composes into an agent prompt, so it carries no composition signal —
|
|
21
|
+
# writing a Tool no longer invalidates the composition schema cache).
|
|
22
|
+
#
|
|
23
|
+
# Agents reference Tools by name via ``dep_filters.tools`` (unchanged — the
|
|
24
|
+
# alias ``helix-tool`` is stable). Stored as ``tools/<name>.yaml`` —
|
|
25
|
+
# marketplace-shareable as standalone bundles.
|
|
26
|
+
#
|
|
27
|
+
# tenant_scope intentionally NOT declared — permissive (base Tool + optional
|
|
28
|
+
# per-tenant overlay). A tenant may legitimately override a tool's
|
|
29
|
+
# description/parameters (the SaaS value hook); the base stays intact and the
|
|
30
|
+
# máxima "inheritable ⇒ never tenanted" holds.
|
|
31
|
+
#
|
|
32
|
+
# PARITY-CRITICAL package data: byte-identical mirror under both runtimes'
|
|
33
|
+
# helix/kinds/ (tests/test_descriptor_hash_parity.py enforces).
|
|
34
|
+
apiVersion: github.com/ruinosus/dna/core/v1
|
|
35
|
+
kind: KindDefinition
|
|
36
|
+
metadata:
|
|
37
|
+
name: tool
|
|
38
|
+
spec:
|
|
39
|
+
target_api_version: github.com/ruinosus/dna/v1
|
|
40
|
+
target_kind: Tool
|
|
41
|
+
alias: helix-tool
|
|
42
|
+
origin: github.com/ruinosus/dna/tool
|
|
43
|
+
plane: record
|
|
44
|
+
prompt_target: false
|
|
45
|
+
flatten_in_context: false
|
|
46
|
+
prompt_target_priority: 0
|
|
47
|
+
storage:
|
|
48
|
+
type: yaml
|
|
49
|
+
container: tools
|
|
50
|
+
schema:
|
|
51
|
+
type: object
|
|
52
|
+
properties:
|
|
53
|
+
type:
|
|
54
|
+
type: string
|
|
55
|
+
enum:
|
|
56
|
+
- http
|
|
57
|
+
- mcp
|
|
58
|
+
- python
|
|
59
|
+
- shell
|
|
60
|
+
- builtin
|
|
61
|
+
description: How the tool is executed. builtin | http | mcp | python
|
|
62
|
+
| shell.
|
|
63
|
+
endpoint:
|
|
64
|
+
type: string
|
|
65
|
+
description: URL called when type=http. Supports {placeholder}
|
|
66
|
+
templating.
|
|
67
|
+
method:
|
|
68
|
+
type: string
|
|
69
|
+
description: HTTP method when type=http (default POST).
|
|
70
|
+
mcp_server:
|
|
71
|
+
type: string
|
|
72
|
+
description: MCP server name when type=mcp.
|
|
73
|
+
mcp_tool:
|
|
74
|
+
type: string
|
|
75
|
+
description: Tool name on the MCP server when type=mcp.
|
|
76
|
+
python_module:
|
|
77
|
+
type: string
|
|
78
|
+
description: Dotted import path when type=python.
|
|
79
|
+
python_callable:
|
|
80
|
+
type: string
|
|
81
|
+
description: Attribute on the module (function or class) when
|
|
82
|
+
type=python.
|
|
83
|
+
shell_command:
|
|
84
|
+
type: string
|
|
85
|
+
description: Command template when type=shell. Never executed without
|
|
86
|
+
confirmation.
|
|
87
|
+
input_schema:
|
|
88
|
+
type: object
|
|
89
|
+
description: JSON Schema of the arguments the agent passes when
|
|
90
|
+
invoking the tool — the "parameters" the model fills in. Surfaced
|
|
91
|
+
as ``parameters`` by ``dna.load_tools`` / ``loadTools``.
|
|
92
|
+
output_schema:
|
|
93
|
+
type: object
|
|
94
|
+
description: JSON Schema describing the shape of the tool's response.
|
|
95
|
+
auth_type:
|
|
96
|
+
type: string
|
|
97
|
+
enum:
|
|
98
|
+
- none
|
|
99
|
+
- api_key
|
|
100
|
+
- bearer
|
|
101
|
+
- oauth2
|
|
102
|
+
description: Credential strategy for the invocation.
|
|
103
|
+
auth_env_var:
|
|
104
|
+
type: string
|
|
105
|
+
description: Environment variable holding the credential (e.g.
|
|
106
|
+
GITHUB_TOKEN).
|
|
107
|
+
read_only:
|
|
108
|
+
type: boolean
|
|
109
|
+
description: False = the tool may mutate state (DB writes, file
|
|
110
|
+
changes, external side effects).
|
|
111
|
+
requires_confirmation:
|
|
112
|
+
type: boolean
|
|
113
|
+
description: Force user approval before each invocation.
|
|
114
|
+
tags:
|
|
115
|
+
type: array
|
|
116
|
+
items:
|
|
117
|
+
type: string
|
|
118
|
+
description: Free-form labels for filtering and search.
|
|
119
|
+
examples:
|
|
120
|
+
type: array
|
|
121
|
+
items:
|
|
122
|
+
type: object
|
|
123
|
+
description: Usage examples ([{input, output}]).
|
|
124
|
+
ui:
|
|
125
|
+
mode: build
|
|
126
|
+
label:
|
|
127
|
+
en: Tools
|
|
128
|
+
pt-BR: Ferramentas
|
|
129
|
+
description:
|
|
130
|
+
en: '@tool functions available to agents.'
|
|
131
|
+
pt-BR: Ferramentas (@tool) disponíveis aos agentes.
|
|
132
|
+
routes:
|
|
133
|
+
list: docs/Tool
|
|
134
|
+
detail: docs/Tool/:name
|
|
135
|
+
permissions:
|
|
136
|
+
list: any
|
|
137
|
+
detail: any
|
|
138
|
+
in_sidebar: true
|
|
139
|
+
display_order: 53
|
|
140
|
+
ui_schema:
|
|
141
|
+
type:
|
|
142
|
+
widget: select
|
|
143
|
+
label: Invocation type
|
|
144
|
+
help: 'How the tool is executed: http | mcp | python | shell | builtin.'
|
|
145
|
+
order: 10
|
|
146
|
+
endpoint:
|
|
147
|
+
widget: text
|
|
148
|
+
label: HTTP endpoint
|
|
149
|
+
help: URL called when type=http. Supports {placeholder} templating.
|
|
150
|
+
order: 20
|
|
151
|
+
method:
|
|
152
|
+
widget: select
|
|
153
|
+
label: HTTP method
|
|
154
|
+
order: 25
|
|
155
|
+
mcp_server:
|
|
156
|
+
widget: text
|
|
157
|
+
label: MCP server
|
|
158
|
+
help: Server name when type=mcp.
|
|
159
|
+
order: 30
|
|
160
|
+
mcp_tool:
|
|
161
|
+
widget: text
|
|
162
|
+
label: MCP tool name
|
|
163
|
+
order: 35
|
|
164
|
+
python_module:
|
|
165
|
+
widget: text
|
|
166
|
+
label: Python module
|
|
167
|
+
help: Dotted import path when type=python.
|
|
168
|
+
order: 40
|
|
169
|
+
python_callable:
|
|
170
|
+
widget: text
|
|
171
|
+
label: Python callable
|
|
172
|
+
help: Attribute on the module (function or class).
|
|
173
|
+
order: 45
|
|
174
|
+
shell_command:
|
|
175
|
+
widget: textarea
|
|
176
|
+
label: Shell command
|
|
177
|
+
help: Command template when type=shell. Never executed without
|
|
178
|
+
confirmation.
|
|
179
|
+
order: 50
|
|
180
|
+
input_schema:
|
|
181
|
+
widget: code
|
|
182
|
+
language: yaml
|
|
183
|
+
label: Input schema (JSON Schema)
|
|
184
|
+
help: Validates the arguments the agent passes when invoking the tool.
|
|
185
|
+
height: 260
|
|
186
|
+
order: 60
|
|
187
|
+
output_schema:
|
|
188
|
+
widget: code
|
|
189
|
+
language: yaml
|
|
190
|
+
label: Output schema (JSON Schema)
|
|
191
|
+
help: Describes the shape of the tool's response.
|
|
192
|
+
height: 220
|
|
193
|
+
order: 70
|
|
194
|
+
auth_type:
|
|
195
|
+
widget: select
|
|
196
|
+
label: Auth type
|
|
197
|
+
help: none | api_key | bearer | oauth2.
|
|
198
|
+
order: 80
|
|
199
|
+
auth_env_var:
|
|
200
|
+
widget: text
|
|
201
|
+
label: Auth env var
|
|
202
|
+
help: Environment variable holding the credential.
|
|
203
|
+
order: 85
|
|
204
|
+
read_only:
|
|
205
|
+
widget: checkbox
|
|
206
|
+
label: Read-only
|
|
207
|
+
help: Uncheck if the tool mutates state (database writes, file changes,
|
|
208
|
+
external side effects).
|
|
209
|
+
order: 90
|
|
210
|
+
requires_confirmation:
|
|
211
|
+
widget: checkbox
|
|
212
|
+
label: Requires confirmation
|
|
213
|
+
help: Force user approval before each invocation.
|
|
214
|
+
order: 95
|
|
215
|
+
tags:
|
|
216
|
+
widget: tags
|
|
217
|
+
label: Tags
|
|
218
|
+
order: 100
|
|
219
|
+
examples:
|
|
220
|
+
widget: readonly
|
|
221
|
+
label: Examples
|
|
222
|
+
help: Usage examples. Nested list — edit via YAML for now.
|
|
223
|
+
order: 110
|
|
224
|
+
graph_style:
|
|
225
|
+
fill: '#14B8A6'
|
|
226
|
+
stroke: '#0D9488'
|
|
227
|
+
text_color: '#fff'
|
|
228
|
+
ascii_icon: 🔧
|
|
229
|
+
display_label: Tools
|
|
230
|
+
docs: A Tool is a declarative, invocable capability an agent can call — an
|
|
231
|
+
HTTP endpoint, an MCP server tool, a Python callable, a shell command, or
|
|
232
|
+
a builtin. It bridges DNA with OpenAI/Anthropic tool-calling conventions.
|
|
233
|
+
The agent-facing surface is its ``metadata.description`` (the text the
|
|
234
|
+
model reads to decide to call it) and its ``spec.input_schema`` (the
|
|
235
|
+
"parameters" JSON Schema of the arguments); ``dna.load_tools`` /
|
|
236
|
+
``loadTools`` serve exactly that surface, identically to Python and
|
|
237
|
+
TypeScript consumers from this one source. It also declares an auth
|
|
238
|
+
strategy and read_only / requires_confirmation flags the host honors at
|
|
239
|
+
runtime. Agents reference Tools via ``dep_filters.tools``. Stored as
|
|
240
|
+
``tools/<name>.yaml`` — marketplace-shareable as standalone bundles.
|