@2kw/ai 7.1.0-dev.14 → 7.1.0-dev.16

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.
@@ -3,6 +3,7 @@ import chalk from "chalk";
3
3
  import { getClient, runAction } from "../lib/client.js";
4
4
  import { isJsonOutput } from "../lib/config.js";
5
5
  import { BackboneApiError, CliUsageError } from "../lib/errors.js";
6
+ import { ensurePublished } from "../lib/agent-publish.js";
6
7
  import { findAgentByName } from "../lib/agent-lookup.js";
7
8
  import { loadAgentConfig, maskSecrets } from "../agent-config/file.js";
8
9
  import { CONFIG_FIELDS, planApply } from "../agent-config/plan.js";
@@ -110,30 +111,31 @@ export function makeAgentApplyCommand() {
110
111
  let versionId = latest?.id;
111
112
  version = latest?.versionNumber ?? null;
112
113
  if (plan.action === "created") {
113
- let created;
114
- try {
115
- ({ data: created } = await client.POST("/v1/agents", {
116
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
- body: agentBody(config),
118
- }));
119
- }
120
- catch (err) {
121
- if (err instanceof BackboneApiError && err.status === 500) {
122
- throw new BackboneApiError({
123
- status: err.status,
124
- error: err.errorType,
125
- message: `${err.message} — an agent named "${config.name}" may already exist`,
126
- timestamp: err.timestamp,
127
- code: err.code,
128
- });
129
- }
130
- throw err;
131
- }
114
+ // A duplicate name answers 409 with the backend's own message (#668).
115
+ const { data: created } = await client.POST("/v1/agents", {
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ body: agentBody(config),
118
+ });
132
119
  if (!created?.id)
133
120
  throw new Error("Agent was created but the response has no id");
134
121
  agentId = String(created.id);
122
+ // Create publishes version 1 (#668); an older backend does not, so publish as a fallback.
123
+ const head = await ensurePublished(client, created, versionBody(config, opts.message));
124
+ versionId = head.versionId;
125
+ version = head.version;
126
+ // A message given on the command line is kept in the history as a second version (D4);
127
+ // the default message is not worth a version of its own.
128
+ if (!head.fallback && command.getOptionValueSource("message") === "cli") {
129
+ const { data: v } = await client.POST("/v1/agents/{agentId}/versions", {
130
+ params: { path: { agentId } },
131
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
132
+ body: versionBody(config, opts.message),
133
+ });
134
+ versionId = v?.id;
135
+ version = v?.versionNumber ?? null;
136
+ }
135
137
  }
136
- if (plan.action === "created" || plan.action === "versioned") {
138
+ if (plan.action === "versioned") {
137
139
  const { data: v } = await client.POST("/v1/agents/{agentId}/versions", {
138
140
  params: { path: { agentId: agentId } },
139
141
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -10,6 +10,7 @@ import { makeAgentExportCommand } from "./agent-export.js";
10
10
  import { makeAgentPolicyCommand } from "./agent-policy.js";
11
11
  import { makeAgentInitCommand, makeAgentSchemaCommand } from "./agent-init.js";
12
12
  import { maskSecrets } from "../agent-config/file.js";
13
+ import { ensurePublished } from "../lib/agent-publish.js";
13
14
  import { collectModel, MODEL_HELP, MODELS_HELP, modelFieldsFromOptions } from "../lib/agent-models.js";
14
15
  import { assertNameNotBlank } from "../lib/overlay.js";
15
16
  export function makeAgentsCommand() {
@@ -58,7 +59,7 @@ export function makeAgentsCommand() {
58
59
  });
59
60
  cmd
60
61
  .command("create")
61
- .description("Create a new agent")
62
+ .description("Create a new agent; it is published as version 1 and runnable at once")
62
63
  .requiredOption("-n, --name <name>", "Agent name")
63
64
  .option("-m, --model <model>", MODEL_HELP, collectModel)
64
65
  .option("--models <list>", MODELS_HELP)
@@ -72,18 +73,38 @@ export function makeAgentsCommand() {
72
73
  await runAction(command, async () => {
73
74
  const models = modelFieldsFromOptions(opts, true);
74
75
  const client = getClient(command);
76
+ const tools = opts.tools ? JSON.parse(opts.tools) : undefined;
77
+ const skills = opts.skills ? JSON.parse(opts.skills) : undefined;
78
+ const options = opts.options ? JSON.parse(opts.options) : undefined;
79
+ const hitlPolicy = opts.hitlPolicy ? JSON.parse(opts.hitlPolicy) : undefined;
75
80
  const { data } = await client.POST("/v1/agents", {
76
81
  body: {
77
82
  name: opts.name,
78
83
  ...models,
79
84
  description: opts.description,
80
85
  instructions: opts.instructions,
81
- options: opts.options ? JSON.parse(opts.options) : undefined,
82
- tools: opts.tools ? JSON.parse(opts.tools) : undefined,
83
- skills: opts.skills ? JSON.parse(opts.skills) : undefined,
84
- hitlPolicy: opts.hitlPolicy ? JSON.parse(opts.hitlPolicy) : undefined,
86
+ options,
87
+ tools,
88
+ skills,
89
+ hitlPolicy,
85
90
  },
86
91
  });
92
+ if (data?.id) {
93
+ // Create publishes version 1; an older backend leaves it unpublished, so publish then.
94
+ const head = await ensurePublished(client, data, {
95
+ ...models,
96
+ instructions: opts.instructions,
97
+ options,
98
+ tools,
99
+ skills,
100
+ hitlPolicy,
101
+ changeDescription: "Initial version",
102
+ });
103
+ // Report the head the fallback just published, not the pre-publish create response.
104
+ if (head.fallback && head.versionId) {
105
+ Object.assign(data, { latestVersionId: head.versionId, latestVersionNumber: head.version ?? undefined });
106
+ }
107
+ }
87
108
  formatDetail(maskSecrets(data, []), command);
88
109
  });
89
110
  });
@@ -0,0 +1,15 @@
1
+ import type { AgentDTO, ApiClient } from "./agent-lookup.js";
2
+ export interface PublishedHead {
3
+ versionId: string | undefined;
4
+ version: number | null;
5
+ }
6
+ /**
7
+ * The head of a just-created agent. A backend that publishes version 1 on create (#668) answers with
8
+ * `latestVersionId`: use it. An older backend leaves the agent unpublished, so publish with
9
+ * `POST /v1/agents/{agentId}/versions` as before. One path, correct against either backend, so neither
10
+ * deploy order nor a backend rollback leaves a new agent unrunnable.
11
+ */
12
+ export declare function ensurePublished(client: ApiClient, created: AgentDTO, versionBody: Record<string, unknown>): Promise<PublishedHead & {
13
+ fallback: boolean;
14
+ }>;
15
+ //# sourceMappingURL=agent-publish.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The head of a just-created agent. A backend that publishes version 1 on create (#668) answers with
3
+ * `latestVersionId`: use it. An older backend leaves the agent unpublished, so publish with
4
+ * `POST /v1/agents/{agentId}/versions` as before. One path, correct against either backend, so neither
5
+ * deploy order nor a backend rollback leaves a new agent unrunnable.
6
+ */
7
+ export async function ensurePublished(client, created, versionBody) {
8
+ if (created.latestVersionId) {
9
+ return { versionId: created.latestVersionId, version: created.latestVersionNumber ?? 1, fallback: false };
10
+ }
11
+ const { data: v } = await client.POST("/v1/agents/{agentId}/versions", {
12
+ params: { path: { agentId: String(created.id) } },
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ body: versionBody,
15
+ });
16
+ return { versionId: v?.id, version: v?.versionNumber ?? null, fallback: true };
17
+ }
18
+ //# sourceMappingURL=agent-publish.js.map
@@ -94,6 +94,7 @@ const HINTS = {
94
94
  429: "Rate limit exceeded. Wait a moment and try again.",
95
95
  };
96
96
  const PENDING_APPROVALS_HINT = "The approval was already decided or its response was superseded. See what is still pending: 2kw agents approvals --agent <agent-id> --status pending";
97
+ const NOT_PUBLISHED_HINT = "The agent has no published version. Run: 2kw agents apply -f agent.yaml (or 2kw agents versions create).";
97
98
  const CODE_HINTS = {
98
99
  approval_hmac_mismatch: PENDING_APPROVALS_HINT,
99
100
  unknown_approval_id: PENDING_APPROVALS_HINT,
@@ -115,6 +116,10 @@ export function hintFor(err) {
115
116
  }
116
117
  if (err.code && CODE_HINTS[err.code])
117
118
  return CODE_HINTS[err.code];
119
+ // The gateway answers `agent_not_published` (code), the REST layer AGENT_NOT_PUBLISHED (errorCode).
120
+ if (err.code === "agent_not_published" || err.errorCode === "AGENT_NOT_PUBLISHED") {
121
+ return NOT_PUBLISHED_HINT;
122
+ }
118
123
  // The 403 whose cause is the credential, not the role (#805). Without this the
119
124
  // generic 403 hint tells an organization admin to check a role that is already
120
125
  // right, and no role would ever have fixed it. Matched on the backend's error code,
@@ -125,7 +130,7 @@ export function hintFor(err) {
125
130
  // Anchored on the agent service's wording: prompts and schemas send the same
126
131
  // "Label '<x>' not found" prefix, and must keep the generic 404 hint.
127
132
  if (err.status === 404 && /^Label 'latest' not found (on agent|for this agent)/.test(err.message)) {
128
- return "The agent has no published version. Run: 2kw agents apply -f agent.yaml (or 2kw agents versions create).";
133
+ return NOT_PUBLISHED_HINT;
129
134
  }
130
135
  if (err.status === 404 && /^Label '.+' not found (on agent|for this agent)/.test(err.message)) {
131
136
  return "That label does not exist on the agent. List labels: 2kw agents labels list --agent <agent-id>";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "7.1.0-dev.14",
3
+ "version": "7.1.0-dev.16",
4
4
  "description": "CLI for 2kw.ai — schema-driven document extraction, an OpenAI-compatible EU LLM gateway, transcription, prompts, datasets, and experiments from your terminal or agentic workflows. Ships as 2kw, backbone, and bb.",
5
5
  "keywords": [
6
6
  "cli",