@tdk-landscape/tdk-cli-core 1.3.170 → 1.3.171
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/cli/dist/agent-runtimes/custom.js +151 -0
- package/cli/dist/agent-runtimes/registry.js +36 -0
- package/cli/dist/backend-frameworks/registry.js +16 -0
- package/cli/dist/commands/help.js +1 -0
- package/cli/dist/commands/resource.js +39 -7
- package/cli/dist/types/index.js +2 -0
- package/cli/dist/utils/constants.js +4 -0
- package/cli/dist/utils/doctor-wiring.js +1 -1
- package/cli/dist/utils/resource-kind.js +1 -1
- package/cli/dist/utils/service-manifest.js +1 -0
- package/cli/dist/utils/status-ports.js +3 -1
- package/cli/dist-bundle/{chunk-MZBDLTO2.js → chunk-KCHXMU3C.js} +3 -3
- package/cli/dist-bundle/cli.js +400 -263
- package/cli/dist-bundle/{cold-preflight-PZKLJJGV.js → cold-preflight-6C6O6NDI.js} +1 -1
- package/cli/package.json +1 -1
- package/engine/schemas/service-schema.json +25 -1
- package/engine/topologies/platform/docker/compose/compose.star +31 -3
- package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +8 -0
- package/engine/topologies/tilt/manifest/loader.star +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `custom` agent runtime: a plain Bun HTTP service with no model SDK, no agent framework and no vendor dependency.
|
|
3
|
+
* It is the escape hatch the agent resource type is built on. Replace the `runTurn` body with your model call and tools;
|
|
4
|
+
* the process contract around it (health, readiness, session checkpoints, SIGTERM drain) is what TDK relies on.
|
|
5
|
+
*/
|
|
6
|
+
export function getCustomAgentIndexTemplate(name) {
|
|
7
|
+
return `import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
8
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
// Contract with TDK (see docs/ai-agents.md):
|
|
12
|
+
// AGENT_DURABILITY files (set by tdk from service.json agent.durability)
|
|
13
|
+
// AGENT_STATE_DIR where files-durability checkpoints live (a named volume that survives tdk down)
|
|
14
|
+
// PORT port to listen on
|
|
15
|
+
// /health: the process is alive. /ready: the checkpoint store is reachable. Idle is healthy, so /health never checks the store.
|
|
16
|
+
|
|
17
|
+
const DURABILITY = 'files';
|
|
18
|
+
const STATE_DIR = process.env.AGENT_STATE_DIR || join(process.cwd(), '.agent-state');
|
|
19
|
+
const PORT = Number(process.env.PORT || 3000);
|
|
20
|
+
const DRAIN_TIMEOUT_MS = 10_000;
|
|
21
|
+
|
|
22
|
+
interface Checkpoint {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
messages: Array<{ role: 'user' | 'agent'; content: string; at: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Session store. files: one JSON checkpoint per session, written atomically (temp file, then rename), so a kill
|
|
28
|
+
// mid-write leaves the previous checkpoint intact.
|
|
29
|
+
const store = {
|
|
30
|
+
ready(): boolean {
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
load(sessionId: string): Checkpoint {
|
|
39
|
+
const file = join(STATE_DIR, sessionId + '.json');
|
|
40
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { sessionId, messages: [] };
|
|
41
|
+
},
|
|
42
|
+
save(checkpoint: Checkpoint): void {
|
|
43
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
44
|
+
const file = join(STATE_DIR, checkpoint.sessionId + '.json');
|
|
45
|
+
const tmp = file + '.' + process.pid + '.tmp';
|
|
46
|
+
writeFileSync(tmp, JSON.stringify(checkpoint));
|
|
47
|
+
renameSync(tmp, file);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Replace this with the agent's work: call your model, run tools, return the reply. Keep it a pure function of the
|
|
52
|
+
// checkpoint so a resumed session continues from what was saved.
|
|
53
|
+
async function runTurn(checkpoint: Checkpoint, input: string): Promise<string> {
|
|
54
|
+
// The caller has already appended this turn's user message, so the turn number is half the message count, rounded up.
|
|
55
|
+
return 'received ' + input + ' (turn ' + Math.ceil(checkpoint.messages.length / 2) + ')';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let draining = false;
|
|
59
|
+
let inFlight = 0;
|
|
60
|
+
|
|
61
|
+
function sendJson(res: ServerResponse, status: number, body: unknown) {
|
|
62
|
+
res.statusCode = status;
|
|
63
|
+
res.setHeader('content-type', 'application/json');
|
|
64
|
+
res.end(JSON.stringify(body));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readJson(req: IncomingMessage): Promise<any> {
|
|
68
|
+
const chunks: Buffer[] = [];
|
|
69
|
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
|
70
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
71
|
+
return raw ? JSON.parse(raw) : {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const server = createServer(async (req, res) => {
|
|
75
|
+
inFlight++;
|
|
76
|
+
res.on('close', () => inFlight--);
|
|
77
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (url.pathname === '/health') {
|
|
81
|
+
return sendJson(res, 200, { status: 'ok', service: '${name}', draining });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (url.pathname === '/ready') {
|
|
85
|
+
const ok = !draining && store.ready();
|
|
86
|
+
return sendJson(res, ok ? 200 : 503, { ready: ok, durability: DURABILITY });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Session routing: the session id comes from the x-session-id header, so several instances of one agent can
|
|
90
|
+
// share a Traefik route and each session still reaches its own checkpoint.
|
|
91
|
+
if (url.pathname === '/turn' && req.method === 'POST') {
|
|
92
|
+
if (draining) return sendJson(res, 503, { error: 'draining' });
|
|
93
|
+
const sessionId = String(req.headers['x-session-id'] ?? '');
|
|
94
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(sessionId)) {
|
|
95
|
+
return sendJson(res, 400, { error: 'x-session-id header is required (1-128 letters, digits, - or _)' });
|
|
96
|
+
}
|
|
97
|
+
const body = await readJson(req);
|
|
98
|
+
const input = String(body.input ?? '');
|
|
99
|
+
const checkpoint = store.load(sessionId);
|
|
100
|
+
checkpoint.messages.push({ role: 'user', content: input, at: new Date().toISOString() });
|
|
101
|
+
const reply = await runTurn(checkpoint, input);
|
|
102
|
+
checkpoint.messages.push({ role: 'agent', content: reply, at: new Date().toISOString() });
|
|
103
|
+
store.save(checkpoint);
|
|
104
|
+
return sendJson(res, 200, { sessionId, reply, turns: checkpoint.messages.length / 2 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (url.pathname === '/session' && req.method === 'GET') {
|
|
108
|
+
const sessionId = String(req.headers['x-session-id'] ?? '');
|
|
109
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(sessionId)) return sendJson(res, 400, { error: 'x-session-id header is required' });
|
|
110
|
+
const checkpoint = store.load(sessionId);
|
|
111
|
+
return sendJson(res, 200, { sessionId, turns: checkpoint.messages.length / 2, messages: checkpoint.messages });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return sendJson(res, 404, { error: 'not_found' });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('[agent] request failed:', error);
|
|
117
|
+
return sendJson(res, 500, { error: 'internal_error' });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Bind beyond loopback: Traefik reaches the container over the Docker network.
|
|
122
|
+
server.listen(PORT, '0.0.0.0', () => {
|
|
123
|
+
console.log('\\n🤖 ${name} agent on http://localhost:' + PORT + ' (durability: ' + DURABILITY + ')');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Drain on SIGTERM (tdk down, hot reload, docker stop): stop taking turns, let in-flight turns finish, then exit 0.
|
|
127
|
+
// Checkpoints are written before a turn is answered, so a drained exit loses no acknowledged turn.
|
|
128
|
+
function shutdown(signal: string) {
|
|
129
|
+
if (draining) return;
|
|
130
|
+
draining = true;
|
|
131
|
+
console.log('[agent] ' + signal + ' received, draining ' + inFlight + ' in-flight request(s)');
|
|
132
|
+
server.close();
|
|
133
|
+
const deadline = Date.now() + DRAIN_TIMEOUT_MS;
|
|
134
|
+
const timer = setInterval(() => {
|
|
135
|
+
if (inFlight === 0 || Date.now() > deadline) {
|
|
136
|
+
clearInterval(timer);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
}, 100);
|
|
140
|
+
}
|
|
141
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
142
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
export const customAgentRuntime = {
|
|
146
|
+
id: "custom",
|
|
147
|
+
label: "Custom agent runtime (Bun HTTP, no vendor dependency)",
|
|
148
|
+
dependencies: {},
|
|
149
|
+
devDependencies: {},
|
|
150
|
+
createIndex: getCustomAgentIndexTemplate,
|
|
151
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Copyright (c) 2026 TDK Landscape contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { TdkError } from "../utils/errors.js";
|
|
4
|
+
import { customAgentRuntime } from "./custom.js";
|
|
5
|
+
/**
|
|
6
|
+
* Agent runtimes an `agent` resource can be scaffolded with. No runtime is privileged: each is a scaffold plus the
|
|
7
|
+
* process contract in its index file, and `custom` is the default because it has no vendor dependency.
|
|
8
|
+
*/
|
|
9
|
+
export const AGENT_RUNTIMES = {
|
|
10
|
+
custom: customAgentRuntime,
|
|
11
|
+
};
|
|
12
|
+
export const DEFAULT_AGENT_RUNTIME = "custom";
|
|
13
|
+
// `postgres` is deliberately absent: the scaffold has no Postgres session store yet (#1256), so offering it would produce an
|
|
14
|
+
// agent that answers every turn with a 500. Add it here together with the store.
|
|
15
|
+
export const AGENT_DURABILITY_OPTIONS = ["files"];
|
|
16
|
+
export function resolveAgentRuntime(runtimeId) {
|
|
17
|
+
const id = runtimeId ?? DEFAULT_AGENT_RUNTIME;
|
|
18
|
+
const provider = AGENT_RUNTIMES[id];
|
|
19
|
+
if (!provider) {
|
|
20
|
+
const supported = Object.keys(AGENT_RUNTIMES).join(", ");
|
|
21
|
+
throw new TdkError(`Unknown agent runtime "${id}". Supported runtimes: ${supported}.`, [
|
|
22
|
+
`Use one of: ${supported}`,
|
|
23
|
+
`Omit --runtime to use ${DEFAULT_AGENT_RUNTIME}`,
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
return provider;
|
|
27
|
+
}
|
|
28
|
+
export function resolveAgentDurability(value) {
|
|
29
|
+
const durability = value ?? "files";
|
|
30
|
+
if (!AGENT_DURABILITY_OPTIONS.includes(durability)) {
|
|
31
|
+
throw new TdkError(`Unknown --durability "${durability}".`, [
|
|
32
|
+
`Use one of: ${AGENT_DURABILITY_OPTIONS.join(", ")}`,
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
return durability;
|
|
36
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 TDK Landscape contributors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { resolveAgentRuntime } from "../agent-runtimes/registry.js";
|
|
3
4
|
import { getBackendIndexTemplate } from "../backend-languages/bun.js";
|
|
4
5
|
import { TdkError } from "../utils/errors.js";
|
|
5
6
|
import { elysiaBackendProvider } from "./elysia.js";
|
|
@@ -56,6 +57,21 @@ export function resolveBackendFramework(resourceType, frameworkId, language) {
|
|
|
56
57
|
}
|
|
57
58
|
return mcpBackendProvider;
|
|
58
59
|
}
|
|
60
|
+
// `agent` is scaffolded by an agent runtime (src/agent-runtimes/), not a framework. Only the default runtime is reachable here:
|
|
61
|
+
// `tdk resource` validates --runtime before it scaffolds, so the package.json and tsconfig always match the chosen runtime.
|
|
62
|
+
if (resourceType === "agent") {
|
|
63
|
+
if (frameworkId !== undefined) {
|
|
64
|
+
throw new TdkError("--framework cannot be combined with --type agent.", [
|
|
65
|
+
"An agent resource is scaffolded by an agent runtime; use --runtime <id> instead",
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
if (language?.createFiles) {
|
|
69
|
+
throw new TdkError(`--language ${language.id} cannot be combined with --type agent.`, [
|
|
70
|
+
"An agent resource is a Bun service; drop --language",
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
return resolveAgentRuntime();
|
|
74
|
+
}
|
|
59
75
|
if (resourceType !== "backend" || frameworkId === undefined)
|
|
60
76
|
return undefined;
|
|
61
77
|
if (language?.createFiles) {
|
|
@@ -131,6 +131,7 @@ export function showHelp() {
|
|
|
131
131
|
${chalk.cyan("$")} tdk project # Create master configs
|
|
132
132
|
${chalk.cyan("$")} tdk resource api --type backend # Create a backend service
|
|
133
133
|
${chalk.cyan("$")} tdk resource app --type frontend # Create a frontend app
|
|
134
|
+
${chalk.cyan("$")} tdk resource bot --type agent # Create an AI agent (sessions survive tdk down)
|
|
134
135
|
${chalk.cyan("$")} tdk stacks --services # List stacks with their resources
|
|
135
136
|
${chalk.cyan("$")} tdk up my-stack # Start a stack
|
|
136
137
|
${chalk.cyan("$")} tdk networks # Show Traefik-routed URLs
|
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync } from "node:fs";
|
|
|
4
4
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
+
import { DEFAULT_AGENT_RUNTIME, resolveAgentDurability, resolveAgentRuntime, } from "../agent-runtimes/registry.js";
|
|
7
8
|
import { resolveBackendFramework } from "../backend-frameworks/registry.js";
|
|
8
9
|
import { getBackendIndexTemplate } from "../backend-languages/bun.js";
|
|
9
10
|
import { resolveBackendLanguage } from "../backend-languages/registry.js";
|
|
@@ -56,6 +57,10 @@ export const TYPE_SPECIFIC = {
|
|
|
56
57
|
mcp: {
|
|
57
58
|
healthCheckPath: "/health",
|
|
58
59
|
},
|
|
60
|
+
// /ready is the readiness route (checkpoint store reachable); /health is liveness only, so an idle agent stays healthy.
|
|
61
|
+
agent: {
|
|
62
|
+
healthCheckPath: "/health",
|
|
63
|
+
},
|
|
59
64
|
[BRING_YOUR_OWN_TYPE]: {
|
|
60
65
|
healthCheckPath: "/health",
|
|
61
66
|
},
|
|
@@ -84,7 +89,7 @@ export function resolveResourcePort(resourceType, value, assignedPort, resources
|
|
|
84
89
|
}
|
|
85
90
|
return port;
|
|
86
91
|
}
|
|
87
|
-
export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId, languageId) {
|
|
92
|
+
export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId, languageId, agent) {
|
|
88
93
|
const framework = resolveFrontendFramework(type, frameworkId);
|
|
89
94
|
// Only an explicit selection is persisted: a missing `language` has always meant Bun.
|
|
90
95
|
const language = languageId === undefined ? undefined : resolveBackendLanguage(type, languageId);
|
|
@@ -123,8 +128,11 @@ export function createServiceJson(name, type, stack, port, extraFeatures = [], f
|
|
|
123
128
|
stack,
|
|
124
129
|
port,
|
|
125
130
|
...(framework ? { framework: framework.id } : {}),
|
|
126
|
-
...(backendFramework && type !== "mcp"
|
|
131
|
+
...(backendFramework && type !== "mcp" && type !== "agent"
|
|
132
|
+
? { framework: backendFramework.id }
|
|
133
|
+
: {}),
|
|
127
134
|
...(language ? { language: language.id } : {}),
|
|
135
|
+
...(type === "agent" && agent ? { agent } : {}),
|
|
128
136
|
};
|
|
129
137
|
}
|
|
130
138
|
export function createByoServiceJson(name, stack, port, options) {
|
|
@@ -272,7 +280,7 @@ export function parseResourceType(type) {
|
|
|
272
280
|
export const resourceCommand = new Command("resource")
|
|
273
281
|
.description("Create a new resource (service) from scratch, or register an existing one")
|
|
274
282
|
.argument("[name]", "Resource name (kebab-case)")
|
|
275
|
-
.option("-t, --type <type>", "Resource type: backend, frontend, worker, mcp, bring-your-own, sdk", "backend")
|
|
283
|
+
.option("-t, --type <type>", "Resource type: backend, frontend, worker, mcp, agent, bring-your-own, sdk", "backend")
|
|
276
284
|
.option("--framework <id>", "Framework: frontend react (default), vue, svelte, preact, lit, solid, qwik, vanilla, tanstack-router (see --frameworks); backend hono (default), express, elysia, fastify, nestjs, koa, h3")
|
|
277
285
|
.option("--frameworks", "List registered frontend frameworks and exit")
|
|
278
286
|
.option("--language <id>", "Backend language: bun (default), python, go, rust")
|
|
@@ -289,6 +297,8 @@ export const resourceCommand = new Command("resource")
|
|
|
289
297
|
.option("--image <name>", "Docker image name instead of building from Dockerfile (for bring-your-own)")
|
|
290
298
|
.option("--port <port>", "Port for the resource, within its type's range (default: next free port in that range)")
|
|
291
299
|
.option("--feature <feature...>", "Enable a resource feature (for example: prisma)")
|
|
300
|
+
.option("--runtime <id>", "Agent runtime (for agent only): custom (default)")
|
|
301
|
+
.option("--durability <kind>", "Where agent sessions are checkpointed (for agent only): files (default, a named volume that survives tdk down)")
|
|
292
302
|
.action(async (name, options) => {
|
|
293
303
|
if (options.frameworks) {
|
|
294
304
|
for (const f of listFrontendFrameworks()) {
|
|
@@ -322,6 +332,21 @@ export const resourceCommand = new Command("resource")
|
|
|
322
332
|
assertValid(validateResourceName(resourceName));
|
|
323
333
|
}
|
|
324
334
|
const resourceType = parseResourceType(options.type);
|
|
335
|
+
if (resourceType !== "agent" &&
|
|
336
|
+
(options.runtime !== undefined || options.durability !== undefined)) {
|
|
337
|
+
throw new TdkError("--runtime and --durability can only be used with --type agent.", [
|
|
338
|
+
"Drop --runtime and --durability, or pass --type agent",
|
|
339
|
+
]);
|
|
340
|
+
}
|
|
341
|
+
// Validate the agent options before anything is written, so a bad flag leaves no half-scaffolded directory.
|
|
342
|
+
let agentConfig;
|
|
343
|
+
if (resourceType === "agent") {
|
|
344
|
+
resolveAgentRuntime(options.runtime);
|
|
345
|
+
agentConfig = {
|
|
346
|
+
runtime: options.runtime ?? DEFAULT_AGENT_RUNTIME,
|
|
347
|
+
durability: resolveAgentDurability(options.durability),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
325
350
|
let frameworkOption = options.framework;
|
|
326
351
|
if (resourceType === "frontend" &&
|
|
327
352
|
frameworkOption === undefined &&
|
|
@@ -343,7 +368,9 @@ export const resourceCommand = new Command("resource")
|
|
|
343
368
|
const backendFramework = resolveBackendFramework(resourceType, frameworkOption, backendLanguage);
|
|
344
369
|
// The id passed on to the file generators. An mcp resource has one fixed scaffold and no `framework` setting, so it passes none
|
|
345
370
|
// (its provider is selected by the resource type itself).
|
|
346
|
-
const scaffoldFrameworkId = resourceType === "mcp"
|
|
371
|
+
const scaffoldFrameworkId = resourceType === "mcp" || resourceType === "agent"
|
|
372
|
+
? undefined
|
|
373
|
+
: (frontendFramework?.id ?? backendFramework?.id);
|
|
347
374
|
let dddEnabled = false;
|
|
348
375
|
if (options.ddd) {
|
|
349
376
|
if (resourceType !== "backend" && resourceType !== "worker") {
|
|
@@ -423,6 +450,7 @@ export const resourceCommand = new Command("resource")
|
|
|
423
450
|
frontend: `apps/${resourceName}`,
|
|
424
451
|
worker: `workers/${resourceName}`,
|
|
425
452
|
mcp: `services/${stackName}/${resourceName}`,
|
|
453
|
+
agent: `services/${stackName}/${resourceName}`,
|
|
426
454
|
[BRING_YOUR_OWN_TYPE]: `services/${stackName}/${resourceName}`,
|
|
427
455
|
sdk: `packages/${resourceName}`,
|
|
428
456
|
};
|
|
@@ -607,7 +635,7 @@ This file contains the resource configuration for TDK.
|
|
|
607
635
|
// Prepare file generation tasks
|
|
608
636
|
const requestedFeatures = Array.isArray(options.feature) ? options.feature : [];
|
|
609
637
|
const prismaEnabled = requestedFeatures.includes("prisma");
|
|
610
|
-
const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, [...(dddEnabled ? ["ddd"] : []), ...requestedFeatures], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id);
|
|
638
|
+
const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, [...(dddEnabled ? ["ddd"] : []), ...requestedFeatures], scaffoldFrameworkId, options.language === undefined ? undefined : backendLanguage?.id, agentConfig);
|
|
611
639
|
const packageJson = createPackageJson(resourceName, resourceType, scaffoldFrameworkId, prismaEnabled);
|
|
612
640
|
if (prismaEnabled && resourceType === "backend") {
|
|
613
641
|
serviceJson.dependsOn = [`${resourceName}-migrator`];
|
|
@@ -645,12 +673,16 @@ This file contains the resource configuration for TDK.
|
|
|
645
673
|
},
|
|
646
674
|
];
|
|
647
675
|
// Add source files based on resource type
|
|
648
|
-
if (resourceType === "backend" || resourceType === "mcp") {
|
|
676
|
+
if (resourceType === "backend" || resourceType === "mcp" || resourceType === "agent") {
|
|
649
677
|
tasks.push({
|
|
650
678
|
type: "text",
|
|
651
679
|
filename: "src/index.ts",
|
|
652
680
|
content: (backendFramework?.createIndex ?? getBackendIndexTemplate)(resourceName),
|
|
653
|
-
description: resourceType === "mcp"
|
|
681
|
+
description: resourceType === "mcp"
|
|
682
|
+
? "Generating MCP server source"
|
|
683
|
+
: resourceType === "agent"
|
|
684
|
+
? "Generating agent source"
|
|
685
|
+
: "Generating backend source",
|
|
654
686
|
emoji: "💻",
|
|
655
687
|
});
|
|
656
688
|
}
|
package/cli/dist/types/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export const RESOURCE_CONFIG_APP_TYPES = [
|
|
|
9
9
|
"worker",
|
|
10
10
|
"migrator",
|
|
11
11
|
"mcp",
|
|
12
|
+
"agent",
|
|
12
13
|
"bring-your-own",
|
|
13
14
|
];
|
|
14
15
|
export const CREATABLE_RESOURCE_TYPES = [
|
|
@@ -16,6 +17,7 @@ export const CREATABLE_RESOURCE_TYPES = [
|
|
|
16
17
|
"frontend",
|
|
17
18
|
"worker",
|
|
18
19
|
"mcp",
|
|
20
|
+
"agent",
|
|
19
21
|
"bring-your-own",
|
|
20
22
|
];
|
|
21
23
|
export function isCreatableResourceType(value) {
|
|
@@ -45,6 +45,7 @@ export const VALID_RESOURCE_TYPES = [
|
|
|
45
45
|
"worker",
|
|
46
46
|
"migrator",
|
|
47
47
|
"mcp",
|
|
48
|
+
"agent",
|
|
48
49
|
BRING_YOUR_OWN_TYPE,
|
|
49
50
|
];
|
|
50
51
|
// Frontend/library/sdk services are served statically or consumed as
|
|
@@ -56,6 +57,7 @@ export const REQUIRED_PACKAGE_SCRIPTS = {
|
|
|
56
57
|
worker: ["dev", "build", "start"],
|
|
57
58
|
migrator: ["dev", "build", "start"],
|
|
58
59
|
mcp: ["dev", "build", "start"],
|
|
60
|
+
agent: ["dev", "build", "start"],
|
|
59
61
|
frontend: ["dev", "build"],
|
|
60
62
|
library: ["dev", "build"],
|
|
61
63
|
sdk: ["dev", "build"],
|
|
@@ -76,6 +78,8 @@ export const PORT_RANGES = {
|
|
|
76
78
|
backend: { base: 4000, min: 4000, max: 4999, range: "4000-4999" },
|
|
77
79
|
// An MCP server is an HTTP service, so it shares the backend-compatible range with bring-your-own.
|
|
78
80
|
mcp: { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
|
|
81
|
+
// An agent serves /health and /ready over HTTP (see agent-runtimes/custom.ts), so it shares the same range.
|
|
82
|
+
agent: { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
|
|
79
83
|
[BRING_YOUR_OWN_TYPE]: { base: 4000, min: 4000, max: 5999, range: "4000-5999" },
|
|
80
84
|
worker: { base: 6000, min: 6000, max: 6999, range: "6000-6999" },
|
|
81
85
|
health: { base: 5000, min: 5000, max: 5999, range: "5000-5999" },
|
|
@@ -232,7 +232,7 @@ export function checkDuplicateResourceNames(projectRoot = findProjectRoot() ?? p
|
|
|
232
232
|
fix: 'Give each resource its own "appName" in its service.json. The name becomes the Compose service and the Traefik router, so two resources with one name collide',
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
|
-
/** A resource that gets a Traefik route to its own `port`: a backend, an mcp server, or a bring-your-own resource that is not opted out. */
|
|
235
|
+
/** A resource that gets a Traefik route to its own `port`: a backend, an mcp server, an agent, or a bring-your-own resource that is not opted out. */
|
|
236
236
|
function isRoutedToPort(resource) {
|
|
237
237
|
const appType = resource.config?.appType;
|
|
238
238
|
if (isApiServiceType(appType))
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Protocol endpoint, which the engine builds, routes and health-checks exactly like a backend).
|
|
6
6
|
*/
|
|
7
7
|
export function isApiServiceType(appType) {
|
|
8
|
-
return appType === "backend" || appType === "mcp";
|
|
8
|
+
return appType === "backend" || appType === "mcp" || appType === "agent";
|
|
9
9
|
}
|
|
@@ -9,7 +9,9 @@ export function buildServicePorts(resource, ingressHttp) {
|
|
|
9
9
|
const { appBase, apiBase } = resolveSubdomainBases(ingressHttp);
|
|
10
10
|
url = `${isApiServiceType(appType) ? apiBase : appBase}${resolveServicePath(resource)}`;
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
const containerPort = resource.port ?? resource.config?.port ?? null;
|
|
13
|
+
const directUrl = !routable && containerPort ? `http://localhost:${containerPort}` : null;
|
|
14
|
+
return { url, containerPort, hostPort: null, directUrl };
|
|
13
15
|
}
|
|
14
16
|
/** Ports a client should forward or open: ingress, Tilt UI, and the published datastore. Not one per service. */
|
|
15
17
|
export function buildStackPorts(plan, tiltPort) {
|