@stigmer/cli 3.5.3 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/agent-exec-flags.d.ts +1 -0
- package/commands/agent-exec-flags.d.ts.map +1 -1
- package/commands/agent-exec-flags.js +3 -1
- package/commands/agent-exec-flags.js.map +1 -1
- package/commands/list.js +8 -5
- package/commands/list.js.map +1 -1
- package/commands/mcp-server.js +1 -1
- package/commands/resume.js +5 -1
- package/commands/resume.js.map +1 -1
- package/commands/schedule.d.ts +3 -0
- package/commands/schedule.d.ts.map +1 -0
- package/commands/schedule.js +57 -0
- package/commands/schedule.js.map +1 -0
- package/package.json +5 -5
- package/program.d.ts.map +1 -1
- package/program.js +2 -0
- package/program.js.map +1 -1
- package/registry/metadata.d.ts.map +1 -1
- package/registry/metadata.js +2 -0
- package/registry/metadata.js.map +1 -1
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +29 -21
- package/registry/verb-support.js.map +1 -1
- package/registry/verbs.d.ts.map +1 -1
- package/registry/verbs.js +0 -1
- package/registry/verbs.js.map +1 -1
- package/resources/apply/apply.d.ts +6 -1
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +14 -24
- package/resources/apply/apply.js.map +1 -1
- package/resources/apply/handlers.d.ts +9 -1
- package/resources/apply/handlers.d.ts.map +1 -1
- package/resources/apply/handlers.js +50 -148
- package/resources/apply/handlers.js.map +1 -1
- package/resources/delete.d.ts +12 -0
- package/resources/delete.d.ts.map +1 -1
- package/resources/delete.js +8 -4
- package/resources/delete.js.map +1 -1
- package/resources/execution.d.ts +14 -4
- package/resources/execution.d.ts.map +1 -1
- package/resources/execution.js +16 -6
- package/resources/execution.js.map +1 -1
- package/resources/get-bindings.d.ts +1 -0
- package/resources/get-bindings.d.ts.map +1 -1
- package/resources/get-bindings.js +8 -1
- package/resources/get-bindings.js.map +1 -1
- package/resources/list.d.ts +4 -0
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +95 -27
- package/resources/list.js.map +1 -1
- package/resources/reference.d.ts +2 -0
- package/resources/reference.d.ts.map +1 -1
- package/resources/reference.js +4 -0
- package/resources/reference.js.map +1 -1
- package/resources/run/agent-exec.d.ts.map +1 -1
- package/resources/run/agent-exec.js +1 -0
- package/resources/run/agent-exec.js.map +1 -1
- package/resources/run/create.d.ts +2 -1
- package/resources/run/create.d.ts.map +1 -1
- package/resources/run/create.js +12 -6
- package/resources/run/create.js.map +1 -1
- package/resources/run/prepare.d.ts +15 -0
- package/resources/run/prepare.d.ts.map +1 -1
- package/resources/run/prepare.js +13 -0
- package/resources/run/prepare.js.map +1 -1
- package/resources/schedule.d.ts +22 -0
- package/resources/schedule.d.ts.map +1 -0
- package/resources/schedule.js +114 -0
- package/resources/schedule.js.map +1 -0
- package/src/commands/agent-exec-flags.ts +5 -2
- package/src/commands/list.ts +8 -5
- package/src/commands/mcp-server.ts +1 -1
- package/src/commands/resume.ts +8 -1
- package/src/commands/schedule.ts +70 -0
- package/src/program.ts +2 -0
- package/src/registry/metadata.ts +2 -0
- package/src/registry/registry.test.ts +113 -8
- package/src/registry/verb-support.ts +29 -21
- package/src/registry/verbs.ts +0 -1
- package/src/resources/apply/apply.integration.test.ts +132 -0
- package/src/resources/apply/apply.ts +16 -25
- package/src/resources/apply/handlers.ts +60 -150
- package/src/resources/delete.ts +8 -4
- package/src/resources/execution.ts +20 -6
- package/src/resources/get-bindings.ts +8 -1
- package/src/resources/list.ts +107 -31
- package/src/resources/mutate-verbs.integration.test.ts +78 -4
- package/src/resources/read-verbs.integration.test.ts +124 -5
- package/src/resources/reference.ts +5 -0
- package/src/resources/run/agent-exec.test.ts +1 -0
- package/src/resources/run/agent-exec.ts +1 -0
- package/src/resources/run/create.test.ts +44 -1
- package/src/resources/run/create.ts +16 -7
- package/src/resources/run/prepare.test.ts +14 -1
- package/src/resources/run/prepare.ts +25 -0
- package/src/resources/schedule.ts +132 -0
|
@@ -34,9 +34,15 @@ import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/e
|
|
|
34
34
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
35
35
|
import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
|
|
36
36
|
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
37
|
+
import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
38
|
+
import { ScheduleCommandController } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/command_pb";
|
|
39
|
+
import { ScheduleQueryController } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/query_pb";
|
|
37
40
|
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
38
41
|
import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/command_pb";
|
|
39
42
|
import { WorkflowQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/query_pb";
|
|
43
|
+
import { WorkflowInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/api_pb";
|
|
44
|
+
import { WorkflowInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/command_pb";
|
|
45
|
+
import { WorkflowInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/query_pb";
|
|
40
46
|
import { createNodeClient, normalizeEndpoint } from "@stigmer/sdk/node";
|
|
41
47
|
import type { Stigmer } from "@stigmer/sdk";
|
|
42
48
|
import { createServer as createHttp2Server, type Http2Server, type ServerHttp2Session } from "node:http2";
|
|
@@ -85,6 +91,14 @@ const knownChannelApp = create(ChannelAppSchema, {
|
|
|
85
91
|
metadata: { name: "clinic-meta-app", slug: "clinic-meta-app", org: "acme", id: "chapp_1" },
|
|
86
92
|
});
|
|
87
93
|
|
|
94
|
+
const knownSchedule = create(ScheduleSchema, {
|
|
95
|
+
metadata: { name: "daily-fee-reminders", slug: "daily-fee-reminders", org: "acme", id: "sch_1" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const knownWorkflowInstance = create(WorkflowInstanceSchema, {
|
|
99
|
+
metadata: { name: "deploy-default", slug: "deploy-default", org: "acme", id: "win_1" },
|
|
100
|
+
});
|
|
101
|
+
|
|
88
102
|
// Pending execution (cancellable) vs. an already-terminal one.
|
|
89
103
|
const pendingExecution = create(AgentExecutionSchema, {
|
|
90
104
|
metadata: { id: "aex_run" },
|
|
@@ -110,8 +124,11 @@ let tagCalls: { workflowId: string; versionHash: string; tag: string }[] = [];
|
|
|
110
124
|
let datastoreDeletes: { resourceId: string; force: boolean }[] = [];
|
|
111
125
|
let environmentDeletes: { resourceId: string; force: boolean }[] = [];
|
|
112
126
|
let channelAppDeletes: { resourceId: string; force: boolean }[] = [];
|
|
113
|
-
// agent_channel
|
|
127
|
+
// agent_channel, schedule, and workflow_instance delete by typed ID — no
|
|
128
|
+
// force field exists on their wire contracts.
|
|
114
129
|
let channelDeleteIds: string[] = [];
|
|
130
|
+
let scheduleDeleteIds: string[] = [];
|
|
131
|
+
let workflowInstanceDeleteIds: string[] = [];
|
|
115
132
|
|
|
116
133
|
beforeEach(() => {
|
|
117
134
|
cancelCalls = [];
|
|
@@ -120,6 +137,8 @@ beforeEach(() => {
|
|
|
120
137
|
environmentDeletes = [];
|
|
121
138
|
channelAppDeletes = [];
|
|
122
139
|
channelDeleteIds = [];
|
|
140
|
+
scheduleDeleteIds = [];
|
|
141
|
+
workflowInstanceDeleteIds = [];
|
|
123
142
|
});
|
|
124
143
|
|
|
125
144
|
beforeAll(async () => {
|
|
@@ -216,6 +235,32 @@ beforeAll(async () => {
|
|
|
216
235
|
},
|
|
217
236
|
});
|
|
218
237
|
|
|
238
|
+
router.service(ScheduleQueryController, {
|
|
239
|
+
getByReference: (req) => {
|
|
240
|
+
if (req.slug !== "daily-fee-reminders") throw new ConnectError("schedule not found", Code.NotFound);
|
|
241
|
+
return knownSchedule;
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
router.service(ScheduleCommandController, {
|
|
245
|
+
delete: (req) => {
|
|
246
|
+
scheduleDeleteIds.push(req.value);
|
|
247
|
+
return knownSchedule;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
router.service(WorkflowInstanceQueryController, {
|
|
252
|
+
getByReference: (req) => {
|
|
253
|
+
if (req.slug !== "deploy-default") throw new ConnectError("workflow instance not found", Code.NotFound);
|
|
254
|
+
return knownWorkflowInstance;
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
router.service(WorkflowInstanceCommandController, {
|
|
258
|
+
delete: (req) => {
|
|
259
|
+
workflowInstanceDeleteIds.push(req.value);
|
|
260
|
+
return knownWorkflowInstance;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
219
264
|
router.service(WorkflowQueryController, {
|
|
220
265
|
getByReference: (req) => {
|
|
221
266
|
if (req.slug !== "deploy") throw new ConnectError("workflow not found", Code.NotFound);
|
|
@@ -306,22 +351,51 @@ describe("delete (standard kinds)", () => {
|
|
|
306
351
|
expect(classify(err)?.exitCode).toBe(ExitCode.NotFound);
|
|
307
352
|
});
|
|
308
353
|
|
|
354
|
+
it("deletes a schedule via its typed ID (tears down the Temporal artifact server-side)", async () => {
|
|
355
|
+
const plan = await planDelete(client, "schedule", "daily-fee-reminders", "acme");
|
|
356
|
+
const result = await plan.perform();
|
|
357
|
+
expect(result.status).toBe("success");
|
|
358
|
+
expect(result.message).toBe("Schedule deleted successfully");
|
|
359
|
+
expect(scheduleDeleteIds).toEqual(["sch_1"]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("deletes a workflow instance via its typed ID", async () => {
|
|
363
|
+
const plan = await planDelete(client, "workflow-instance", "deploy-default", "acme");
|
|
364
|
+
const result = await plan.perform();
|
|
365
|
+
expect(result.status).toBe("success");
|
|
366
|
+
expect(result.message).toBe("Workflow Instance deleted successfully");
|
|
367
|
+
expect(workflowInstanceDeleteIds).toEqual(["win_1"]);
|
|
368
|
+
});
|
|
369
|
+
|
|
309
370
|
it("rejects an unknown type and lists exactly the wired types", async () => {
|
|
310
371
|
const err = await planDelete(client, "bogus", "x", "acme").catch((e) => e);
|
|
311
372
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
312
373
|
// The list is derived from DELETE_HANDLERS (+ the two special cases), so
|
|
313
|
-
// it must name the wired kinds and never the
|
|
374
|
+
// it must name the wired kinds and never the narrowed ones (#354).
|
|
314
375
|
const message = String(err.message);
|
|
315
|
-
for (const wired of [
|
|
376
|
+
for (const wired of [
|
|
377
|
+
"datastore",
|
|
378
|
+
"environment",
|
|
379
|
+
"agentchannel",
|
|
380
|
+
"channelapp",
|
|
381
|
+
"schedule",
|
|
382
|
+
"workflowinstance",
|
|
383
|
+
"execution",
|
|
384
|
+
"organization",
|
|
385
|
+
]) {
|
|
316
386
|
expect(message).toContain(wired);
|
|
317
387
|
}
|
|
318
388
|
expect(message).not.toContain("session");
|
|
319
389
|
expect(message).not.toContain("oauthapp");
|
|
320
390
|
});
|
|
321
391
|
|
|
322
|
-
it("rejects a kind
|
|
392
|
+
it("rejects a narrowed kind at the verb gate with a usage error", async () => {
|
|
393
|
+
// session's delete verb is narrowed out of the matrix
|
|
394
|
+
// (stigmer/stigmer#354), so the refusal now happens at the verb-support
|
|
395
|
+
// gate ("does not support") rather than the dispatch fall-through.
|
|
323
396
|
const err = await planDelete(client, "session", "ses_1", "acme").catch((e) => e);
|
|
324
397
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
398
|
+
expect(String(err.message)).toContain("does not support");
|
|
325
399
|
});
|
|
326
400
|
});
|
|
327
401
|
|
|
@@ -21,6 +21,8 @@ import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/
|
|
|
21
21
|
import { ChannelAppQueryController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/query_pb";
|
|
22
22
|
import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
23
23
|
import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/query_pb";
|
|
24
|
+
import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
25
|
+
import { ScheduleQueryController } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/query_pb";
|
|
24
26
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
25
27
|
import { ApiKeySchema } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
|
|
26
28
|
import { ApiKeyQueryController } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/query_pb";
|
|
@@ -142,6 +144,44 @@ const secondChannelApp = create(ChannelAppSchema, {
|
|
|
142
144
|
},
|
|
143
145
|
});
|
|
144
146
|
|
|
147
|
+
// Spec/status populated to exercise every SCHEDULE_TABLE column — and the
|
|
148
|
+
// exact condition the table exists to surface (stigmer/stigmer#352): the
|
|
149
|
+
// owner's switch is ON (spec.enabled) while the platform's failure latch is
|
|
150
|
+
// SET (status.paused_reason), two different states with two different
|
|
151
|
+
// remedies.
|
|
152
|
+
const pausedSchedule = create(ScheduleSchema, {
|
|
153
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
154
|
+
kind: "Schedule",
|
|
155
|
+
metadata: { name: "daily-fee-reminders", slug: "daily-fee-reminders", org: "acme", id: "sch_1" },
|
|
156
|
+
spec: {
|
|
157
|
+
cron: "0 9 * * *",
|
|
158
|
+
timeZone: "Asia/Kolkata",
|
|
159
|
+
enabled: true,
|
|
160
|
+
target: {
|
|
161
|
+
case: "agent",
|
|
162
|
+
value: { agentRef: { kind: ApiResourceKind.agent, org: "acme", slug: "clinic-assistant" } },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
status: { pausedReason: "5 consecutive failed runs", consecutiveFailures: 5 },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Owner-disabled and never fired: empty status, so the row must derive
|
|
169
|
+
// "active" from an absent paused_reason and print no bogus timestamps.
|
|
170
|
+
const disabledSchedule = create(ScheduleSchema, {
|
|
171
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
172
|
+
kind: "Schedule",
|
|
173
|
+
metadata: { name: "weekly-digest", slug: "weekly-digest", org: "acme", id: "sch_2" },
|
|
174
|
+
spec: {
|
|
175
|
+
cron: "30 6 * * 1",
|
|
176
|
+
timeZone: "UTC",
|
|
177
|
+
enabled: false,
|
|
178
|
+
target: {
|
|
179
|
+
case: "agent",
|
|
180
|
+
value: { agentRef: { kind: ApiResourceKind.agent, org: "acme", slug: "reviewer" } },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
145
185
|
let backend: Http2Server;
|
|
146
186
|
let client: Stigmer;
|
|
147
187
|
const openSessions = new Set<ServerHttp2Session>();
|
|
@@ -226,6 +266,20 @@ beforeAll(async () => {
|
|
|
226
266
|
return { entries: [knownChannelApp, secondChannelApp] };
|
|
227
267
|
},
|
|
228
268
|
});
|
|
269
|
+
router.service(ScheduleQueryController, {
|
|
270
|
+
get: (req) => {
|
|
271
|
+
if (req.value !== "sch_1") throw new ConnectError("schedule not found", Code.NotFound);
|
|
272
|
+
return pausedSchedule;
|
|
273
|
+
},
|
|
274
|
+
getByReference: (req) => {
|
|
275
|
+
if (req.slug !== "daily-fee-reminders") throw new ConnectError("schedule not found", Code.NotFound);
|
|
276
|
+
return pausedSchedule;
|
|
277
|
+
},
|
|
278
|
+
list: (req) => {
|
|
279
|
+
if (req.org !== "acme") throw new ConnectError("org is required", Code.InvalidArgument);
|
|
280
|
+
return { totalCount: 2, items: [pausedSchedule, disabledSchedule] };
|
|
281
|
+
},
|
|
282
|
+
});
|
|
229
283
|
};
|
|
230
284
|
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
231
285
|
backend.on("session", (session) => {
|
|
@@ -341,6 +395,27 @@ describe("get integration", () => {
|
|
|
341
395
|
});
|
|
342
396
|
});
|
|
343
397
|
|
|
398
|
+
it("fetches a schedule by org/slug and renders backend protojson", async () => {
|
|
399
|
+
const { schema, message } = await fetchResource(client, ApiResourceKind.schedule, {
|
|
400
|
+
kind: "ref",
|
|
401
|
+
org: "acme",
|
|
402
|
+
slug: "daily-fee-reminders",
|
|
403
|
+
});
|
|
404
|
+
const rendered = JSON.parse(renderResource(schema, message, "json"));
|
|
405
|
+
expect(rendered).toEqual(toJson(ScheduleSchema, pausedSchedule, { useProtoFieldName: true }));
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("fetches a schedule by ID, exposing the pause diagnosis get exists for", async () => {
|
|
409
|
+
const { message } = await fetchResource(client, ApiResourceKind.schedule, { kind: "id", id: "sch_1" });
|
|
410
|
+
// The full resource is where an operator reads WHY the platform paused
|
|
411
|
+
// a schedule (paused_reason + the streak) — the list table only flags it.
|
|
412
|
+
expect(JSON.parse(renderResource(ScheduleSchema, message, "json"))).toMatchObject({
|
|
413
|
+
metadata: { id: "sch_1" },
|
|
414
|
+
spec: { enabled: true },
|
|
415
|
+
status: { paused_reason: "5 consecutive failed runs", consecutive_failures: 5 },
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
344
419
|
it("maps a NotFound backend error to ExitCode.NotFound", async () => {
|
|
345
420
|
const err = await fetchResource(client, ApiResourceKind.agent, { kind: "id", id: "missing" }).catch((e) => e);
|
|
346
421
|
const classified = classify(err);
|
|
@@ -348,8 +423,11 @@ describe("get integration", () => {
|
|
|
348
423
|
});
|
|
349
424
|
|
|
350
425
|
it("rejects an unsupported kind with a usage error", async () => {
|
|
351
|
-
// session
|
|
352
|
-
//
|
|
426
|
+
// session's read verbs are narrowed out of the verb matrix
|
|
427
|
+
// (stigmer/stigmer#354): the command layer refuses at the gate, and this
|
|
428
|
+
// resource-layer fallback answers anyone bypassing the gate. Wiring
|
|
429
|
+
// session get requires BOTH the binding and the matrix line — the
|
|
430
|
+
// registry conformance test enforces the pairing.
|
|
353
431
|
const err = await fetchResource(client, ApiResourceKind.session, { kind: "id", id: "x" }).catch((e) => e);
|
|
354
432
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
355
433
|
});
|
|
@@ -441,10 +519,51 @@ describe("list integration", () => {
|
|
|
441
519
|
expect(JSON.parse(out)).toEqual([toJson(ChannelAppSchema, knownChannelApp, { useProtoFieldName: true })]);
|
|
442
520
|
});
|
|
443
521
|
|
|
522
|
+
it("lists schedules as a table keeping the owner switch and the platform latch distinct", async () => {
|
|
523
|
+
const out = await listResources(client, ApiResourceKind.schedule, "acme", 50, "table");
|
|
524
|
+
for (const header of ["ID", "SLUG", "TARGET", "CRON", "TZ", "ENABLED", "STATE"]) {
|
|
525
|
+
expect(out).toContain(header);
|
|
526
|
+
}
|
|
527
|
+
const rows = out.split("\n");
|
|
528
|
+
// The stigmer/stigmer#352 condition: enabled by the owner, paused by the
|
|
529
|
+
// platform. Collapsing these into one column would hide exactly this.
|
|
530
|
+
const pausedRow = rows.find((line) => line.includes("sch_1"));
|
|
531
|
+
expect(pausedRow).toContain("acme/clinic-assistant");
|
|
532
|
+
expect(pausedRow).toContain("0 9 * * *");
|
|
533
|
+
expect(pausedRow).toContain("Asia/Kolkata");
|
|
534
|
+
expect(pausedRow).toContain("true");
|
|
535
|
+
expect(pausedRow).toContain("paused");
|
|
536
|
+
// Owner-disabled with an empty status: derived state is "active" (no
|
|
537
|
+
// platform latch), and the absent timestamps render nothing bogus.
|
|
538
|
+
const disabledRow = rows.find((line) => line.includes("sch_2"));
|
|
539
|
+
expect(disabledRow).toContain("false");
|
|
540
|
+
expect(disabledRow).toContain("active");
|
|
541
|
+
expect(disabledRow).not.toContain("paused");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("lists schedules as JSON matching backend protojson", async () => {
|
|
545
|
+
const out = await listResources(client, ApiResourceKind.schedule, "acme", 50, "json");
|
|
546
|
+
expect(JSON.parse(out)).toEqual([
|
|
547
|
+
toJson(ScheduleSchema, pausedSchedule, { useProtoFieldName: true }),
|
|
548
|
+
toJson(ScheduleSchema, disabledSchedule, { useProtoFieldName: true }),
|
|
549
|
+
]);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("refuses to list schedules without an org context, before any RPC", async () => {
|
|
553
|
+
// ListSchedulesRequest requires org (min_len 1); an unset cloud context
|
|
554
|
+
// resolves to "". The branch refuses with actionable copy instead of
|
|
555
|
+
// relaying the server's raw validation error.
|
|
556
|
+
const err = await listResources(client, ApiResourceKind.schedule, "", 50, "table").catch((e) => e);
|
|
557
|
+
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
558
|
+
expect(String(err.message)).toContain("--org");
|
|
559
|
+
});
|
|
560
|
+
|
|
444
561
|
it("rejects a kind whose list is unwired with a usage error", async () => {
|
|
445
|
-
// agent_share
|
|
446
|
-
// the
|
|
447
|
-
//
|
|
562
|
+
// agent_share's read verbs are narrowed out of the verb matrix
|
|
563
|
+
// (stigmer/stigmer#354): the command layer refuses at the gate, and this
|
|
564
|
+
// resource-layer fallback answers anyone bypassing the gate. Wiring
|
|
565
|
+
// agent_share list requires BOTH the LIST_HANDLERS entry and the matrix
|
|
566
|
+
// line — the registry conformance test enforces the pairing.
|
|
448
567
|
const err = await listResources(client, ApiResourceKind.agent_share, "acme", 50, "json").catch((e) => e);
|
|
449
568
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
450
569
|
});
|
|
@@ -86,6 +86,11 @@ export function isSessionId(ref: string): boolean {
|
|
|
86
86
|
return hasKindPrefix(ref, idPrefixFor(ApiResourceKind.session));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** True if `ref` carries the schedule ID prefix (`sch_…`/`sch-…`). */
|
|
90
|
+
export function isScheduleId(ref: string): boolean {
|
|
91
|
+
return hasKindPrefix(ref, idPrefixFor(ApiResourceKind.schedule));
|
|
92
|
+
}
|
|
93
|
+
|
|
89
94
|
/**
|
|
90
95
|
* True if `ref` starts with ANY known resource-ID prefix (length-agnostic).
|
|
91
96
|
* Mirrors Go's reference.HasResourceIDPrefix: use it to detect *intent* (the
|
|
@@ -42,6 +42,7 @@ export async function executeResolvedAgent(input: ResolvedAgentExecInput): Promi
|
|
|
42
42
|
workspaceEntries: prepared.workspaceEntries,
|
|
43
43
|
model: prepared.model,
|
|
44
44
|
mode: prepared.mode,
|
|
45
|
+
serviceTier: prepared.serviceTier,
|
|
45
46
|
autoApproveAll: prepared.autoApproveAll,
|
|
46
47
|
});
|
|
47
48
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// is faked to capture the exact proto sent to the RPC.
|
|
5
5
|
|
|
6
6
|
import { describe, expect, it } from "vitest";
|
|
7
|
-
import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
|
+
import { InteractionMode, ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
8
8
|
import { create } from "@bufbuild/protobuf";
|
|
9
9
|
import {
|
|
10
10
|
LocalPathSourceSchema,
|
|
@@ -40,6 +40,7 @@ describe("createAgentExecution", () => {
|
|
|
40
40
|
workspaceEntries: [],
|
|
41
41
|
model: "claude",
|
|
42
42
|
mode: "plan",
|
|
43
|
+
serviceTier: "",
|
|
43
44
|
autoApproveAll: true,
|
|
44
45
|
});
|
|
45
46
|
|
|
@@ -67,12 +68,51 @@ describe("createAgentExecution", () => {
|
|
|
67
68
|
workspaceEntries: [],
|
|
68
69
|
model: "",
|
|
69
70
|
mode: "",
|
|
71
|
+
serviceTier: "",
|
|
70
72
|
autoApproveAll: false,
|
|
71
73
|
});
|
|
72
74
|
expect(exec.spec?.message).toBe("hi");
|
|
73
75
|
expect(exec.spec?.executionConfig).toBeUndefined();
|
|
74
76
|
});
|
|
75
77
|
|
|
78
|
+
it("maps --service-tier fast to the enum (#357)", async () => {
|
|
79
|
+
const { fn } = fakeController();
|
|
80
|
+
const exec = await createAgentExecution(fn, {
|
|
81
|
+
agentId: "agt_1",
|
|
82
|
+
orgId: "acme",
|
|
83
|
+
message: "x",
|
|
84
|
+
runtimeEnv: {},
|
|
85
|
+
attachments: [],
|
|
86
|
+
workspaceFileRefs: [],
|
|
87
|
+
workspaceEntries: [],
|
|
88
|
+
model: "composer-2.5",
|
|
89
|
+
mode: "",
|
|
90
|
+
serviceTier: "fast",
|
|
91
|
+
autoApproveAll: false,
|
|
92
|
+
});
|
|
93
|
+
expect(exec.spec?.executionConfig?.serviceTier).toBe(ServiceTier.FAST);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("maps an explicit --service-tier standard to STANDARD, not UNSPECIFIED", async () => {
|
|
97
|
+
// Unspecified-vs-explicit-standard is a load-bearing ledger
|
|
98
|
+
// distinction (#357): an explicit choice must survive to the proto.
|
|
99
|
+
const { fn } = fakeController();
|
|
100
|
+
const exec = await createAgentExecution(fn, {
|
|
101
|
+
agentId: "agt_1",
|
|
102
|
+
orgId: "acme",
|
|
103
|
+
message: "x",
|
|
104
|
+
runtimeEnv: {},
|
|
105
|
+
attachments: [],
|
|
106
|
+
workspaceFileRefs: [],
|
|
107
|
+
workspaceEntries: [],
|
|
108
|
+
model: "",
|
|
109
|
+
mode: "",
|
|
110
|
+
serviceTier: "standard",
|
|
111
|
+
autoApproveAll: false,
|
|
112
|
+
});
|
|
113
|
+
expect(exec.spec?.executionConfig?.serviceTier).toBe(ServiceTier.STANDARD);
|
|
114
|
+
});
|
|
115
|
+
|
|
76
116
|
it("leaves InteractionMode unspecified for agent mode", async () => {
|
|
77
117
|
const { fn } = fakeController();
|
|
78
118
|
const exec = await createAgentExecution(fn, {
|
|
@@ -85,6 +125,7 @@ describe("createAgentExecution", () => {
|
|
|
85
125
|
workspaceEntries: [],
|
|
86
126
|
model: "m",
|
|
87
127
|
mode: "agent",
|
|
128
|
+
serviceTier: "",
|
|
88
129
|
autoApproveAll: false,
|
|
89
130
|
});
|
|
90
131
|
expect(exec.spec?.sessionId).toBe("ses_1");
|
|
@@ -110,6 +151,7 @@ describe("createAgentExecution", () => {
|
|
|
110
151
|
workspaceEntries: [entry],
|
|
111
152
|
model: "",
|
|
112
153
|
mode: "",
|
|
154
|
+
serviceTier: "",
|
|
113
155
|
autoApproveAll: false,
|
|
114
156
|
});
|
|
115
157
|
|
|
@@ -132,6 +174,7 @@ describe("createAgentExecution", () => {
|
|
|
132
174
|
workspaceEntries: [],
|
|
133
175
|
model: "",
|
|
134
176
|
mode: "",
|
|
177
|
+
serviceTier: "",
|
|
135
178
|
autoApproveAll: false,
|
|
136
179
|
});
|
|
137
180
|
expect(exec.spec?.sessionSpec).toBeUndefined();
|
|
@@ -10,7 +10,7 @@ import type { Client } from "@connectrpc/connect";
|
|
|
10
10
|
import { create, type DescService } from "@bufbuild/protobuf";
|
|
11
11
|
import { type AgentExecution, AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
12
12
|
import { AgentExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/command_pb";
|
|
13
|
-
import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
13
|
+
import { InteractionMode, ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
14
14
|
import type { Attachment } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
15
15
|
import {
|
|
16
16
|
AgentExecutionSpecSchema,
|
|
@@ -29,7 +29,7 @@ import { WorkflowExecutionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/
|
|
|
29
29
|
import { ExecutionValueSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/spec_pb";
|
|
30
30
|
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
31
31
|
import type { RuntimeEnv } from "./env.js";
|
|
32
|
-
import type { RunMode } from "./prepare.js";
|
|
32
|
+
import type { RunMode, ServiceTierFlag } from "./prepare.js";
|
|
33
33
|
|
|
34
34
|
const API_VERSION = "agentic.stigmer.ai/v1";
|
|
35
35
|
|
|
@@ -57,6 +57,7 @@ export interface CreateAgentExecutionInput {
|
|
|
57
57
|
readonly workspaceEntries: readonly WorkspaceEntry[];
|
|
58
58
|
readonly model: string;
|
|
59
59
|
readonly mode: RunMode;
|
|
60
|
+
readonly serviceTier: ServiceTierFlag;
|
|
60
61
|
readonly autoApproveAll: boolean;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -83,7 +84,7 @@ export async function createAgentExecution(
|
|
|
83
84
|
input.workspaceEntries.length > 0
|
|
84
85
|
? create(SessionSpecSchema, { workspaceEntries: [...input.workspaceEntries] })
|
|
85
86
|
: undefined,
|
|
86
|
-
executionConfig: buildExecutionConfig(input.model, input.mode),
|
|
87
|
+
executionConfig: buildExecutionConfig(input.model, input.mode, input.serviceTier),
|
|
87
88
|
}),
|
|
88
89
|
});
|
|
89
90
|
return controller(AgentExecutionCommandController).create(execution);
|
|
@@ -119,14 +120,22 @@ export async function createWorkflowExecution(
|
|
|
119
120
|
return controller(WorkflowExecutionCommandController).create(execution);
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
// Build ExecutionConfig, or undefined when
|
|
123
|
+
// Build ExecutionConfig, or undefined when no flag is set so the backend
|
|
123
124
|
// applies its defaults. Mirrors Go's buildExecutionConfig (only "plan" maps to a
|
|
124
|
-
// non-default InteractionMode; "agent"/"" leave it unspecified).
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
// non-default InteractionMode; "agent"/"" leave it unspecified). An explicit
|
|
126
|
+
// --service-tier value maps to the enum even for "standard": unspecified vs
|
|
127
|
+
// explicit-standard is a load-bearing ledger distinction (#357).
|
|
128
|
+
function buildExecutionConfig(
|
|
129
|
+
model: string,
|
|
130
|
+
mode: RunMode,
|
|
131
|
+
serviceTier: ServiceTierFlag,
|
|
132
|
+
): ExecutionConfig | undefined {
|
|
133
|
+
if (model === "" && mode === "" && serviceTier === "") return undefined;
|
|
127
134
|
const cfg = create(ExecutionConfigSchema);
|
|
128
135
|
if (model !== "") cfg.modelName = model;
|
|
129
136
|
if (mode === "plan") cfg.interactionMode = InteractionMode.PLAN;
|
|
137
|
+
if (serviceTier === "fast") cfg.serviceTier = ServiceTier.FAST;
|
|
138
|
+
else if (serviceTier === "standard") cfg.serviceTier = ServiceTier.STANDARD;
|
|
130
139
|
return cfg;
|
|
131
140
|
}
|
|
132
141
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
7
|
import type { Stigmer } from "@stigmer/sdk";
|
|
8
|
-
import { type AgentExecFlags, parseApprovalAction, prepareAgentExec, validateMode } from "./prepare.js";
|
|
8
|
+
import { type AgentExecFlags, parseApprovalAction, prepareAgentExec, validateMode, validateServiceTier } from "./prepare.js";
|
|
9
9
|
|
|
10
10
|
describe("parseApprovalAction", () => {
|
|
11
11
|
it("maps each accepted value (case-insensitive)", () => {
|
|
@@ -35,6 +35,18 @@ describe("validateMode", () => {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
describe("validateServiceTier", () => {
|
|
39
|
+
it("accepts empty, standard, and fast", () => {
|
|
40
|
+
expect(() => validateServiceTier("")).not.toThrow();
|
|
41
|
+
expect(() => validateServiceTier("standard")).not.toThrow();
|
|
42
|
+
expect(() => validateServiceTier("fast")).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects anything else before a network round trip", () => {
|
|
46
|
+
expect(() => validateServiceTier("turbo")).toThrow(/must be "standard" or "fast"/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
38
50
|
const BASE_FLAGS: AgentExecFlags = {
|
|
39
51
|
message: "hi",
|
|
40
52
|
attach: [],
|
|
@@ -51,6 +63,7 @@ const BASE_FLAGS: AgentExecFlags = {
|
|
|
51
63
|
model: "",
|
|
52
64
|
autoApprove: false,
|
|
53
65
|
mode: "",
|
|
66
|
+
serviceTier: "",
|
|
54
67
|
};
|
|
55
68
|
|
|
56
69
|
// prepareAgentExec only touches client.agentExecution for attachment uploads;
|
|
@@ -19,6 +19,13 @@ import { localWorkspaceRoots, parseWorkspaceEntries } from "./workspace.js";
|
|
|
19
19
|
/** The interaction mode requested for the run; "" means the agent default. */
|
|
20
20
|
export type RunMode = "" | "agent" | "plan";
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The `--service-tier` flag's value space (#357). Empty means unset —
|
|
24
|
+
* distinct from an explicit "standard" so the ledger keeps the
|
|
25
|
+
* unspecified-vs-explicit distinction.
|
|
26
|
+
*/
|
|
27
|
+
export type ServiceTierFlag = "" | "standard" | "fast";
|
|
28
|
+
|
|
22
29
|
/** Raw agent-execution flags shared by `run` and `draft` (Go's agentExecFlags). */
|
|
23
30
|
export interface AgentExecFlags {
|
|
24
31
|
readonly message: string;
|
|
@@ -36,6 +43,7 @@ export interface AgentExecFlags {
|
|
|
36
43
|
readonly model: string;
|
|
37
44
|
readonly autoApprove: boolean;
|
|
38
45
|
readonly mode: RunMode;
|
|
46
|
+
readonly serviceTier: ServiceTierFlag;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/**
|
|
@@ -54,6 +62,7 @@ export interface PreparedRun {
|
|
|
54
62
|
readonly model: string;
|
|
55
63
|
readonly autoApproveAll: boolean;
|
|
56
64
|
readonly mode: RunMode;
|
|
65
|
+
readonly serviceTier: ServiceTierFlag;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
/**
|
|
@@ -68,6 +77,7 @@ export async function prepareAgentExec(
|
|
|
68
77
|
): Promise<PreparedRun> {
|
|
69
78
|
const defaultAction = parseApprovalAction(flags.approveDefault);
|
|
70
79
|
validateMode(flags.mode);
|
|
80
|
+
validateServiceTier(flags.serviceTier);
|
|
71
81
|
|
|
72
82
|
const workspaceEntries = parseWorkspaceEntries(flags.workspace, flags.branch, flags.commit);
|
|
73
83
|
|
|
@@ -100,6 +110,7 @@ export async function prepareAgentExec(
|
|
|
100
110
|
model: flags.model,
|
|
101
111
|
autoApproveAll: flags.autoApprove,
|
|
102
112
|
mode: flags.mode,
|
|
113
|
+
serviceTier: flags.serviceTier,
|
|
103
114
|
};
|
|
104
115
|
}
|
|
105
116
|
|
|
@@ -137,3 +148,17 @@ export function validateMode(mode: string): asserts mode is RunMode {
|
|
|
137
148
|
throw new UsageError(`invalid --mode value "${mode}": must be "agent" or "plan"`);
|
|
138
149
|
}
|
|
139
150
|
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate the `--service-tier` flag. Empty means "platform default" (which
|
|
154
|
+
* resolves to standard — never the provider account default, #357). The
|
|
155
|
+
* server refuses fast on models without a registry fast variant; this check
|
|
156
|
+
* only catches spelling errors before a network round trip.
|
|
157
|
+
*/
|
|
158
|
+
export function validateServiceTier(tier: string): asserts tier is ServiceTierFlag {
|
|
159
|
+
if (tier !== "" && tier !== "standard" && tier !== "fast") {
|
|
160
|
+
throw new UsageError(
|
|
161
|
+
`invalid --service-tier value "${tier}": must be "standard" or "fast"`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|