dna-sdk 0.8.0 → 0.10.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/config.d.ts +7 -0
- package/dist/config.js +10 -1
- package/dist/emit/agentFramework.d.ts +2 -0
- package/dist/emit/agentFramework.js +6 -0
- package/dist/emit/agno.d.ts +25 -0
- package/dist/emit/agno.js +65 -0
- package/dist/emit/bedrock.d.ts +3 -0
- package/dist/emit/bedrock.js +9 -0
- package/dist/emit/deepagents.d.ts +25 -0
- package/dist/emit/deepagents.js +71 -0
- package/dist/emit/index.d.ts +33 -2
- package/dist/emit/index.js +20 -1
- package/dist/emit/langgraph.d.ts +26 -0
- package/dist/emit/langgraph.js +70 -0
- package/dist/emit/openaiAgents.d.ts +26 -0
- package/dist/emit/openaiAgents.js +91 -0
- package/dist/emit/scaffold.d.ts +65 -0
- package/dist/emit/scaffold.js +179 -0
- package/dist/emit/scaffolds/agno/prompt-only.py.tmpl +15 -0
- package/dist/emit/scaffolds/agno/with-tools.py.tmpl +23 -0
- package/dist/emit/scaffolds/deepagents/prompt-only.py.tmpl +16 -0
- package/dist/emit/scaffolds/deepagents/with-tools.py.tmpl +22 -0
- package/dist/emit/scaffolds/langgraph/prompt-only.py.tmpl +16 -0
- package/dist/emit/scaffolds/langgraph/with-tools.py.tmpl +25 -0
- package/dist/emit/scaffolds/openai-agents/prompt-only.py.tmpl +15 -0
- package/dist/emit/scaffolds/openai-agents/with-tools.py.tmpl +24 -0
- package/dist/emit/vertex.d.ts +22 -0
- package/dist/emit/vertex.js +162 -0
- package/dist/extensions/sdlc.js +3 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export interface DnaConfig {
|
|
|
11
11
|
search: SearchMode;
|
|
12
12
|
/** Embedding provider selector (default `off`). */
|
|
13
13
|
embedding: EmbeddingMode;
|
|
14
|
+
/**
|
|
15
|
+
* Opaque passthrough of the `auth:` section — the SDK only checks it is a
|
|
16
|
+
* mapping; its detailed schema (`providers[]` — the pluggable N-provider IdP
|
|
17
|
+
* layer of the MCP runtime face) is owned by the consumer (the CLI's
|
|
18
|
+
* `_mcp_auth.parse_auth_providers`). `null` when the file has no `auth:`.
|
|
19
|
+
*/
|
|
20
|
+
auth: Record<string, unknown> | null;
|
|
14
21
|
/** Where it was loaded from (`null` for a synthesized default). */
|
|
15
22
|
path: string | null;
|
|
16
23
|
}
|
package/dist/config.js
CHANGED
|
@@ -25,7 +25,7 @@ import yaml from "js-yaml";
|
|
|
25
25
|
export const CONFIG_FILENAME = "dna.config.yaml";
|
|
26
26
|
const VALID_SEARCH = ["off", "pgvector", "sqlite-vec"];
|
|
27
27
|
const VALID_EMBEDDING = ["off", "fake", "onnx"];
|
|
28
|
-
const KNOWN_KEYS = new Set(["source", "search", "embedding"]);
|
|
28
|
+
const KNOWN_KEYS = new Set(["source", "search", "embedding", "auth"]);
|
|
29
29
|
/**
|
|
30
30
|
* Path to `dna.config.yaml` in `start` (default: cwd), or `null` if absent.
|
|
31
31
|
* Deliberately NOT a walk-up — a config's meaning is tied to the boot dir.
|
|
@@ -93,10 +93,19 @@ function parse(raw, path) {
|
|
|
93
93
|
throw new Error(`${path}: \`embedding: ${embedding}\` is not valid — choose one of ` +
|
|
94
94
|
`${JSON.stringify(VALID_EMBEDDING)}.`);
|
|
95
95
|
}
|
|
96
|
+
const rawAuth = obj.auth;
|
|
97
|
+
if (rawAuth !== undefined &&
|
|
98
|
+
rawAuth !== null &&
|
|
99
|
+
(typeof rawAuth !== "object" || Array.isArray(rawAuth))) {
|
|
100
|
+
throw new Error(`${path}: \`auth:\` must be a mapping (its \`providers:\` list configures ` +
|
|
101
|
+
`the MCP IdP layer).`);
|
|
102
|
+
}
|
|
103
|
+
const auth = (rawAuth ?? null);
|
|
96
104
|
return {
|
|
97
105
|
source,
|
|
98
106
|
search: search,
|
|
99
107
|
embedding: embedding,
|
|
108
|
+
auth,
|
|
100
109
|
path,
|
|
101
110
|
};
|
|
102
111
|
}
|
|
@@ -13,4 +13,6 @@ export declare class AgentFrameworkEmitter implements EmitterPort {
|
|
|
13
13
|
* order is intentional and preserved by js-yaml (insertion order). */
|
|
14
14
|
toPromptAgent(ctx: EmitContext): Record<string, unknown>;
|
|
15
15
|
emit(ctx: EmitContext): EmitResult;
|
|
16
|
+
/** Byte-equal invariant hook: read `instructions` back from the emitted YAML. */
|
|
17
|
+
extractInstructions(artifact: string): string | null;
|
|
16
18
|
}
|
|
@@ -124,4 +124,10 @@ export class AgentFrameworkEmitter {
|
|
|
124
124
|
mapping,
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
|
+
/** Byte-equal invariant hook: read `instructions` back from the emitted YAML. */
|
|
128
|
+
extractInstructions(artifact) {
|
|
129
|
+
const doc = yaml.load(artifact);
|
|
130
|
+
const value = doc?.instructions;
|
|
131
|
+
return typeof value === "string" ? value : null;
|
|
132
|
+
}
|
|
127
133
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → **Agno** emitter (code-first, `agno.agent`; TS twin of python
|
|
3
|
+
* `dna.emit.agno`).
|
|
4
|
+
*
|
|
5
|
+
* Agno is code-first — you construct `Agent(name=..., model=..., instructions=...,
|
|
6
|
+
* tools=[...])`, there is no declarative agent file to map onto. So this target is a
|
|
7
|
+
* {@link ScaffoldEmitter}: it fills a curated `{agno × case}` template rather than
|
|
8
|
+
* generating code ad-hoc, and the emitted `INSTRUCTIONS` constant is byte-equal to
|
|
9
|
+
* the DNA-composed prompt.
|
|
10
|
+
*
|
|
11
|
+
* Model: the DNA coordinate is PRESERVED as a string (Agno accepts a `provider:model`
|
|
12
|
+
* string). Cases: `prompt-only` (no tools) and `with-tools` (one plain-function stub
|
|
13
|
+
* per DNA Tool — Agno auto-wraps callables as tools). `structured-output` (Agno's
|
|
14
|
+
* `output_schema`) is not shipped yet and falls back with a recorded loss.
|
|
15
|
+
*/
|
|
16
|
+
import { ScaffoldEmitter, type ScaffoldChoice } from "./scaffold.js";
|
|
17
|
+
import type { EmitContext } from "./index.js";
|
|
18
|
+
export declare class AgnoEmitter extends ScaffoldEmitter {
|
|
19
|
+
readonly framework = "agno";
|
|
20
|
+
readonly target = "agno";
|
|
21
|
+
readonly fileExtension = "py";
|
|
22
|
+
renderContext(ctx: EmitContext, _case: string): Record<string, unknown>;
|
|
23
|
+
losses(ctx: EmitContext, _choice: ScaffoldChoice): string[];
|
|
24
|
+
mapping(): Record<string, string>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → **Agno** emitter (code-first, `agno.agent`; TS twin of python
|
|
3
|
+
* `dna.emit.agno`).
|
|
4
|
+
*
|
|
5
|
+
* Agno is code-first — you construct `Agent(name=..., model=..., instructions=...,
|
|
6
|
+
* tools=[...])`, there is no declarative agent file to map onto. So this target is a
|
|
7
|
+
* {@link ScaffoldEmitter}: it fills a curated `{agno × case}` template rather than
|
|
8
|
+
* generating code ad-hoc, and the emitted `INSTRUCTIONS` constant is byte-equal to
|
|
9
|
+
* the DNA-composed prompt.
|
|
10
|
+
*
|
|
11
|
+
* Model: the DNA coordinate is PRESERVED as a string (Agno accepts a `provider:model`
|
|
12
|
+
* string). Cases: `prompt-only` (no tools) and `with-tools` (one plain-function stub
|
|
13
|
+
* per DNA Tool — Agno auto-wraps callables as tools). `structured-output` (Agno's
|
|
14
|
+
* `output_schema`) is not shipped yet and falls back with a recorded loss.
|
|
15
|
+
*/
|
|
16
|
+
import { pyIdentifier, pyStrLiteral, ScaffoldEmitter } from "./scaffold.js";
|
|
17
|
+
export class AgnoEmitter extends ScaffoldEmitter {
|
|
18
|
+
framework = "agno";
|
|
19
|
+
target = "agno";
|
|
20
|
+
fileExtension = "py";
|
|
21
|
+
renderContext(ctx, _case) {
|
|
22
|
+
const tools = ctx.tools.map((t) => ({
|
|
23
|
+
name: t.name,
|
|
24
|
+
func_name: pyIdentifier(t.name),
|
|
25
|
+
docstring_literal: pyStrLiteral(t.description || t.name),
|
|
26
|
+
}));
|
|
27
|
+
return {
|
|
28
|
+
has_model: ctx.model !== null,
|
|
29
|
+
model_literal: ctx.model ? pyStrLiteral(ctx.model) : "",
|
|
30
|
+
tools,
|
|
31
|
+
tool_list: tools.map((t) => t.func_name).join(", "),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
losses(ctx, _choice) {
|
|
35
|
+
const out = [];
|
|
36
|
+
if (ctx.tools.length > 0) {
|
|
37
|
+
out.push("tool body — each tool is a scaffolded STUB (a bare callable + " +
|
|
38
|
+
"`raise NotImplementedError`); its real implementation and typed signature " +
|
|
39
|
+
"must be wired (Agno derives the tool schema from the function signature + " +
|
|
40
|
+
"docstring)");
|
|
41
|
+
}
|
|
42
|
+
if (ctx.model === null) {
|
|
43
|
+
out.push("model unbound in DNA and none supplied — emitted `Agent(...)` has no " +
|
|
44
|
+
"`model=`; supply one at wire-up (Agno requires a model)");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
out.push("model coordinate — the DNA coordinate is carried verbatim as a string; this " +
|
|
48
|
+
"assumes Agno's string-model resolution. A specific `Model` object (e.g. " +
|
|
49
|
+
"`OpenAIChat(id=...)`) is the alternative at wire-up");
|
|
50
|
+
}
|
|
51
|
+
if (ctx.outputSchema) {
|
|
52
|
+
out.push("output_schema — map DNA's `spec.output_schema` to `Agent(output_schema=...)` " +
|
|
53
|
+
"(a Pydantic model) by hand; the scaffold does not synthesize the class");
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
mapping() {
|
|
58
|
+
return {
|
|
59
|
+
"buildPrompt (Soul+guardrails+instruction)": "INSTRUCTIONS constant (byte-equal)",
|
|
60
|
+
"metadata.name": "Agent(name=...)",
|
|
61
|
+
"spec.model / Genome.default_llm": "Agent(model=...) (DNA coordinate preserved)",
|
|
62
|
+
"spec.tools[] (Tool Kind)": "plain-function stubs → Agent(tools=[...])",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/emit/bedrock.d.ts
CHANGED
|
@@ -44,4 +44,7 @@ export declare class BedrockEmitter implements EmitterPort {
|
|
|
44
44
|
* order is intentional and preserved by JSON.stringify (insertion order). */
|
|
45
45
|
toTemplate(ctx: EmitContext): Record<string, unknown>;
|
|
46
46
|
emit(ctx: EmitContext): EmitResult;
|
|
47
|
+
/** Byte-equal invariant hook: read `Properties.Instruction` back from the
|
|
48
|
+
* emitted CloudFormation template. */
|
|
49
|
+
extractInstructions(artifact: string): string | null;
|
|
47
50
|
}
|
package/dist/emit/bedrock.js
CHANGED
|
@@ -166,4 +166,13 @@ export class BedrockEmitter {
|
|
|
166
166
|
mapping,
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
|
+
/** Byte-equal invariant hook: read `Properties.Instruction` back from the
|
|
170
|
+
* emitted CloudFormation template. */
|
|
171
|
+
extractInstructions(artifact) {
|
|
172
|
+
const template = JSON.parse(artifact);
|
|
173
|
+
const resources = template.Resources ?? {};
|
|
174
|
+
const first = Object.values(resources)[0];
|
|
175
|
+
const value = first?.Properties?.Instruction;
|
|
176
|
+
return typeof value === "string" ? value : null;
|
|
177
|
+
}
|
|
169
178
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → **DeepAgents** emitter (code-first, `deepagents`; TS twin of python
|
|
3
|
+
* `dna.emit.deepagents`).
|
|
4
|
+
*
|
|
5
|
+
* DeepAgents (the LangChain "batteries-included agent harness") is code-first — you
|
|
6
|
+
* call `create_deep_agent(model=..., tools=[...], system_prompt="...")`, there is no
|
|
7
|
+
* declarative agent file to map onto. So this target is a {@link ScaffoldEmitter}: it
|
|
8
|
+
* fills a curated `{deepagents × case}` template rather than generating code ad-hoc,
|
|
9
|
+
* and the emitted `INSTRUCTIONS` constant is byte-equal to the DNA-composed prompt.
|
|
10
|
+
*
|
|
11
|
+
* `system_prompt` sits in FRONT of the deep-agent's built-in harness prompt (the
|
|
12
|
+
* planning / filesystem / sub-agent scaffolding the framework appends) — so the DNA
|
|
13
|
+
* prompt is a PREFIX of the effective system prompt. Model: the DNA coordinate is
|
|
14
|
+
* PRESERVED (resolved via `init_chat_model`). Cases: `prompt-only` and `with-tools`.
|
|
15
|
+
*/
|
|
16
|
+
import { ScaffoldEmitter, type ScaffoldChoice } from "./scaffold.js";
|
|
17
|
+
import type { EmitContext } from "./index.js";
|
|
18
|
+
export declare class DeepAgentsEmitter extends ScaffoldEmitter {
|
|
19
|
+
readonly framework = "deepagents";
|
|
20
|
+
readonly target = "deepagents";
|
|
21
|
+
readonly fileExtension = "py";
|
|
22
|
+
renderContext(ctx: EmitContext, _case: string): Record<string, unknown>;
|
|
23
|
+
losses(ctx: EmitContext, _choice: ScaffoldChoice): string[];
|
|
24
|
+
mapping(): Record<string, string>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → **DeepAgents** emitter (code-first, `deepagents`; TS twin of python
|
|
3
|
+
* `dna.emit.deepagents`).
|
|
4
|
+
*
|
|
5
|
+
* DeepAgents (the LangChain "batteries-included agent harness") is code-first — you
|
|
6
|
+
* call `create_deep_agent(model=..., tools=[...], system_prompt="...")`, there is no
|
|
7
|
+
* declarative agent file to map onto. So this target is a {@link ScaffoldEmitter}: it
|
|
8
|
+
* fills a curated `{deepagents × case}` template rather than generating code ad-hoc,
|
|
9
|
+
* and the emitted `INSTRUCTIONS` constant is byte-equal to the DNA-composed prompt.
|
|
10
|
+
*
|
|
11
|
+
* `system_prompt` sits in FRONT of the deep-agent's built-in harness prompt (the
|
|
12
|
+
* planning / filesystem / sub-agent scaffolding the framework appends) — so the DNA
|
|
13
|
+
* prompt is a PREFIX of the effective system prompt. Model: the DNA coordinate is
|
|
14
|
+
* PRESERVED (resolved via `init_chat_model`). Cases: `prompt-only` and `with-tools`.
|
|
15
|
+
*/
|
|
16
|
+
import { pyIdentifier, pyStrLiteral, ScaffoldEmitter } from "./scaffold.js";
|
|
17
|
+
export class DeepAgentsEmitter extends ScaffoldEmitter {
|
|
18
|
+
framework = "deepagents";
|
|
19
|
+
target = "deepagents";
|
|
20
|
+
fileExtension = "py";
|
|
21
|
+
renderContext(ctx, _case) {
|
|
22
|
+
const tools = ctx.tools.map((t) => ({
|
|
23
|
+
name: t.name,
|
|
24
|
+
func_name: pyIdentifier(t.name),
|
|
25
|
+
docstring_literal: pyStrLiteral(t.description || t.name),
|
|
26
|
+
}));
|
|
27
|
+
return {
|
|
28
|
+
has_model: ctx.model !== null,
|
|
29
|
+
model_literal: ctx.model ? pyStrLiteral(ctx.model) : "",
|
|
30
|
+
tools,
|
|
31
|
+
tool_list: tools.map((t) => t.func_name).join(", "),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
losses(ctx, _choice) {
|
|
35
|
+
const out = [
|
|
36
|
+
"harness prompt — `system_prompt` sits in FRONT of the deep-agent's built-in " +
|
|
37
|
+
"harness prompt (planning / filesystem / sub-agent scaffolding), which the " +
|
|
38
|
+
"framework appends; the DNA prompt is a PREFIX of the effective system prompt, " +
|
|
39
|
+
"not the whole of it",
|
|
40
|
+
"metadata.name — `create_deep_agent` has no declarative name slot; the DNA agent " +
|
|
41
|
+
"name is not carried",
|
|
42
|
+
];
|
|
43
|
+
if (ctx.tools.length > 0) {
|
|
44
|
+
out.push("tool body — each tool is a scaffolded STUB (a callable + " +
|
|
45
|
+
"`raise NotImplementedError`); its real implementation and typed signature " +
|
|
46
|
+
"must be wired");
|
|
47
|
+
}
|
|
48
|
+
if (ctx.model === null) {
|
|
49
|
+
out.push("model unbound in DNA and none supplied — emitted `create_deep_agent(...)` has " +
|
|
50
|
+
"no `model=`; the framework falls back to its default deep-agent model");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
out.push("model coordinate — the DNA coordinate is carried verbatim; " +
|
|
54
|
+
"`create_deep_agent` resolves it via `init_chat_model`, whose provider " +
|
|
55
|
+
"prefixes are `openai` / `anthropic` / `azure_openai` / `google_genai`; a " +
|
|
56
|
+
"DNA `azure/…` coordinate needs the `azure_openai:` prefix at wire-up");
|
|
57
|
+
}
|
|
58
|
+
if (ctx.outputSchema) {
|
|
59
|
+
out.push("output_schema — DNA's `spec.output_schema` has no direct `create_deep_agent` " +
|
|
60
|
+
"slot; enforce structured output via a middleware or a typed sub-agent by hand");
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
mapping() {
|
|
65
|
+
return {
|
|
66
|
+
"buildPrompt (Soul+guardrails+instruction)": "INSTRUCTIONS constant (byte-equal, PREFIX of system_prompt)",
|
|
67
|
+
"spec.model / Genome.default_llm": "create_deep_agent(model=...) (DNA coordinate preserved)",
|
|
68
|
+
"spec.tools[] (Tool Kind)": "callable stubs → create_deep_agent(tools=[...])",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/emit/index.d.ts
CHANGED
|
@@ -7,9 +7,28 @@
|
|
|
7
7
|
* consumes. The concrete first step of "DNA as the Terraform of agents": author
|
|
8
8
|
* once, emit per runtime, swap runtimes without rewriting the agent.
|
|
9
9
|
*
|
|
10
|
+
* The {@link EmitterPort} is a **first-class DNA port** — a documented contract
|
|
11
|
+
* on the same footing as the kernel's five ports, one layer OUT: the kernel
|
|
12
|
+
* *composes* the neutral agent; the EmitterPort *materializes* it for a runtime.
|
|
13
|
+
* See the `How to write an emitter` guide (`docs/guides/writing-an-emitter.md`).
|
|
14
|
+
*
|
|
15
|
+
* The contract has two surfaces: `buildEmitContext(mi, agent)` (the kernel-facing
|
|
16
|
+
* half — compose + project to the neutral {@link EmitContext}) and
|
|
17
|
+
* `EmitterPort.emit(ctx)` (the runtime-facing half a target implements — PURE,
|
|
18
|
+
* no kernel I/O). Its **central invariant**: the composed `instructions` in the
|
|
19
|
+
* emitted artifact is byte-equal to `mi.buildPrompt({agent})`. That invariant is
|
|
20
|
+
* checkable and *inheritable* via {@link EmitterPort.extractInstructions} — one
|
|
21
|
+
* generic test runs it over EVERY registered target.
|
|
22
|
+
*
|
|
23
|
+
* Two flavors satisfy the same port: **config-declarative** (a runtime with a
|
|
24
|
+
* published declarative schema — agent-framework / bedrock / vertex) and
|
|
25
|
+
* **scaffold-code** (a code-first runtime — see `scaffold.ts`; the `openai-agents`
|
|
26
|
+
* target is the reference).
|
|
27
|
+
*
|
|
10
28
|
* Parity: the pure de-para (`toPromptAgent` in `agentFramework.ts`) mirrors the
|
|
11
29
|
* 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
|
|
30
|
+
* rendering detail, so the parity contract is the emitted OBJECT/behavior, not
|
|
31
|
+
* the bytes.
|
|
13
32
|
*
|
|
14
33
|
* Shape: {@link EmitContext} (runtime-agnostic view of a composed agent) →
|
|
15
34
|
* {@link EmitterPort} (a target) → {@link EmitResult} (artifact + honest
|
|
@@ -57,11 +76,19 @@ export interface EmitResult {
|
|
|
57
76
|
mapping: Record<string, string>;
|
|
58
77
|
}
|
|
59
78
|
/** A runtime emitter — pure: reads an {@link EmitContext}, returns an
|
|
60
|
-
* {@link EmitResult}. No kernel I/O, no network.
|
|
79
|
+
* {@link EmitResult}. No kernel I/O, no network.
|
|
80
|
+
*
|
|
81
|
+
* A conforming emitter provides identity (`target` / `fileExtension`), the
|
|
82
|
+
* materialization ({@link emit}), and the byte-equal invariant hook
|
|
83
|
+
* ({@link extractInstructions}) that makes the central invariant inheritable. */
|
|
61
84
|
export interface EmitterPort {
|
|
62
85
|
readonly target: string;
|
|
63
86
|
readonly fileExtension: string;
|
|
64
87
|
emit(ctx: EmitContext): EmitResult;
|
|
88
|
+
/** Recover the composed instruction embedded in `artifact` (the inverse of the
|
|
89
|
+
* instruction half of {@link emit}) — used by the generic byte-equal contract
|
|
90
|
+
* test. Returns null only when the target has no instruction slot at all. */
|
|
91
|
+
extractInstructions(artifact: string): string | null;
|
|
65
92
|
}
|
|
66
93
|
export declare class EmitError extends Error {
|
|
67
94
|
}
|
|
@@ -72,6 +99,10 @@ export declare class UnknownTarget extends EmitError {
|
|
|
72
99
|
}
|
|
73
100
|
/** Register an emitter under its `target` (last registration wins). */
|
|
74
101
|
export declare function registerEmitter(emitter: EmitterPort): EmitterPort;
|
|
102
|
+
/** Remove a registered emitter (a host override or a test double). Returns
|
|
103
|
+
* whether a target was actually removed. Parity with the Python side popping
|
|
104
|
+
* from `EMITTER_REGISTRY`. */
|
|
105
|
+
export declare function unregisterEmitter(target: string): boolean;
|
|
75
106
|
/** Look up a registered emitter or throw {@link UnknownTarget}. */
|
|
76
107
|
export declare function getEmitter(target: string): Promise<EmitterPort>;
|
|
77
108
|
/** Sorted list of registered target ids. */
|
package/dist/emit/index.js
CHANGED
|
@@ -21,7 +21,20 @@ async function ensureBuiltins() {
|
|
|
21
21
|
builtinsWired = true;
|
|
22
22
|
const { AgentFrameworkEmitter } = await import("./agentFramework.js");
|
|
23
23
|
const { BedrockEmitter } = await import("./bedrock.js");
|
|
24
|
-
|
|
24
|
+
const { VertexEmitter } = await import("./vertex.js");
|
|
25
|
+
const { OpenAIAgentsEmitter } = await import("./openaiAgents.js");
|
|
26
|
+
const { LanggraphEmitter } = await import("./langgraph.js");
|
|
27
|
+
const { AgnoEmitter } = await import("./agno.js");
|
|
28
|
+
const { DeepAgentsEmitter } = await import("./deepagents.js");
|
|
29
|
+
for (const e of [
|
|
30
|
+
new AgentFrameworkEmitter(),
|
|
31
|
+
new BedrockEmitter(),
|
|
32
|
+
new VertexEmitter(),
|
|
33
|
+
new OpenAIAgentsEmitter(),
|
|
34
|
+
new LanggraphEmitter(),
|
|
35
|
+
new AgnoEmitter(),
|
|
36
|
+
new DeepAgentsEmitter(),
|
|
37
|
+
]) {
|
|
25
38
|
if (!EMITTER_REGISTRY.has(e.target))
|
|
26
39
|
EMITTER_REGISTRY.set(e.target, e);
|
|
27
40
|
}
|
|
@@ -31,6 +44,12 @@ export function registerEmitter(emitter) {
|
|
|
31
44
|
EMITTER_REGISTRY.set(emitter.target, emitter);
|
|
32
45
|
return emitter;
|
|
33
46
|
}
|
|
47
|
+
/** Remove a registered emitter (a host override or a test double). Returns
|
|
48
|
+
* whether a target was actually removed. Parity with the Python side popping
|
|
49
|
+
* from `EMITTER_REGISTRY`. */
|
|
50
|
+
export function unregisterEmitter(target) {
|
|
51
|
+
return EMITTER_REGISTRY.delete(target);
|
|
52
|
+
}
|
|
34
53
|
/** Look up a registered emitter or throw {@link UnknownTarget}. */
|
|
35
54
|
export async function getEmitter(target) {
|
|
36
55
|
await ensureBuiltins();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → **LangGraph** emitter (code-first, `langgraph.prebuilt`; TS twin of
|
|
3
|
+
* python `dna.emit.langgraph`).
|
|
4
|
+
*
|
|
5
|
+
* LangGraph is code-first — you build an agent by calling
|
|
6
|
+
* `create_react_agent(model, tools=[...], prompt="...")`, there is no declarative
|
|
7
|
+
* agent file to map onto. So this target is a {@link ScaffoldEmitter}: it fills a
|
|
8
|
+
* curated `{langgraph × case}` template rather than generating code ad-hoc, and the
|
|
9
|
+
* emitted `INSTRUCTIONS` constant is byte-equal to the DNA-composed prompt.
|
|
10
|
+
*
|
|
11
|
+
* Model: the DNA coordinate is PRESERVED (unlike openai-agents, which strips the
|
|
12
|
+
* provider token) — `create_react_agent` resolves the string via `init_chat_model`,
|
|
13
|
+
* which takes a `provider:model` coordinate. Cases: `prompt-only` (no tools) and
|
|
14
|
+
* `with-tools` (the ReAct idiom — one `@tool` stub per DNA Tool). `structured-output`
|
|
15
|
+
* (LangGraph's `response_format`) is not shipped yet and falls back with a loss.
|
|
16
|
+
*/
|
|
17
|
+
import { ScaffoldEmitter, type ScaffoldChoice } from "./scaffold.js";
|
|
18
|
+
import type { EmitContext } from "./index.js";
|
|
19
|
+
export declare class LanggraphEmitter extends ScaffoldEmitter {
|
|
20
|
+
readonly framework = "langgraph";
|
|
21
|
+
readonly target = "langgraph";
|
|
22
|
+
readonly fileExtension = "py";
|
|
23
|
+
renderContext(ctx: EmitContext, _case: string): Record<string, unknown>;
|
|
24
|
+
losses(ctx: EmitContext, _choice: ScaffoldChoice): string[];
|
|
25
|
+
mapping(): Record<string, string>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → **LangGraph** emitter (code-first, `langgraph.prebuilt`; TS twin of
|
|
3
|
+
* python `dna.emit.langgraph`).
|
|
4
|
+
*
|
|
5
|
+
* LangGraph is code-first — you build an agent by calling
|
|
6
|
+
* `create_react_agent(model, tools=[...], prompt="...")`, there is no declarative
|
|
7
|
+
* agent file to map onto. So this target is a {@link ScaffoldEmitter}: it fills a
|
|
8
|
+
* curated `{langgraph × case}` template rather than generating code ad-hoc, and the
|
|
9
|
+
* emitted `INSTRUCTIONS` constant is byte-equal to the DNA-composed prompt.
|
|
10
|
+
*
|
|
11
|
+
* Model: the DNA coordinate is PRESERVED (unlike openai-agents, which strips the
|
|
12
|
+
* provider token) — `create_react_agent` resolves the string via `init_chat_model`,
|
|
13
|
+
* which takes a `provider:model` coordinate. Cases: `prompt-only` (no tools) and
|
|
14
|
+
* `with-tools` (the ReAct idiom — one `@tool` stub per DNA Tool). `structured-output`
|
|
15
|
+
* (LangGraph's `response_format`) is not shipped yet and falls back with a loss.
|
|
16
|
+
*/
|
|
17
|
+
import { pyIdentifier, pyStrLiteral, ScaffoldEmitter } from "./scaffold.js";
|
|
18
|
+
export class LanggraphEmitter extends ScaffoldEmitter {
|
|
19
|
+
framework = "langgraph";
|
|
20
|
+
target = "langgraph";
|
|
21
|
+
fileExtension = "py";
|
|
22
|
+
renderContext(ctx, _case) {
|
|
23
|
+
const tools = ctx.tools.map((t) => ({
|
|
24
|
+
name: t.name,
|
|
25
|
+
func_name: pyIdentifier(t.name),
|
|
26
|
+
docstring_literal: pyStrLiteral(t.description || t.name),
|
|
27
|
+
}));
|
|
28
|
+
return {
|
|
29
|
+
has_model: ctx.model !== null,
|
|
30
|
+
model_literal: ctx.model ? pyStrLiteral(ctx.model) : "",
|
|
31
|
+
has_name: Boolean(ctx.name),
|
|
32
|
+
tools,
|
|
33
|
+
tool_list: tools.map((t) => t.func_name).join(", "),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
losses(ctx, _choice) {
|
|
37
|
+
const out = [];
|
|
38
|
+
if (ctx.tools.length > 0) {
|
|
39
|
+
out.push("tool body — each `@tool` is a scaffolded STUB (name + " +
|
|
40
|
+
"`raise NotImplementedError`); its real implementation and typed signature " +
|
|
41
|
+
"must be wired (LangChain derives the tool schema from the function " +
|
|
42
|
+
"signature + docstring)");
|
|
43
|
+
}
|
|
44
|
+
if (ctx.model === null) {
|
|
45
|
+
out.push("model unbound in DNA and none supplied — `create_react_agent` REQUIRES a " +
|
|
46
|
+
"model; the emitted call omits `model=`, so supply one at wire-up");
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
out.push("model coordinate — the DNA coordinate is carried verbatim; " +
|
|
50
|
+
"`create_react_agent` resolves it via `init_chat_model`, whose provider " +
|
|
51
|
+
"prefixes are `openai` / `anthropic` / `azure_openai` / `google_genai`; a " +
|
|
52
|
+
"DNA `azure/…` coordinate needs the `azure_openai:` prefix (or a model " +
|
|
53
|
+
"instance) at wire-up");
|
|
54
|
+
}
|
|
55
|
+
if (ctx.outputSchema) {
|
|
56
|
+
out.push("output_schema — map DNA's `spec.output_schema` to " +
|
|
57
|
+
"`create_react_agent(response_format=...)` (a Pydantic model) by hand; the " +
|
|
58
|
+
"scaffold does not synthesize the class");
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
mapping() {
|
|
63
|
+
return {
|
|
64
|
+
"buildPrompt (Soul+guardrails+instruction)": "INSTRUCTIONS constant (byte-equal)",
|
|
65
|
+
"metadata.name": "create_react_agent(name=...) (graph name)",
|
|
66
|
+
"spec.model / Genome.default_llm": "create_react_agent(model=...) (DNA coordinate preserved)",
|
|
67
|
+
"spec.tools[] (Tool Kind)": "@tool stubs → create_react_agent(tools=[...])",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → **OpenAI Agents SDK** emitter (the first CODE-FIRST target; TS twin of
|
|
3
|
+
* python `dna.emit.openai_agents`).
|
|
4
|
+
*
|
|
5
|
+
* The OpenAI Agents SDK is code-first — you construct an agent in Python
|
|
6
|
+
* (`Agent(name=..., instructions=..., model=..., tools=[...])`) — so this target
|
|
7
|
+
* is a {@link ScaffoldEmitter}: it fills a curated `{openai-agents × case}`
|
|
8
|
+
* template rather than generating code ad-hoc, and the emitted `INSTRUCTIONS`
|
|
9
|
+
* constant is byte-equal to the DNA-composed prompt.
|
|
10
|
+
*
|
|
11
|
+
* Cases (selected from ctx signals): `prompt-only` (no tools) and `with-tools`
|
|
12
|
+
* (the ReAct idiom — one `@function_tool` stub per DNA Tool). `structured-output`
|
|
13
|
+
* is not shipped yet and falls back with a recorded loss.
|
|
14
|
+
*/
|
|
15
|
+
import { ScaffoldEmitter, type ScaffoldChoice } from "./scaffold.js";
|
|
16
|
+
import type { EmitContext } from "./index.js";
|
|
17
|
+
/** Strip a DNA `prov:model` / `prov/model` coordinate to the bare id. */
|
|
18
|
+
export declare function bareModelId(model: string | null): string | null;
|
|
19
|
+
export declare class OpenAIAgentsEmitter extends ScaffoldEmitter {
|
|
20
|
+
readonly framework = "openai-agents";
|
|
21
|
+
readonly target = "openai-agents";
|
|
22
|
+
readonly fileExtension = "py";
|
|
23
|
+
renderContext(ctx: EmitContext, _case: string): Record<string, unknown>;
|
|
24
|
+
losses(ctx: EmitContext, _choice: ScaffoldChoice): string[];
|
|
25
|
+
mapping(): Record<string, string>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → **OpenAI Agents SDK** emitter (the first CODE-FIRST target; TS twin of
|
|
3
|
+
* python `dna.emit.openai_agents`).
|
|
4
|
+
*
|
|
5
|
+
* The OpenAI Agents SDK is code-first — you construct an agent in Python
|
|
6
|
+
* (`Agent(name=..., instructions=..., model=..., tools=[...])`) — so this target
|
|
7
|
+
* is a {@link ScaffoldEmitter}: it fills a curated `{openai-agents × case}`
|
|
8
|
+
* template rather than generating code ad-hoc, and the emitted `INSTRUCTIONS`
|
|
9
|
+
* constant is byte-equal to the DNA-composed prompt.
|
|
10
|
+
*
|
|
11
|
+
* Cases (selected from ctx signals): `prompt-only` (no tools) and `with-tools`
|
|
12
|
+
* (the ReAct idiom — one `@function_tool` stub per DNA Tool). `structured-output`
|
|
13
|
+
* is not shipped yet and falls back with a recorded loss.
|
|
14
|
+
*/
|
|
15
|
+
import { pyIdentifier, pyStrLiteral, ScaffoldEmitter } from "./scaffold.js";
|
|
16
|
+
const DNA_PROVIDER_TOKENS = new Set([
|
|
17
|
+
"azure", "azureopenai", "azure_openai", "openai", "foundry", "azureaifoundry",
|
|
18
|
+
"vertex", "google", "gemini", "anthropic",
|
|
19
|
+
]);
|
|
20
|
+
/** Strip a DNA `prov:model` / `prov/model` coordinate to the bare id. */
|
|
21
|
+
export function bareModelId(model) {
|
|
22
|
+
if (!model)
|
|
23
|
+
return null;
|
|
24
|
+
const ident = model.trim();
|
|
25
|
+
for (const sep of [":", "/"]) {
|
|
26
|
+
const i = ident.indexOf(sep);
|
|
27
|
+
if (i >= 0) {
|
|
28
|
+
const token = ident.slice(0, i).trim().toLowerCase();
|
|
29
|
+
if (DNA_PROVIDER_TOKENS.has(token))
|
|
30
|
+
return ident.slice(i + 1).trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return ident;
|
|
34
|
+
}
|
|
35
|
+
function isOpenAIModel(modelId) {
|
|
36
|
+
if (!modelId)
|
|
37
|
+
return false;
|
|
38
|
+
const m = modelId.trim().toLowerCase();
|
|
39
|
+
return m.startsWith("gpt-") || m.startsWith("o1") || m.startsWith("o3") || m.startsWith("o4");
|
|
40
|
+
}
|
|
41
|
+
export class OpenAIAgentsEmitter extends ScaffoldEmitter {
|
|
42
|
+
framework = "openai-agents";
|
|
43
|
+
target = "openai-agents";
|
|
44
|
+
fileExtension = "py";
|
|
45
|
+
renderContext(ctx, _case) {
|
|
46
|
+
const modelId = bareModelId(ctx.model);
|
|
47
|
+
const tools = ctx.tools.map((t) => ({
|
|
48
|
+
name: t.name,
|
|
49
|
+
func_name: pyIdentifier(t.name),
|
|
50
|
+
docstring_literal: pyStrLiteral(t.description || t.name),
|
|
51
|
+
}));
|
|
52
|
+
return {
|
|
53
|
+
has_model: modelId !== null,
|
|
54
|
+
model_literal: modelId ? pyStrLiteral(modelId) : "",
|
|
55
|
+
tools,
|
|
56
|
+
tool_list: tools.map((t) => t.func_name).join(", "),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
losses(ctx, _choice) {
|
|
60
|
+
const out = [];
|
|
61
|
+
if (ctx.tools.length > 0) {
|
|
62
|
+
out.push("tool body — each `@function_tool` is a scaffolded STUB (name + " +
|
|
63
|
+
"`raise NotImplementedError`); its real implementation and typed " +
|
|
64
|
+
"signature must be wired (the OpenAI Agents SDK derives the tool schema " +
|
|
65
|
+
"from the Python function signature + docstring)");
|
|
66
|
+
}
|
|
67
|
+
const modelId = bareModelId(ctx.model);
|
|
68
|
+
if (modelId === null) {
|
|
69
|
+
out.push("model unbound in DNA and none supplied — emitted `Agent(...)` has no " +
|
|
70
|
+
"`model=`; the SDK falls back to its default");
|
|
71
|
+
}
|
|
72
|
+
else if (!isOpenAIModel(modelId)) {
|
|
73
|
+
out.push("model coordinate — the OpenAI Agents SDK `model=` takes an OpenAI model " +
|
|
74
|
+
"name or a `Model` object; a non-OpenAI coordinate needs a custom " +
|
|
75
|
+
"provider / `ModelSettings` at wire-up");
|
|
76
|
+
}
|
|
77
|
+
if (ctx.outputSchema) {
|
|
78
|
+
out.push("output_schema — map DNA's `spec.output_schema` to `Agent(output_type=...)` " +
|
|
79
|
+
"(a Pydantic/TypedDict class) by hand; the scaffold does not synthesize the class");
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
mapping() {
|
|
84
|
+
return {
|
|
85
|
+
"buildPrompt (Soul+guardrails+instruction)": "INSTRUCTIONS constant (byte-equal)",
|
|
86
|
+
"metadata.name": "Agent(name=...)",
|
|
87
|
+
"spec.model / Genome.default_llm": "Agent(model=...) (provider token stripped)",
|
|
88
|
+
"spec.tools[] (Tool Kind)": "@function_tool stubs → Agent(tools=[...])",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type EmitContext, type EmitResult, type EmitterPort } from "./index.js";
|
|
2
|
+
/** Render `value` as a Python string literal that round-trips exactly. JSON
|
|
3
|
+
* string syntax is a valid Python literal (`\n`, `\"`, `\\`, `\uXXXX`), single
|
|
4
|
+
* line — so the emitted `INSTRUCTIONS` constant is byte-equal-recoverable. */
|
|
5
|
+
export declare function pyStrLiteral(value: string): string;
|
|
6
|
+
/** `kb-search` → `kb_search` — a valid Python identifier for a tool stub. */
|
|
7
|
+
export declare function pyIdentifier(name: string): string;
|
|
8
|
+
/** The default case classifier — read the DNA signals the ctx already carries. */
|
|
9
|
+
export declare function classifyCase(ctx: EmitContext): string;
|
|
10
|
+
/** Resolve a `{framework × case}` template to its Mustache source — the abstract
|
|
11
|
+
* seam between an emitter and *where a template lives*. The MVP reads
|
|
12
|
+
* package-data ({@link PackageDataScaffoldResolver}); a future kernel-backed
|
|
13
|
+
* resolver returns a per-scope/per-tenant Scaffold Kind body, swappable with no
|
|
14
|
+
* emitter change. Returns null when the source has no template for the pair. */
|
|
15
|
+
export interface ScaffoldResolver {
|
|
16
|
+
resolve(framework: string, kase: string): string | null;
|
|
17
|
+
}
|
|
18
|
+
/** The MVP resolver: read `emit/scaffolds/<framework>/<case>.py.tmpl`. */
|
|
19
|
+
export declare class PackageDataScaffoldResolver implements ScaffoldResolver {
|
|
20
|
+
resolve(framework: string, kase: string): string | null;
|
|
21
|
+
}
|
|
22
|
+
/** Swap the active template resolver — the seam the Scaffold-as-Kind promotion
|
|
23
|
+
* (`s-scaffold-as-kind`) plugs into. */
|
|
24
|
+
export declare function setScaffoldResolver(resolver: ScaffoldResolver): ScaffoldResolver;
|
|
25
|
+
/** Resolve a `{framework × case}` template through the active resolver. */
|
|
26
|
+
export declare function resolveScaffold(framework: string, kase: string): string | null;
|
|
27
|
+
/** The outcome of {@link selectScaffold}: which case template to fill. */
|
|
28
|
+
export interface ScaffoldChoice {
|
|
29
|
+
/** The case actually selected (a template exists for it). */
|
|
30
|
+
case: string;
|
|
31
|
+
/** The template source (Mustache). */
|
|
32
|
+
template: string;
|
|
33
|
+
/** The case the classifier requested — differs from `case` on a fallback. */
|
|
34
|
+
requested: string;
|
|
35
|
+
}
|
|
36
|
+
/** Pick the `{framework × case}` template for `ctx`: classify, then resolve to a
|
|
37
|
+
* real template, falling back down the generality chain when needed. */
|
|
38
|
+
export declare function selectScaffold(framework: string, ctx: EmitContext, classify?: (ctx: EmitContext) => string, resolver?: ScaffoldResolver | null): ScaffoldChoice;
|
|
39
|
+
/** Base for a CODE-FIRST {@link EmitterPort}. A subclass is THIN — it declares the
|
|
40
|
+
* framework + target ids and supplies the framework-specific template variables
|
|
41
|
+
* ({@link renderContext}), losses, and field mapping; case selection, template
|
|
42
|
+
* fill, and the byte-equal invariant hook are inherited. */
|
|
43
|
+
export declare abstract class ScaffoldEmitter implements EmitterPort {
|
|
44
|
+
abstract readonly framework: string;
|
|
45
|
+
abstract readonly target: string;
|
|
46
|
+
readonly fileExtension: string;
|
|
47
|
+
/** Optional template-resolution override (defaults to the active resolver —
|
|
48
|
+
* package-data today, a Scaffold Kind resolver tomorrow). */
|
|
49
|
+
readonly resolver: ScaffoldResolver | null;
|
|
50
|
+
/** Framework-specific template variables (merged over the common ones). */
|
|
51
|
+
renderContext(_ctx: EmitContext, _case: string): Record<string, unknown>;
|
|
52
|
+
/** Framework-specific de-para losses (in addition to the common ones). */
|
|
53
|
+
losses(_ctx: EmitContext, _choice: ScaffoldChoice): string[];
|
|
54
|
+
/** Field-level de-para (`dnaField -> targetField`) for reporting. */
|
|
55
|
+
mapping(): Record<string, string>;
|
|
56
|
+
/** The case classifier — override to add a framework-specific case. */
|
|
57
|
+
classify(ctx: EmitContext): string;
|
|
58
|
+
private commonContext;
|
|
59
|
+
private commonLosses;
|
|
60
|
+
emit(ctx: EmitContext): EmitResult;
|
|
61
|
+
/** Byte-equal invariant hook: read the top-level `INSTRUCTIONS = <literal>`
|
|
62
|
+
* constant every scaffold template emits (a JSON/Python string literal on one
|
|
63
|
+
* line), uniform across all code-first targets regardless of constructor shape. */
|
|
64
|
+
extractInstructions(artifact: string): string | null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `emit/scaffold` — the CODE-FIRST flavor of the {@link EmitterPort} (TS twin of
|
|
3
|
+
* python `dna.emit.scaffold`).
|
|
4
|
+
*
|
|
5
|
+
* Some runtimes have NO declarative agent format — you construct an agent object
|
|
6
|
+
* in code (OpenAI Agents SDK, LangGraph, Agno, DeepAgents all expose a simple
|
|
7
|
+
* constructor). For those the emitter must produce SOURCE CODE, and it does so by
|
|
8
|
+
* **filling a curated template**, never by generating code ad-hoc — the template
|
|
9
|
+
* captures the framework's best-practice idiom; the emitter only routes and fills.
|
|
10
|
+
*
|
|
11
|
+
* The mechanism is a template library indexed by `{framework × case}` plus a tiny
|
|
12
|
+
* case classifier:
|
|
13
|
+
*
|
|
14
|
+
* scaffolds/<framework>/<case>.py.tmpl // curated best-practice idiom per case
|
|
15
|
+
* selectScaffold(framework, ctx) // inspect ctx's DNA signals → pick a case
|
|
16
|
+
*
|
|
17
|
+
* There is deliberately NO single "one template per framework": a prompt-only, a
|
|
18
|
+
* tool-calling (ReAct), and a structured-output agent are DIFFERENT structures in
|
|
19
|
+
* the SAME framework. The classifier reads the neutral {@link EmitContext} signals
|
|
20
|
+
* (no tools → `prompt-only`; tools → `with-tools`; `outputSchema` →
|
|
21
|
+
* `structured-output`) and falls back down a generality chain when a framework
|
|
22
|
+
* does not ship a case, recording the fallback as a loss.
|
|
23
|
+
*
|
|
24
|
+
* Future direction — Scaffold as a Kind: templates are read through an abstract
|
|
25
|
+
* seam, {@link resolveScaffold} (a {@link ScaffoldResolver}), NOT a hardcoded file
|
|
26
|
+
* path. The MVP resolver reads package-data, but the seam lets a second source
|
|
27
|
+
* plug in with no change to any emitter — a first-class **Scaffold Kind** resolved
|
|
28
|
+
* by the kernel (scope-aware, tenant-overridable). Tracked as `s-scaffold-as-kind`.
|
|
29
|
+
*/
|
|
30
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
31
|
+
import { dirname, join } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
import Mustache from "mustache";
|
|
34
|
+
import { EmitError } from "./index.js";
|
|
35
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
/** The generality fallback chain (specific → general) per requested case. */
|
|
37
|
+
const FALLBACK = {
|
|
38
|
+
"structured-output": ["structured-output", "with-tools", "prompt-only"],
|
|
39
|
+
"with-tools": ["with-tools", "prompt-only"],
|
|
40
|
+
"prompt-only": ["prompt-only"],
|
|
41
|
+
};
|
|
42
|
+
/** Render `value` as a Python string literal that round-trips exactly. JSON
|
|
43
|
+
* string syntax is a valid Python literal (`\n`, `\"`, `\\`, `\uXXXX`), single
|
|
44
|
+
* line — so the emitted `INSTRUCTIONS` constant is byte-equal-recoverable. */
|
|
45
|
+
export function pyStrLiteral(value) {
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
}
|
|
48
|
+
/** `kb-search` → `kb_search` — a valid Python identifier for a tool stub. */
|
|
49
|
+
export function pyIdentifier(name) {
|
|
50
|
+
let ident = String(name)
|
|
51
|
+
.trim()
|
|
52
|
+
.replace(/[^0-9a-zA-Z_]/g, "_")
|
|
53
|
+
.replace(/^_+|_+$/g, "");
|
|
54
|
+
if (!ident)
|
|
55
|
+
ident = "tool";
|
|
56
|
+
if (/^[0-9]/.test(ident))
|
|
57
|
+
ident = `_${ident}`;
|
|
58
|
+
return ident;
|
|
59
|
+
}
|
|
60
|
+
/** The default case classifier — read the DNA signals the ctx already carries. */
|
|
61
|
+
export function classifyCase(ctx) {
|
|
62
|
+
if (ctx.outputSchema)
|
|
63
|
+
return "structured-output";
|
|
64
|
+
if (ctx.tools.length > 0)
|
|
65
|
+
return "with-tools";
|
|
66
|
+
return "prompt-only";
|
|
67
|
+
}
|
|
68
|
+
/** The MVP resolver: read `emit/scaffolds/<framework>/<case>.py.tmpl`. */
|
|
69
|
+
export class PackageDataScaffoldResolver {
|
|
70
|
+
resolve(framework, kase) {
|
|
71
|
+
const path = join(HERE, "scaffolds", framework, `${kase}.py.tmpl`);
|
|
72
|
+
return existsSync(path) ? readFileSync(path, "utf-8") : null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let activeResolver = new PackageDataScaffoldResolver();
|
|
76
|
+
/** Swap the active template resolver — the seam the Scaffold-as-Kind promotion
|
|
77
|
+
* (`s-scaffold-as-kind`) plugs into. */
|
|
78
|
+
export function setScaffoldResolver(resolver) {
|
|
79
|
+
activeResolver = resolver;
|
|
80
|
+
return resolver;
|
|
81
|
+
}
|
|
82
|
+
/** Resolve a `{framework × case}` template through the active resolver. */
|
|
83
|
+
export function resolveScaffold(framework, kase) {
|
|
84
|
+
return activeResolver.resolve(framework, kase);
|
|
85
|
+
}
|
|
86
|
+
/** Pick the `{framework × case}` template for `ctx`: classify, then resolve to a
|
|
87
|
+
* real template, falling back down the generality chain when needed. */
|
|
88
|
+
export function selectScaffold(framework, ctx, classify = classifyCase, resolver = null) {
|
|
89
|
+
const resolve = resolver
|
|
90
|
+
? (f, k) => resolver.resolve(f, k)
|
|
91
|
+
: resolveScaffold;
|
|
92
|
+
const requested = classify(ctx);
|
|
93
|
+
for (const kase of FALLBACK[requested] ?? [requested]) {
|
|
94
|
+
const template = resolve(framework, kase);
|
|
95
|
+
if (template !== null)
|
|
96
|
+
return { case: kase, template, requested };
|
|
97
|
+
}
|
|
98
|
+
throw new EmitError(`no scaffold template for framework '${framework}' ` +
|
|
99
|
+
`(looked for case '${requested}' and its fallbacks)`);
|
|
100
|
+
}
|
|
101
|
+
/** Base for a CODE-FIRST {@link EmitterPort}. A subclass is THIN — it declares the
|
|
102
|
+
* framework + target ids and supplies the framework-specific template variables
|
|
103
|
+
* ({@link renderContext}), losses, and field mapping; case selection, template
|
|
104
|
+
* fill, and the byte-equal invariant hook are inherited. */
|
|
105
|
+
export class ScaffoldEmitter {
|
|
106
|
+
fileExtension = "py";
|
|
107
|
+
/** Optional template-resolution override (defaults to the active resolver —
|
|
108
|
+
* package-data today, a Scaffold Kind resolver tomorrow). */
|
|
109
|
+
resolver = null;
|
|
110
|
+
// ── the hooks a subclass overrides ────────────────────────────────────────
|
|
111
|
+
/** Framework-specific template variables (merged over the common ones). */
|
|
112
|
+
renderContext(_ctx, _case) {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
/** Framework-specific de-para losses (in addition to the common ones). */
|
|
116
|
+
losses(_ctx, _choice) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
/** Field-level de-para (`dnaField -> targetField`) for reporting. */
|
|
120
|
+
mapping() {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
/** The case classifier — override to add a framework-specific case. */
|
|
124
|
+
classify(ctx) {
|
|
125
|
+
return classifyCase(ctx);
|
|
126
|
+
}
|
|
127
|
+
// ── the inherited machinery ───────────────────────────────────────────────
|
|
128
|
+
commonContext(ctx) {
|
|
129
|
+
return {
|
|
130
|
+
name: ctx.name,
|
|
131
|
+
name_literal: pyStrLiteral(ctx.name),
|
|
132
|
+
description: ctx.description,
|
|
133
|
+
has_description: Boolean(ctx.description),
|
|
134
|
+
// INSTRUCTIONS constant — byte-equal, recoverable via extractInstructions.
|
|
135
|
+
instructions_literal: pyStrLiteral(ctx.instructions),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
commonLosses(ctx, choice) {
|
|
139
|
+
const losses = [
|
|
140
|
+
"composition structure — Soul reuse + wired Guardrails flatten to one " +
|
|
141
|
+
"`INSTRUCTIONS` string (a code-first agent has no `soul:`/`guardrails:` slot)",
|
|
142
|
+
"tenant overlay — a per-tenant persona without a fork has no code-first field",
|
|
143
|
+
"eval-as-contract — prompt invariants (EvalCases) have no code-first slot",
|
|
144
|
+
];
|
|
145
|
+
if (choice.case !== choice.requested) {
|
|
146
|
+
losses.push(`scaffold case — the '${choice.requested}' idiom is not shipped for ` +
|
|
147
|
+
`'${this.framework}'; fell back to the '${choice.case}' template ` +
|
|
148
|
+
"(structure closest to but not identical to the requested case)");
|
|
149
|
+
}
|
|
150
|
+
return losses;
|
|
151
|
+
}
|
|
152
|
+
emit(ctx) {
|
|
153
|
+
const choice = selectScaffold(this.framework, ctx, (c) => this.classify(c), this.resolver);
|
|
154
|
+
const variables = { ...this.commonContext(ctx), ...this.renderContext(ctx, choice.case) };
|
|
155
|
+
const artifact = Mustache.render(choice.template, variables);
|
|
156
|
+
const losses = [...this.commonLosses(ctx, choice), ...this.losses(ctx, choice)];
|
|
157
|
+
return {
|
|
158
|
+
artifact,
|
|
159
|
+
target: this.target,
|
|
160
|
+
filename: `${ctx.name}.${this.fileExtension}`,
|
|
161
|
+
losses,
|
|
162
|
+
mapping: this.mapping(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Byte-equal invariant hook: read the top-level `INSTRUCTIONS = <literal>`
|
|
166
|
+
* constant every scaffold template emits (a JSON/Python string literal on one
|
|
167
|
+
* line), uniform across all code-first targets regardless of constructor shape. */
|
|
168
|
+
extractInstructions(artifact) {
|
|
169
|
+
const match = artifact.match(/^INSTRUCTIONS = (.+)$/m);
|
|
170
|
+
if (!match)
|
|
171
|
+
return null;
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(match[1]);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""DNA-emitted Agno agent: {{name}}.
|
|
2
|
+
|
|
3
|
+
Generated by `dna emit --target agno`. INSTRUCTIONS is the DNA-composed prompt
|
|
4
|
+
(Soul + guardrails + instruction), carried byte-equal from the source of truth —
|
|
5
|
+
edit the agent in DNA and re-emit, do not hand-edit the prompt here.
|
|
6
|
+
"""
|
|
7
|
+
from agno.agent import Agent
|
|
8
|
+
|
|
9
|
+
INSTRUCTIONS = {{{instructions_literal}}}
|
|
10
|
+
|
|
11
|
+
agent = Agent(
|
|
12
|
+
name={{{name_literal}}},
|
|
13
|
+
{{#has_model}} model={{{model_literal}}},
|
|
14
|
+
{{/has_model}} instructions=INSTRUCTIONS,
|
|
15
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""DNA-emitted Agno agent (tool-calling): {{name}}.
|
|
2
|
+
|
|
3
|
+
Generated by `dna emit --target agno`. INSTRUCTIONS is the DNA-composed prompt
|
|
4
|
+
(Soul + guardrails + instruction), carried byte-equal. Each tool below is a STUB
|
|
5
|
+
scaffolded from a DNA Tool's name + description — wire its real implementation and
|
|
6
|
+
typed signature (Agno derives the tool schema from the function + docstring).
|
|
7
|
+
"""
|
|
8
|
+
from agno.agent import Agent
|
|
9
|
+
|
|
10
|
+
INSTRUCTIONS = {{{instructions_literal}}}
|
|
11
|
+
|
|
12
|
+
{{#tools}}
|
|
13
|
+
def {{func_name}}() -> str:
|
|
14
|
+
{{{docstring_literal}}}
|
|
15
|
+
raise NotImplementedError("DNA scaffold: implement the {{name}} tool")
|
|
16
|
+
|
|
17
|
+
{{/tools}}
|
|
18
|
+
agent = Agent(
|
|
19
|
+
name={{{name_literal}}},
|
|
20
|
+
{{#has_model}} model={{{model_literal}}},
|
|
21
|
+
{{/has_model}} instructions=INSTRUCTIONS,
|
|
22
|
+
tools=[{{tool_list}}],
|
|
23
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""DNA-emitted DeepAgents agent: {{name}}.
|
|
2
|
+
|
|
3
|
+
Generated by `dna emit --target deepagents`. INSTRUCTIONS is the DNA-composed
|
|
4
|
+
prompt (Soul + guardrails + instruction), carried byte-equal from the source of
|
|
5
|
+
truth — edit the agent in DNA and re-emit, do not hand-edit the prompt here.
|
|
6
|
+
`system_prompt` sits in FRONT of the deep-agent's built-in harness prompt.
|
|
7
|
+
"""
|
|
8
|
+
from deepagents import create_deep_agent
|
|
9
|
+
|
|
10
|
+
INSTRUCTIONS = {{{instructions_literal}}}
|
|
11
|
+
|
|
12
|
+
agent = create_deep_agent(
|
|
13
|
+
{{#has_model}} model={{{model_literal}}},
|
|
14
|
+
{{/has_model}} tools=[],
|
|
15
|
+
system_prompt=INSTRUCTIONS,
|
|
16
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""DNA-emitted DeepAgents agent (tool-calling): {{name}}.
|
|
2
|
+
|
|
3
|
+
Generated by `dna emit --target deepagents`. INSTRUCTIONS is the DNA-composed
|
|
4
|
+
prompt (Soul + guardrails + instruction), carried byte-equal. Each tool below is a
|
|
5
|
+
STUB scaffolded from a DNA Tool's name + description — wire its real implementation
|
|
6
|
+
and typed signature. `system_prompt` sits in FRONT of the deep-agent harness prompt.
|
|
7
|
+
"""
|
|
8
|
+
from deepagents import create_deep_agent
|
|
9
|
+
|
|
10
|
+
INSTRUCTIONS = {{{instructions_literal}}}
|
|
11
|
+
|
|
12
|
+
{{#tools}}
|
|
13
|
+
def {{func_name}}() -> str:
|
|
14
|
+
{{{docstring_literal}}}
|
|
15
|
+
raise NotImplementedError("DNA scaffold: implement the {{name}} tool")
|
|
16
|
+
|
|
17
|
+
{{/tools}}
|
|
18
|
+
agent = create_deep_agent(
|
|
19
|
+
{{#has_model}} model={{{model_literal}}},
|
|
20
|
+
{{/has_model}} tools=[{{tool_list}}],
|
|
21
|
+
system_prompt=INSTRUCTIONS,
|
|
22
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""DNA-emitted LangGraph ReAct agent: {{name}}.
|
|
2
|
+
|
|
3
|
+
Generated by `dna emit --target langgraph`. INSTRUCTIONS is the DNA-composed
|
|
4
|
+
prompt (Soul + guardrails + instruction), carried byte-equal from the source of
|
|
5
|
+
truth — edit the agent in DNA and re-emit, do not hand-edit the prompt here.
|
|
6
|
+
"""
|
|
7
|
+
from langgraph.prebuilt import create_react_agent
|
|
8
|
+
|
|
9
|
+
INSTRUCTIONS = {{{instructions_literal}}}
|
|
10
|
+
|
|
11
|
+
agent = create_react_agent(
|
|
12
|
+
{{#has_model}} model={{{model_literal}}},
|
|
13
|
+
{{/has_model}} tools=[],
|
|
14
|
+
prompt=INSTRUCTIONS,
|
|
15
|
+
{{#has_name}} name={{{name_literal}}},
|
|
16
|
+
{{/has_name}})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""DNA-emitted LangGraph ReAct agent (tool-calling): {{name}}.
|
|
2
|
+
|
|
3
|
+
Generated by `dna emit --target langgraph`. INSTRUCTIONS is the DNA-composed
|
|
4
|
+
prompt (Soul + guardrails + instruction), carried byte-equal. Each @tool below is
|
|
5
|
+
a STUB scaffolded from a DNA Tool's name + description — wire its real
|
|
6
|
+
implementation and typed signature (LangChain derives the tool schema from them).
|
|
7
|
+
"""
|
|
8
|
+
from langchain_core.tools import tool
|
|
9
|
+
from langgraph.prebuilt import create_react_agent
|
|
10
|
+
|
|
11
|
+
INSTRUCTIONS = {{{instructions_literal}}}
|
|
12
|
+
|
|
13
|
+
{{#tools}}
|
|
14
|
+
@tool
|
|
15
|
+
def {{func_name}}() -> str:
|
|
16
|
+
{{{docstring_literal}}}
|
|
17
|
+
raise NotImplementedError("DNA scaffold: implement the {{name}} tool")
|
|
18
|
+
|
|
19
|
+
{{/tools}}
|
|
20
|
+
agent = create_react_agent(
|
|
21
|
+
{{#has_model}} model={{{model_literal}}},
|
|
22
|
+
{{/has_model}} tools=[{{tool_list}}],
|
|
23
|
+
prompt=INSTRUCTIONS,
|
|
24
|
+
{{#has_name}} name={{{name_literal}}},
|
|
25
|
+
{{/has_name}})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""DNA-emitted OpenAI Agents SDK agent: {{name}}.
|
|
2
|
+
|
|
3
|
+
Generated by `dna emit --target openai-agents`. INSTRUCTIONS is the DNA-composed
|
|
4
|
+
prompt (Soul + guardrails + instruction), carried byte-equal from the source of
|
|
5
|
+
truth — edit the agent in DNA and re-emit, do not hand-edit the prompt here.
|
|
6
|
+
"""
|
|
7
|
+
from agents import Agent
|
|
8
|
+
|
|
9
|
+
INSTRUCTIONS = {{{instructions_literal}}}
|
|
10
|
+
|
|
11
|
+
agent = Agent(
|
|
12
|
+
name={{{name_literal}}},
|
|
13
|
+
instructions=INSTRUCTIONS,
|
|
14
|
+
{{#has_model}} model={{{model_literal}}},
|
|
15
|
+
{{/has_model}})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""DNA-emitted OpenAI Agents SDK agent (tool-calling / ReAct): {{name}}.
|
|
2
|
+
|
|
3
|
+
Generated by `dna emit --target openai-agents`. INSTRUCTIONS is the DNA-composed
|
|
4
|
+
prompt (Soul + guardrails + instruction), carried byte-equal. Each @function_tool
|
|
5
|
+
below is a STUB scaffolded from a DNA Tool's name + description — wire its real
|
|
6
|
+
implementation and typed signature (the SDK derives the tool schema from them).
|
|
7
|
+
"""
|
|
8
|
+
from agents import Agent, function_tool
|
|
9
|
+
|
|
10
|
+
INSTRUCTIONS = {{{instructions_literal}}}
|
|
11
|
+
|
|
12
|
+
{{#tools}}
|
|
13
|
+
@function_tool
|
|
14
|
+
def {{func_name}}() -> str:
|
|
15
|
+
{{{docstring_literal}}}
|
|
16
|
+
raise NotImplementedError("DNA scaffold: implement the {{name}} tool")
|
|
17
|
+
|
|
18
|
+
{{/tools}}
|
|
19
|
+
agent = Agent(
|
|
20
|
+
name={{{name_literal}}},
|
|
21
|
+
instructions=INSTRUCTIONS,
|
|
22
|
+
{{#has_model}} model={{{model_literal}}},
|
|
23
|
+
{{/has_model}} tools=[{{tool_list}}],
|
|
24
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EmitContext, EmitResult, EmitterPort } from "./index.js";
|
|
2
|
+
/** `concierge-grounded` → `concierge_grounded` (a valid ADK agent name — ADK
|
|
3
|
+
* requires a Python identifier). Mirrors Bedrock's `camel` logical-id transform. */
|
|
4
|
+
export declare function snake(name: string): string;
|
|
5
|
+
/** Project a DNA model coordinate → an ADK `model` id. A known DNA provider token
|
|
6
|
+
* (`vertex/gemini-2.0-flash` → `gemini-2.0-flash`, `openai:gpt-4o` → `gpt-4o`) is
|
|
7
|
+
* stripped; a bare id passes through. */
|
|
8
|
+
export declare function vertexModelId(model: string | null): string | null;
|
|
9
|
+
/** Whether an emitted `model` id is a native Gemini id (else needs `model_code`). */
|
|
10
|
+
export declare function isGemini(modelId: string | null): boolean;
|
|
11
|
+
export declare class VertexEmitter implements EmitterPort {
|
|
12
|
+
readonly target = "vertex";
|
|
13
|
+
readonly fileExtension = "adk.yaml";
|
|
14
|
+
/** The PURE de-para: {@link EmitContext} → the ADK Agent Config object. Field
|
|
15
|
+
* order is intentional; the schema-header comment is prepended at serialization
|
|
16
|
+
* time, not part of this object. */
|
|
17
|
+
toAgentConfig(ctx: EmitContext): Record<string, unknown>;
|
|
18
|
+
emit(ctx: EmitContext): EmitResult;
|
|
19
|
+
/** Byte-equal invariant hook: read `instruction` back from the emitted ADK
|
|
20
|
+
* Agent Config YAML (the `# yaml-language-server` header is a YAML comment). */
|
|
21
|
+
extractInstructions(artifact: string): string | null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA → **Google ADK Agent Config** emitter (TS twin of python
|
|
3
|
+
* `dna.emit.vertex`). Materializes an {@link EmitContext} into a **Google Agent
|
|
4
|
+
* Development Kit (ADK) Agent Config** YAML — the declarative, code-free way to
|
|
5
|
+
* define an ADK `LlmAgent` (loaded with `config_agent_utils.from_config(...)`).
|
|
6
|
+
* The THIRD runtime the SAME DNA source emits to (after Microsoft agent-framework
|
|
7
|
+
* and Amazon Bedrock): the portability proof — author once, emit per runtime.
|
|
8
|
+
*
|
|
9
|
+
* Why the ADK **Agent Config** (not the code-first `LlmAgent` object or Vertex AI
|
|
10
|
+
* Agent Engine): only Agent Config has a published, field-for-field *declarative*
|
|
11
|
+
* schema (`agent_class`, `name`, `model`, `instruction`, `tools`, …), so it is the
|
|
12
|
+
* only honest de-para target. Emitting the YAML gives a lintable artifact that
|
|
13
|
+
* needs no GCP credential to produce or validate structurally (the
|
|
14
|
+
* `# yaml-language-server` header wires the real schema into any editor).
|
|
15
|
+
*
|
|
16
|
+
* The de-para (DNA field → ADK `LlmAgentConfig` field):
|
|
17
|
+
* (fixed) -> agent_class: LlmAgent
|
|
18
|
+
* metadata.name -> name (snake_cased — valid identifier)
|
|
19
|
+
* metadata.description -> description (when present)
|
|
20
|
+
* Soul + guardrails + instruction -> instruction (flat, BYTE-EQUAL)
|
|
21
|
+
* spec.model (or Genome default_llm)-> model (Gemini id; provider stripped)
|
|
22
|
+
* spec.tools[] (Tool Kind surfaces) -> tools[].name (a CODE reference — see loss)
|
|
23
|
+
*
|
|
24
|
+
* `toAgentConfig` is the PURE de-para and is parity-critical: it must build the
|
|
25
|
+
* SAME object the Python `to_agent_config` builds from the same context.
|
|
26
|
+
*/
|
|
27
|
+
import yaml from "js-yaml";
|
|
28
|
+
/** The published ADK Agent Config JSON Schema — emitted as a leading
|
|
29
|
+
* `# yaml-language-server` header so any editor / validator binds the artifact to
|
|
30
|
+
* the REAL schema (the credential-free structural-validation hook). */
|
|
31
|
+
const ADK_SCHEMA_URL = "https://raw.githubusercontent.com/google/adk-python/refs/heads/main/" +
|
|
32
|
+
"src/google/adk/agents/config_schemas/AgentConfig.json";
|
|
33
|
+
/** The ADK `agent_class` this emitter targets (the declarative LLM agent). */
|
|
34
|
+
const AGENT_CLASS = "LlmAgent";
|
|
35
|
+
/** DNA provider tokens (the `prov:model` / `prov/model` prefixes). Stripped to
|
|
36
|
+
* expose the bare model id; a bare Gemini id passes through unchanged. */
|
|
37
|
+
const DNA_PROVIDER_TOKENS = new Set([
|
|
38
|
+
"azure", "azureopenai", "azure_openai", "openai", "foundry", "azureaifoundry",
|
|
39
|
+
"vertex", "google", "gemini", "anthropic",
|
|
40
|
+
]);
|
|
41
|
+
/** `concierge-grounded` → `concierge_grounded` (a valid ADK agent name — ADK
|
|
42
|
+
* requires a Python identifier). Mirrors Bedrock's `camel` logical-id transform. */
|
|
43
|
+
export function snake(name) {
|
|
44
|
+
let ident = String(name)
|
|
45
|
+
.trim()
|
|
46
|
+
.split("")
|
|
47
|
+
.map((ch) => (/[a-zA-Z0-9_]/.test(ch) ? ch : "_"))
|
|
48
|
+
.join("")
|
|
49
|
+
.replace(/^_+|_+$/g, "")
|
|
50
|
+
.toLowerCase();
|
|
51
|
+
if (!ident)
|
|
52
|
+
ident = "agent";
|
|
53
|
+
if (/^[0-9]/.test(ident))
|
|
54
|
+
ident = `_${ident}`;
|
|
55
|
+
return ident;
|
|
56
|
+
}
|
|
57
|
+
/** Project a DNA model coordinate → an ADK `model` id. A known DNA provider token
|
|
58
|
+
* (`vertex/gemini-2.0-flash` → `gemini-2.0-flash`, `openai:gpt-4o` → `gpt-4o`) is
|
|
59
|
+
* stripped; a bare id passes through. */
|
|
60
|
+
export function vertexModelId(model) {
|
|
61
|
+
if (!model)
|
|
62
|
+
return null;
|
|
63
|
+
const ident = model.trim();
|
|
64
|
+
const slash = ident.indexOf("/");
|
|
65
|
+
if (slash >= 0) {
|
|
66
|
+
const token = ident.slice(0, slash).trim().toLowerCase();
|
|
67
|
+
if (DNA_PROVIDER_TOKENS.has(token))
|
|
68
|
+
return ident.slice(slash + 1).trim();
|
|
69
|
+
return ident;
|
|
70
|
+
}
|
|
71
|
+
const colon = ident.indexOf(":");
|
|
72
|
+
if (colon >= 0) {
|
|
73
|
+
const token = ident.slice(0, colon).trim().toLowerCase();
|
|
74
|
+
if (DNA_PROVIDER_TOKENS.has(token))
|
|
75
|
+
return ident.slice(colon + 1).trim();
|
|
76
|
+
}
|
|
77
|
+
return ident;
|
|
78
|
+
}
|
|
79
|
+
/** Whether an emitted `model` id is a native Gemini id (else needs `model_code`). */
|
|
80
|
+
export function isGemini(modelId) {
|
|
81
|
+
return !!modelId && modelId.trim().toLowerCase().startsWith("gemini");
|
|
82
|
+
}
|
|
83
|
+
/** Project neutral tool surfaces → ADK `tools` code references (`- name: <fqn>`).
|
|
84
|
+
* ADK has no inline function-schema slot (unlike agent-framework / Bedrock): it
|
|
85
|
+
* derives a tool's description + parameters from the referenced Python callable at
|
|
86
|
+
* load, so the faithful de-para carries the tool NAME as a placeholder reference. */
|
|
87
|
+
function emitTools(tools) {
|
|
88
|
+
return tools.map((t) => ({ name: t.name }));
|
|
89
|
+
}
|
|
90
|
+
export class VertexEmitter {
|
|
91
|
+
target = "vertex";
|
|
92
|
+
fileExtension = "adk.yaml";
|
|
93
|
+
/** The PURE de-para: {@link EmitContext} → the ADK Agent Config object. Field
|
|
94
|
+
* order is intentional; the schema-header comment is prepended at serialization
|
|
95
|
+
* time, not part of this object. */
|
|
96
|
+
toAgentConfig(ctx) {
|
|
97
|
+
const doc = { agent_class: AGENT_CLASS, name: snake(ctx.name) };
|
|
98
|
+
if (ctx.description)
|
|
99
|
+
doc.description = ctx.description;
|
|
100
|
+
const modelId = vertexModelId(ctx.model);
|
|
101
|
+
if (modelId)
|
|
102
|
+
doc.model = modelId;
|
|
103
|
+
doc.instruction = ctx.instructions; // verbatim — the byte-equal gate
|
|
104
|
+
if (ctx.tools.length > 0)
|
|
105
|
+
doc.tools = emitTools(ctx.tools);
|
|
106
|
+
return doc;
|
|
107
|
+
}
|
|
108
|
+
emit(ctx) {
|
|
109
|
+
const config = this.toAgentConfig(ctx);
|
|
110
|
+
const body = yaml.dump(config, { sortKeys: false, lineWidth: -1 });
|
|
111
|
+
const artifact = `# yaml-language-server: $schema=${ADK_SCHEMA_URL}\n${body}`;
|
|
112
|
+
const losses = [
|
|
113
|
+
"composition structure — Soul reuse + wired Guardrails flatten to one " +
|
|
114
|
+
"`instruction` string (Agent Config has no `soul:`/`guardrails:` slot)",
|
|
115
|
+
"tenant overlay — a per-tenant persona without a fork has no Agent Config field",
|
|
116
|
+
"eval-as-contract — prompt invariants (EvalCases) have no Agent Config slot",
|
|
117
|
+
];
|
|
118
|
+
if (ctx.tools.length > 0) {
|
|
119
|
+
losses.push("tool binding — ADK binds a tool by a CODE reference (a fully qualified " +
|
|
120
|
+
"Python path or a built-in name), not a declarative schema; a Tool's " +
|
|
121
|
+
"`description` and `parameters` (JSON Schema) have no Agent Config slot " +
|
|
122
|
+
"(ADK derives them from the Python callable at load). Each emitted `- name` " +
|
|
123
|
+
"is a placeholder to repoint to the tool's real FQN");
|
|
124
|
+
}
|
|
125
|
+
if (ctx.outputSchema) {
|
|
126
|
+
losses.push("output_schema — ADK `output_schema` is a `CodeConfig` (a reference to a " +
|
|
127
|
+
"Pydantic class by FQN), not an inline JSON Schema; DNA's inline " +
|
|
128
|
+
"`spec.output_schema` has no inline Agent Config slot");
|
|
129
|
+
}
|
|
130
|
+
const modelId = vertexModelId(ctx.model);
|
|
131
|
+
if (modelId === null) {
|
|
132
|
+
losses.push("model unbound in DNA and none supplied — emitted config has no `model`; " +
|
|
133
|
+
"ADK inherits the ancestor / system default (gemini)");
|
|
134
|
+
}
|
|
135
|
+
else if (!isGemini(modelId)) {
|
|
136
|
+
losses.push("model coordinate — ADK `model` natively accepts a Gemini id; a DNA " +
|
|
137
|
+
"`azure/openai` coordinate is not a Gemini id and needs `model_code` (a " +
|
|
138
|
+
"`LiteLlm` CodeConfig) at deploy, plus a GCP project/region");
|
|
139
|
+
}
|
|
140
|
+
const mapping = {
|
|
141
|
+
"metadata.name": "name (snake_case identifier)",
|
|
142
|
+
"metadata.description": "description",
|
|
143
|
+
"buildPrompt (Soul+guardrails+instruction)": "instruction (byte-equal)",
|
|
144
|
+
"spec.model / Genome.default_llm": "model (Gemini id; provider token stripped)",
|
|
145
|
+
"spec.tools[] (Tool Kind)": "tools[].name (code reference)",
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
artifact,
|
|
149
|
+
target: this.target,
|
|
150
|
+
filename: `${ctx.name}.${this.fileExtension}`,
|
|
151
|
+
losses,
|
|
152
|
+
mapping,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** Byte-equal invariant hook: read `instruction` back from the emitted ADK
|
|
156
|
+
* Agent Config YAML (the `# yaml-language-server` header is a YAML comment). */
|
|
157
|
+
extractInstructions(artifact) {
|
|
158
|
+
const config = yaml.load(artifact);
|
|
159
|
+
const value = config?.instruction;
|
|
160
|
+
return typeof value === "string" ? value : null;
|
|
161
|
+
}
|
|
162
|
+
}
|
package/dist/extensions/sdlc.js
CHANGED
|
@@ -1357,6 +1357,9 @@ class HtmlArtifactKind extends KindBase {
|
|
|
1357
1357
|
title: aj.title ?? null,
|
|
1358
1358
|
source: aj.source ?? null,
|
|
1359
1359
|
created_at: aj.created_at ?? null,
|
|
1360
|
+
// published_url — canonical hosted location (e.g. claude.ai artifact
|
|
1361
|
+
// URL) so the gallery renders a clickable link. Lives in artifact_json.
|
|
1362
|
+
published_url: aj.published_url ?? null,
|
|
1360
1363
|
html_bytes: (spec.html ?? "").length,
|
|
1361
1364
|
};
|
|
1362
1365
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,10 +26,14 @@ export { AgentNotFound, ToolNotFound, UnknownLayout } from "./kernel/errors.js";
|
|
|
26
26
|
export { PromptLibrary, loadPrompts } from "./prompts.js";
|
|
27
27
|
export { ToolLibrary, loadTools } from "./tools.js";
|
|
28
28
|
export type { ToolSurface } from "./tools.js";
|
|
29
|
-
export { emitAgent, buildEmitContext, registerEmitter, getEmitter, availableTargets, EmitError, UnknownTarget, } from "./emit/index.js";
|
|
29
|
+
export { emitAgent, buildEmitContext, registerEmitter, unregisterEmitter, getEmitter, availableTargets, EmitError, UnknownTarget, } from "./emit/index.js";
|
|
30
30
|
export type { EmitContext, EmitResult, EmitTool, EmitterPort, BuildEmitContextOpts, } from "./emit/index.js";
|
|
31
31
|
export { AgentFrameworkEmitter } from "./emit/agentFramework.js";
|
|
32
32
|
export { BedrockEmitter } from "./emit/bedrock.js";
|
|
33
|
+
export { VertexEmitter } from "./emit/vertex.js";
|
|
34
|
+
export { OpenAIAgentsEmitter } from "./emit/openaiAgents.js";
|
|
35
|
+
export { ScaffoldEmitter, selectScaffold, classifyCase, resolveScaffold, setScaffoldResolver, PackageDataScaffoldResolver, } from "./emit/scaffold.js";
|
|
36
|
+
export type { ScaffoldChoice, ScaffoldResolver } from "./emit/scaffold.js";
|
|
33
37
|
export type { LoadPromptsOptions } from "./prompts.js";
|
|
34
38
|
export { anchorScopesRoot, PackageScopeNotFound, DEFAULT_SUBPATH } from "./package-scope.js";
|
|
35
39
|
export { loadConfig, findConfig, CONFIG_FILENAME } from "./config.js";
|
package/dist/index.js
CHANGED
|
@@ -30,9 +30,12 @@ export { createKernelWithBuiltins, quickInstance, createRuntimeWithBuiltins, qui
|
|
|
30
30
|
export { AgentNotFound, ToolNotFound, UnknownLayout } from "./kernel/errors.js";
|
|
31
31
|
export { PromptLibrary, loadPrompts } from "./prompts.js";
|
|
32
32
|
export { ToolLibrary, loadTools } from "./tools.js";
|
|
33
|
-
export { emitAgent, buildEmitContext, registerEmitter, getEmitter, availableTargets, EmitError, UnknownTarget, } from "./emit/index.js";
|
|
33
|
+
export { emitAgent, buildEmitContext, registerEmitter, unregisterEmitter, getEmitter, availableTargets, EmitError, UnknownTarget, } from "./emit/index.js";
|
|
34
34
|
export { AgentFrameworkEmitter } from "./emit/agentFramework.js";
|
|
35
35
|
export { BedrockEmitter } from "./emit/bedrock.js";
|
|
36
|
+
export { VertexEmitter } from "./emit/vertex.js";
|
|
37
|
+
export { OpenAIAgentsEmitter } from "./emit/openaiAgents.js";
|
|
38
|
+
export { ScaffoldEmitter, selectScaffold, classifyCase, resolveScaffold, setScaffoldResolver, PackageDataScaffoldResolver, } from "./emit/scaffold.js";
|
|
36
39
|
export { anchorScopesRoot, PackageScopeNotFound, DEFAULT_SUBPATH } from "./package-scope.js";
|
|
37
40
|
export { loadConfig, findConfig, CONFIG_FILENAME } from "./config.js";
|
|
38
41
|
export { sourceFromUrl, resolveDefaultFsUrl, UnsupportedSourceScheme } from "./adapters/source-url.js";
|