@stigmer/cli 3.12.4 → 3.12.6
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/draft.js +4 -2
- package/commands/draft.js.map +1 -1
- package/commands/run.js +4 -2
- package/commands/run.js.map +1 -1
- package/commands/seedpack.d.ts.map +1 -1
- package/commands/seedpack.js +27 -5
- package/commands/seedpack.js.map +1 -1
- package/local/seedpack/apply.d.ts +32 -3
- package/local/seedpack/apply.d.ts.map +1 -1
- package/local/seedpack/apply.js +59 -8
- package/local/seedpack/apply.js.map +1 -1
- package/package.json +5 -5
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +10 -1
- package/resources/apply/apply.js.map +1 -1
- package/resources/connect/connect.d.ts +7 -0
- package/resources/connect/connect.d.ts.map +1 -1
- package/resources/connect/connect.js +47 -30
- package/resources/connect/connect.js.map +1 -1
- package/resources/connect/display.d.ts.map +1 -1
- package/resources/connect/display.js +3 -0
- package/resources/connect/display.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 +11 -6
- package/resources/run/create.js.map +1 -1
- package/resources/run/prepare.d.ts +27 -1
- package/resources/run/prepare.d.ts.map +1 -1
- package/resources/run/prepare.js +41 -2
- package/resources/run/prepare.js.map +1 -1
- package/resources/task-configs.d.ts +15 -0
- package/resources/task-configs.d.ts.map +1 -0
- package/resources/task-configs.js +144 -0
- package/resources/task-configs.js.map +1 -0
- package/resources/validate.d.ts.map +1 -1
- package/resources/validate.js +8 -1
- package/resources/validate.js.map +1 -1
- package/src/commands/agent-exec-flags.ts +5 -2
- package/src/commands/draft.ts +4 -2
- package/src/commands/run.ts +4 -2
- package/src/commands/seedpack.ts +27 -5
- package/src/local/seedpack/apply.test.ts +127 -6
- package/src/local/seedpack/apply.ts +69 -9
- package/src/resources/apply/apply.ts +11 -1
- package/src/resources/connect/connect.ts +70 -44
- package/src/resources/connect/display.ts +3 -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 +48 -1
- package/src/resources/run/create.ts +11 -6
- package/src/resources/run/prepare.test.ts +94 -1
- package/src/resources/run/prepare.ts +66 -1
- package/src/resources/task-configs.test.ts +98 -0
- package/src/resources/task-configs.ts +159 -0
- package/src/resources/validate.test.ts +26 -0
- package/src/resources/validate.ts +10 -2
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { create, fromJson, type JsonValue, type Message } from "@bufbuild/protobuf";
|
|
10
10
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
11
|
+
import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
11
12
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
12
13
|
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
13
14
|
import { UpdateVisibilityInputSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
@@ -19,6 +20,7 @@ import { UsageError } from "../../errors/index.js";
|
|
|
19
20
|
import { CommandResult } from "../../output/index.js";
|
|
20
21
|
import { defaultRegistry, Verb } from "../../registry/index.js";
|
|
21
22
|
import { loadDocuments, resolveYamlFiles } from "../documents.js";
|
|
23
|
+
import { decodeWorkflowTaskConfigs } from "../task-configs.js";
|
|
22
24
|
import { type ApplyHandler, APPLY_HANDLERS, type ControllerFn } from "./handlers.js";
|
|
23
25
|
|
|
24
26
|
export interface ApplyItem {
|
|
@@ -112,7 +114,15 @@ export async function applyItem(
|
|
|
112
114
|
*/
|
|
113
115
|
export function marshalItem(item: ApplyItem): Message {
|
|
114
116
|
try {
|
|
115
|
-
|
|
117
|
+
const message = fromJson(item.handler.schema, item.document, { ignoreUnknownFields: false });
|
|
118
|
+
// Workflow task_config blocks live inside an open Struct the top-level
|
|
119
|
+
// decode cannot see into; decode them per kind so a dry-run (and the
|
|
120
|
+
// reconciler's marshal) rejects exactly what a real apply rejects
|
|
121
|
+
// (stigmer/stigmer#778).
|
|
122
|
+
if (item.handler.kind === ApiResourceKind.workflow) {
|
|
123
|
+
decodeWorkflowTaskConfigs(message as Workflow);
|
|
124
|
+
}
|
|
125
|
+
return message;
|
|
116
126
|
} catch (err) {
|
|
117
127
|
throw new UsageError(`invalid ${item.handler.displayName} in ${item.filePath}: ${(err as Error).message}`);
|
|
118
128
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// `connect mcp-server` orchestration.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// `connect mcp-server` orchestration. Resolve the server, then either run the
|
|
2
|
+
// server-side connect (discovery + tool-approval classification, persisted on
|
|
3
|
+
// the resource) or, for --dry-run, discover locally and return without
|
|
4
|
+
// persisting. The server-side path uses the async lane — startConnect + poll
|
|
5
|
+
// (stigmer/stigmer#425) — with a blocking-RPC fallback for backends that
|
|
6
|
+
// predate it.
|
|
5
7
|
//
|
|
6
8
|
// OAuth: when a server requires OAuth, has no existing grant, and no --env was
|
|
7
9
|
// supplied, the interactive browser flow (oauth.ts) shepherds the user through
|
|
@@ -11,10 +13,12 @@
|
|
|
11
13
|
|
|
12
14
|
import { create } from "@bufbuild/protobuf";
|
|
13
15
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
16
|
+
import type { ConnectInput } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
|
|
14
17
|
import { ConnectInputSchema, GetOAuthGrantStatusInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
|
|
15
18
|
import type { DiscoveredCapabilities } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
|
|
16
19
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
17
20
|
import type { Stigmer } from "@stigmer/sdk";
|
|
21
|
+
import { connectAndWait, ConnectStillRunningError, CONNECT_SETTLE_BOUND_MS } from "@stigmer/sdk";
|
|
18
22
|
import type { BackendType } from "../../config/config.js";
|
|
19
23
|
import { CliExitError, ExitCode, UsageError } from "../../errors/index.js";
|
|
20
24
|
import { defaultRegistry } from "../../registry/index.js";
|
|
@@ -49,6 +53,13 @@ export interface ConnectResult {
|
|
|
49
53
|
readonly capabilities: DiscoveredCapabilities | undefined;
|
|
50
54
|
/** Set when capabilities were persisted (non-dry-run); undefined for dry-run. */
|
|
51
55
|
readonly updated: McpServer | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Start-time advisory from the backend's connect pre-flight (e.g. "no
|
|
58
|
+
* runner appears to be polling the task queue"). Only set on the async
|
|
59
|
+
* lane, and only when the operation ultimately settled anyway — surfaced
|
|
60
|
+
* so the user learns their runner came up late.
|
|
61
|
+
*/
|
|
62
|
+
readonly warning?: string;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
/** Connect to an MCP server and discover its capabilities (push or dry-run). */
|
|
@@ -75,50 +86,65 @@ export async function connectMcpServer(client: Stigmer, opts: ConnectOptions): P
|
|
|
75
86
|
|
|
76
87
|
await ensureOAuthSatisfied(client, server, opts);
|
|
77
88
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
85
|
-
const updated = opts.pushTimeoutMs === undefined
|
|
86
|
-
? await push
|
|
87
|
-
: await boundedPush(push, opts.pushTimeoutMs, server);
|
|
88
|
-
return { server, capabilities: updated.status?.discoveredCapabilities, updated };
|
|
89
|
+
const input = create(ConnectInputSchema, {
|
|
90
|
+
mcpServerId: server.metadata?.id ?? "",
|
|
91
|
+
org: opts.org,
|
|
92
|
+
runtimeEnv: buildRuntimeEnv(server, opts.envOverrides),
|
|
93
|
+
});
|
|
94
|
+
return serverSideConnect(client, server, input, opts);
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
97
|
+
// Run the server-side connect through the SDK's shared async-lane protocol
|
|
98
|
+
// (connectAndWait, stigmer/stigmer#425): startConnect + poll, with the
|
|
99
|
+
// blocking-RPC fallback for backends that predate the lane. The CLI's job is
|
|
100
|
+
// only to translate its --timeout semantics and turn a still-running stop
|
|
101
|
+
// into its own actionable guidance.
|
|
102
|
+
async function serverSideConnect(
|
|
103
|
+
client: Stigmer,
|
|
104
|
+
server: McpServer,
|
|
105
|
+
input: ConnectInput,
|
|
106
|
+
opts: ConnectOptions,
|
|
107
|
+
): Promise<ConnectResult> {
|
|
96
108
|
const slug = server.metadata?.slug ?? server.metadata?.id ?? "the server";
|
|
97
|
-
let
|
|
98
|
-
const timeout = new Promise<never>((_, reject) => {
|
|
99
|
-
timer = setTimeout(
|
|
100
|
-
() =>
|
|
101
|
-
reject(
|
|
102
|
-
new CliExitError(
|
|
103
|
-
`Stopped waiting for the connect of MCP server '${slug}' after ${timeoutMs / 1000}s (--timeout)`,
|
|
104
|
-
ExitCode.Connection,
|
|
105
|
-
[
|
|
106
|
-
"The server-side connect is still running and will persist its result if it succeeds.",
|
|
107
|
-
`Check the outcome with: stigmer get mcp-server ${slug}`,
|
|
108
|
-
"Re-run without --timeout to wait for completion.",
|
|
109
|
-
],
|
|
110
|
-
),
|
|
111
|
-
),
|
|
112
|
-
timeoutMs,
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
// The losing push may settle after the CLI has started exiting; swallow its
|
|
116
|
-
// late rejection so it cannot surface as an unhandled-rejection crash.
|
|
117
|
-
void push.catch(() => {});
|
|
109
|
+
let warning = "";
|
|
118
110
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
const updated = await connectAndWait(client.mcpServer, input, {
|
|
112
|
+
deadlineMs: opts.pushTimeoutMs,
|
|
113
|
+
onStarted: (started) => {
|
|
114
|
+
warning = started.status?.connectStatus?.warning ?? "";
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
server,
|
|
119
|
+
capabilities: updated.status?.discoveredCapabilities,
|
|
120
|
+
updated,
|
|
121
|
+
warning: warning !== "" ? warning : undefined,
|
|
122
|
+
};
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (!(err instanceof ConnectStillRunningError)) throw err;
|
|
125
|
+
if (opts.pushTimeoutMs !== undefined) {
|
|
126
|
+
// The user's explicit --timeout fired: historical soft-bound semantics.
|
|
127
|
+
throw new CliExitError(
|
|
128
|
+
`Stopped waiting for the connect of MCP server '${slug}' after ${opts.pushTimeoutMs / 1000}s (--timeout)`,
|
|
129
|
+
ExitCode.Connection,
|
|
130
|
+
[
|
|
131
|
+
"The server-side connect is still running and will persist its result if it succeeds.",
|
|
132
|
+
`Check the outcome with: stigmer get mcp-server ${slug}`,
|
|
133
|
+
"Re-run without --timeout to wait for completion.",
|
|
134
|
+
],
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
// The SDK's settle bound fired: past the backend's own ceiling, only a
|
|
138
|
+
// connect_status orphaned by a backend restart can still read CONNECTING.
|
|
139
|
+
throw new CliExitError(
|
|
140
|
+
`The connect of MCP server '${slug}' did not settle within ${CONNECT_SETTLE_BOUND_MS / 60_000} minutes`,
|
|
141
|
+
ExitCode.Connection,
|
|
142
|
+
[
|
|
143
|
+
"This usually means the backend restarted mid-operation.",
|
|
144
|
+
`Check the current state with: stigmer get mcp-server ${slug}`,
|
|
145
|
+
"Re-run the connect to start a fresh operation.",
|
|
146
|
+
],
|
|
147
|
+
);
|
|
122
148
|
}
|
|
123
149
|
}
|
|
124
150
|
|
|
@@ -26,6 +26,9 @@ export function renderConnectResult(result: ConnectResult, sink: ConnectSink, co
|
|
|
26
26
|
sink(result.updated !== undefined
|
|
27
27
|
? style.green("✓ Connected — capabilities and tool approvals saved")
|
|
28
28
|
: style.yellow("⚠ Dry run — results not saved to backend"));
|
|
29
|
+
if (result.warning !== undefined) {
|
|
30
|
+
sink(style.yellow(`⚠ ${result.warning}`));
|
|
31
|
+
}
|
|
29
32
|
sink("");
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -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, ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
|
+
import { InteractionMode, ServiceTier, ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
8
8
|
import { create } from "@bufbuild/protobuf";
|
|
9
9
|
import {
|
|
10
10
|
LocalPathSourceSchema,
|
|
@@ -41,6 +41,7 @@ describe("createAgentExecution", () => {
|
|
|
41
41
|
model: "claude",
|
|
42
42
|
mode: "plan",
|
|
43
43
|
serviceTier: "",
|
|
44
|
+
thinking: "",
|
|
44
45
|
autoApproveAll: true,
|
|
45
46
|
});
|
|
46
47
|
|
|
@@ -69,6 +70,7 @@ describe("createAgentExecution", () => {
|
|
|
69
70
|
model: "",
|
|
70
71
|
mode: "",
|
|
71
72
|
serviceTier: "",
|
|
73
|
+
thinking: "",
|
|
72
74
|
autoApproveAll: false,
|
|
73
75
|
});
|
|
74
76
|
expect(exec.spec?.message).toBe("hi");
|
|
@@ -88,6 +90,7 @@ describe("createAgentExecution", () => {
|
|
|
88
90
|
model: "composer-2.5",
|
|
89
91
|
mode: "",
|
|
90
92
|
serviceTier: "fast",
|
|
93
|
+
thinking: "",
|
|
91
94
|
autoApproveAll: false,
|
|
92
95
|
});
|
|
93
96
|
expect(exec.spec?.executionConfig?.serviceTier).toBe(ServiceTier.FAST);
|
|
@@ -108,11 +111,52 @@ describe("createAgentExecution", () => {
|
|
|
108
111
|
model: "",
|
|
109
112
|
mode: "",
|
|
110
113
|
serviceTier: "standard",
|
|
114
|
+
thinking: "",
|
|
111
115
|
autoApproveAll: false,
|
|
112
116
|
});
|
|
113
117
|
expect(exec.spec?.executionConfig?.serviceTier).toBe(ServiceTier.STANDARD);
|
|
114
118
|
});
|
|
115
119
|
|
|
120
|
+
it("maps --thinking enabled to the enum (#772)", async () => {
|
|
121
|
+
const { fn } = fakeController();
|
|
122
|
+
const exec = await createAgentExecution(fn, {
|
|
123
|
+
agentId: "agt_1",
|
|
124
|
+
orgId: "acme",
|
|
125
|
+
message: "x",
|
|
126
|
+
runtimeEnv: {},
|
|
127
|
+
attachments: [],
|
|
128
|
+
workspaceFileRefs: [],
|
|
129
|
+
workspaceEntries: [],
|
|
130
|
+
model: "claude-haiku-4-5",
|
|
131
|
+
mode: "",
|
|
132
|
+
serviceTier: "",
|
|
133
|
+
thinking: "enabled",
|
|
134
|
+
autoApproveAll: false,
|
|
135
|
+
});
|
|
136
|
+
expect(exec.spec?.executionConfig?.thinkingMode).toBe(ThinkingMode.ENABLED);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("maps an explicit --thinking disabled to DISABLED, not UNSPECIFIED", async () => {
|
|
140
|
+
// The tier's #772 twin: unspecified-vs-explicit-disabled is the same
|
|
141
|
+
// load-bearing ledger distinction.
|
|
142
|
+
const { fn } = fakeController();
|
|
143
|
+
const exec = await createAgentExecution(fn, {
|
|
144
|
+
agentId: "agt_1",
|
|
145
|
+
orgId: "acme",
|
|
146
|
+
message: "x",
|
|
147
|
+
runtimeEnv: {},
|
|
148
|
+
attachments: [],
|
|
149
|
+
workspaceFileRefs: [],
|
|
150
|
+
workspaceEntries: [],
|
|
151
|
+
model: "",
|
|
152
|
+
mode: "",
|
|
153
|
+
serviceTier: "",
|
|
154
|
+
thinking: "disabled",
|
|
155
|
+
autoApproveAll: false,
|
|
156
|
+
});
|
|
157
|
+
expect(exec.spec?.executionConfig?.thinkingMode).toBe(ThinkingMode.DISABLED);
|
|
158
|
+
});
|
|
159
|
+
|
|
116
160
|
it("leaves InteractionMode unspecified for agent mode", async () => {
|
|
117
161
|
const { fn } = fakeController();
|
|
118
162
|
const exec = await createAgentExecution(fn, {
|
|
@@ -126,6 +170,7 @@ describe("createAgentExecution", () => {
|
|
|
126
170
|
model: "m",
|
|
127
171
|
mode: "agent",
|
|
128
172
|
serviceTier: "",
|
|
173
|
+
thinking: "",
|
|
129
174
|
autoApproveAll: false,
|
|
130
175
|
});
|
|
131
176
|
expect(exec.spec?.sessionId).toBe("ses_1");
|
|
@@ -152,6 +197,7 @@ describe("createAgentExecution", () => {
|
|
|
152
197
|
model: "",
|
|
153
198
|
mode: "",
|
|
154
199
|
serviceTier: "",
|
|
200
|
+
thinking: "",
|
|
155
201
|
autoApproveAll: false,
|
|
156
202
|
});
|
|
157
203
|
|
|
@@ -175,6 +221,7 @@ describe("createAgentExecution", () => {
|
|
|
175
221
|
model: "",
|
|
176
222
|
mode: "",
|
|
177
223
|
serviceTier: "",
|
|
224
|
+
thinking: "",
|
|
178
225
|
autoApproveAll: false,
|
|
179
226
|
});
|
|
180
227
|
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, ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
13
|
+
import { InteractionMode, ServiceTier, ThinkingMode } 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, ServiceTierFlag } from "./prepare.js";
|
|
32
|
+
import type { RunMode, ServiceTierFlag, ThinkingFlag } from "./prepare.js";
|
|
33
33
|
|
|
34
34
|
const API_VERSION = "agentic.stigmer.ai/v1";
|
|
35
35
|
|
|
@@ -58,6 +58,7 @@ export interface CreateAgentExecutionInput {
|
|
|
58
58
|
readonly model: string;
|
|
59
59
|
readonly mode: RunMode;
|
|
60
60
|
readonly serviceTier: ServiceTierFlag;
|
|
61
|
+
readonly thinking: ThinkingFlag;
|
|
61
62
|
readonly autoApproveAll: boolean;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -84,7 +85,7 @@ export async function createAgentExecution(
|
|
|
84
85
|
input.workspaceEntries.length > 0
|
|
85
86
|
? create(SessionSpecSchema, { workspaceEntries: [...input.workspaceEntries] })
|
|
86
87
|
: undefined,
|
|
87
|
-
executionConfig: buildExecutionConfig(input.model, input.mode, input.serviceTier),
|
|
88
|
+
executionConfig: buildExecutionConfig(input.model, input.mode, input.serviceTier, input.thinking),
|
|
88
89
|
}),
|
|
89
90
|
});
|
|
90
91
|
return controller(AgentExecutionCommandController).create(execution);
|
|
@@ -123,19 +124,23 @@ export async function createWorkflowExecution(
|
|
|
123
124
|
// Build ExecutionConfig, or undefined when no flag is set so the backend
|
|
124
125
|
// applies its defaults. Mirrors Go's buildExecutionConfig (only "plan" maps to a
|
|
125
126
|
// non-default InteractionMode; "agent"/"" leave it unspecified). An explicit
|
|
126
|
-
// --service-tier value maps to the enum even for
|
|
127
|
-
//
|
|
127
|
+
// --service-tier or --thinking value maps to the enum even for the base
|
|
128
|
+
// choice ("standard"/"disabled"): unspecified vs explicit is a load-bearing
|
|
129
|
+
// ledger distinction (#357/#772).
|
|
128
130
|
function buildExecutionConfig(
|
|
129
131
|
model: string,
|
|
130
132
|
mode: RunMode,
|
|
131
133
|
serviceTier: ServiceTierFlag,
|
|
134
|
+
thinking: ThinkingFlag,
|
|
132
135
|
): ExecutionConfig | undefined {
|
|
133
|
-
if (model === "" && mode === "" && serviceTier === "") return undefined;
|
|
136
|
+
if (model === "" && mode === "" && serviceTier === "" && thinking === "") return undefined;
|
|
134
137
|
const cfg = create(ExecutionConfigSchema);
|
|
135
138
|
if (model !== "") cfg.modelName = model;
|
|
136
139
|
if (mode === "plan") cfg.interactionMode = InteractionMode.PLAN;
|
|
137
140
|
if (serviceTier === "fast") cfg.serviceTier = ServiceTier.FAST;
|
|
138
141
|
else if (serviceTier === "standard") cfg.serviceTier = ServiceTier.STANDARD;
|
|
142
|
+
if (thinking === "enabled") cfg.thinkingMode = ThinkingMode.ENABLED;
|
|
143
|
+
else if (thinking === "disabled") cfg.thinkingMode = ThinkingMode.DISABLED;
|
|
139
144
|
return cfg;
|
|
140
145
|
}
|
|
141
146
|
|
|
@@ -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, validateServiceTier } from "./prepare.js";
|
|
8
|
+
import { type AgentExecFlags, parseApprovalAction, prepareAgentExec, validateMode, validateServiceTier, validateThinking } from "./prepare.js";
|
|
9
9
|
|
|
10
10
|
describe("parseApprovalAction", () => {
|
|
11
11
|
it("maps each accepted value (case-insensitive)", () => {
|
|
@@ -47,6 +47,18 @@ describe("validateServiceTier", () => {
|
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
describe("validateThinking", () => {
|
|
51
|
+
it("accepts empty, disabled, and enabled", () => {
|
|
52
|
+
expect(() => validateThinking("")).not.toThrow();
|
|
53
|
+
expect(() => validateThinking("disabled")).not.toThrow();
|
|
54
|
+
expect(() => validateThinking("enabled")).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("rejects anything else before a network round trip", () => {
|
|
58
|
+
expect(() => validateThinking("harder")).toThrow(/must be "disabled" or "enabled"/);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
50
62
|
const BASE_FLAGS: AgentExecFlags = {
|
|
51
63
|
message: "hi",
|
|
52
64
|
attach: [],
|
|
@@ -64,12 +76,34 @@ const BASE_FLAGS: AgentExecFlags = {
|
|
|
64
76
|
autoApprove: false,
|
|
65
77
|
mode: "",
|
|
66
78
|
serviceTier: "",
|
|
79
|
+
thinking: "",
|
|
67
80
|
};
|
|
68
81
|
|
|
69
82
|
// prepareAgentExec only touches client.agentExecution for attachment uploads;
|
|
70
83
|
// with no attachments a bare stub suffices.
|
|
71
84
|
const STUB_CLIENT = { agentExecution: {} } as unknown as Stigmer;
|
|
72
85
|
|
|
86
|
+
/** Stub whose whoAmI resolves an account carrying the given preference. */
|
|
87
|
+
function clientWithPreference(defaultNativeModel: string): Stigmer {
|
|
88
|
+
return {
|
|
89
|
+
agentExecution: {},
|
|
90
|
+
identityAccount: {
|
|
91
|
+
whoAmI: () =>
|
|
92
|
+
Promise.resolve({ spec: { preferences: { defaultNativeModel } } }),
|
|
93
|
+
},
|
|
94
|
+
} as unknown as Stigmer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Stub whose whoAmI rejects (auth failure, backend without the RPC, ...). */
|
|
98
|
+
function clientWithFailingWhoAmI(): Stigmer {
|
|
99
|
+
return {
|
|
100
|
+
agentExecution: {},
|
|
101
|
+
identityAccount: {
|
|
102
|
+
whoAmI: () => Promise.reject(new Error("unimplemented")),
|
|
103
|
+
},
|
|
104
|
+
} as unknown as Stigmer;
|
|
105
|
+
}
|
|
106
|
+
|
|
73
107
|
describe("prepareAgentExec env injection", () => {
|
|
74
108
|
it("injects STIGMER_ORG_ID when absent", async () => {
|
|
75
109
|
const prepared = await prepareAgentExec(BASE_FLAGS, STUB_CLIENT, "acme");
|
|
@@ -103,3 +137,62 @@ describe("prepareAgentExec env injection", () => {
|
|
|
103
137
|
expect(prepared.mode).toBe("plan");
|
|
104
138
|
});
|
|
105
139
|
});
|
|
140
|
+
|
|
141
|
+
describe("prepareAgentExec account-preference model fill (oss#293 Phase 1.5)", () => {
|
|
142
|
+
it("fills an omitted --model from the account preference on cloud", async () => {
|
|
143
|
+
const prepared = await prepareAgentExec(
|
|
144
|
+
BASE_FLAGS,
|
|
145
|
+
clientWithPreference("claude-sonnet-4.6"),
|
|
146
|
+
"acme",
|
|
147
|
+
undefined,
|
|
148
|
+
{ cloudBackend: true },
|
|
149
|
+
);
|
|
150
|
+
expect(prepared.model).toBe("claude-sonnet-4.6");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("an explicit --model always outranks the preference", async () => {
|
|
154
|
+
const prepared = await prepareAgentExec(
|
|
155
|
+
{ ...BASE_FLAGS, model: "gpt-5.3" },
|
|
156
|
+
clientWithPreference("claude-sonnet-4.6"),
|
|
157
|
+
"acme",
|
|
158
|
+
undefined,
|
|
159
|
+
{ cloudBackend: true },
|
|
160
|
+
);
|
|
161
|
+
expect(prepared.model).toBe("gpt-5.3");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("never consults identity on a local backend", async () => {
|
|
165
|
+
// The failing stub doubles as a call detector: local mode must not even
|
|
166
|
+
// attempt whoAmI, so a rejecting client cannot affect the result.
|
|
167
|
+
const prepared = await prepareAgentExec(
|
|
168
|
+
BASE_FLAGS,
|
|
169
|
+
clientWithFailingWhoAmI(),
|
|
170
|
+
"acme",
|
|
171
|
+
undefined,
|
|
172
|
+
{ cloudBackend: false },
|
|
173
|
+
);
|
|
174
|
+
expect(prepared.model).toBe("");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("falls through silently when whoAmI fails — a missing preference never fails a run", async () => {
|
|
178
|
+
const prepared = await prepareAgentExec(
|
|
179
|
+
BASE_FLAGS,
|
|
180
|
+
clientWithFailingWhoAmI(),
|
|
181
|
+
"acme",
|
|
182
|
+
undefined,
|
|
183
|
+
{ cloudBackend: true },
|
|
184
|
+
);
|
|
185
|
+
expect(prepared.model).toBe("");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("stays empty when the account declares no native default", async () => {
|
|
189
|
+
const prepared = await prepareAgentExec(
|
|
190
|
+
BASE_FLAGS,
|
|
191
|
+
clientWithPreference(""),
|
|
192
|
+
"acme",
|
|
193
|
+
undefined,
|
|
194
|
+
{ cloudBackend: true },
|
|
195
|
+
);
|
|
196
|
+
expect(prepared.model).toBe("");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -26,6 +26,12 @@ export type RunMode = "" | "agent" | "plan";
|
|
|
26
26
|
*/
|
|
27
27
|
export type ServiceTierFlag = "" | "standard" | "fast";
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The `--thinking` flag's value space (#772), the tier's twin: empty means
|
|
31
|
+
* unset — distinct from an explicit "disabled" for the same ledger reason.
|
|
32
|
+
*/
|
|
33
|
+
export type ThinkingFlag = "" | "disabled" | "enabled";
|
|
34
|
+
|
|
29
35
|
/** Raw agent-execution flags shared by `run` and `draft` (Go's agentExecFlags). */
|
|
30
36
|
export interface AgentExecFlags {
|
|
31
37
|
readonly message: string;
|
|
@@ -44,6 +50,7 @@ export interface AgentExecFlags {
|
|
|
44
50
|
readonly autoApprove: boolean;
|
|
45
51
|
readonly mode: RunMode;
|
|
46
52
|
readonly serviceTier: ServiceTierFlag;
|
|
53
|
+
readonly thinking: ThinkingFlag;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
/**
|
|
@@ -63,6 +70,19 @@ export interface PreparedRun {
|
|
|
63
70
|
readonly autoApproveAll: boolean;
|
|
64
71
|
readonly mode: RunMode;
|
|
65
72
|
readonly serviceTier: ServiceTierFlag;
|
|
73
|
+
readonly thinking: ThinkingFlag;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Optional behavior switches for {@link prepareAgentExec}. */
|
|
77
|
+
export interface PrepareAgentExecOptions {
|
|
78
|
+
/**
|
|
79
|
+
* When `true` (the caller's backend is Stigmer Cloud) and `--model` is
|
|
80
|
+
* omitted, the model is filled from the caller's account preference
|
|
81
|
+
* (`IdentityAccountPreferences.default_native_model`) via `whoAmI()`.
|
|
82
|
+
* Local mode has no IdentityAccount, so callers pass `false` there and
|
|
83
|
+
* the omitted model keeps resolving to the platform default.
|
|
84
|
+
*/
|
|
85
|
+
readonly cloudBackend?: boolean;
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
/**
|
|
@@ -74,10 +94,22 @@ export async function prepareAgentExec(
|
|
|
74
94
|
client: Stigmer,
|
|
75
95
|
org: string,
|
|
76
96
|
progress?: ProgressSink,
|
|
97
|
+
options?: PrepareAgentExecOptions,
|
|
77
98
|
): Promise<PreparedRun> {
|
|
78
99
|
const defaultAction = parseApprovalAction(flags.approveDefault);
|
|
79
100
|
validateMode(flags.mode);
|
|
80
101
|
validateServiceTier(flags.serviceTier);
|
|
102
|
+
validateThinking(flags.thinking);
|
|
103
|
+
|
|
104
|
+
// Layered model seed (oss#293 Phase 1.5): an explicit --model always wins;
|
|
105
|
+
// an omitted one fills from the account preference on cloud. `run` and
|
|
106
|
+
// `draft` always create a NEW session (threading lives in `resume`, which
|
|
107
|
+
// does not pass through here), so the fill never injects a native model
|
|
108
|
+
// into an existing cursor-harness session.
|
|
109
|
+
const model =
|
|
110
|
+
flags.model === "" && options?.cloudBackend === true
|
|
111
|
+
? await resolveModelFromAccountPreference(client)
|
|
112
|
+
: flags.model;
|
|
81
113
|
|
|
82
114
|
const workspaceEntries = parseWorkspaceEntries(flags.workspace, flags.branch, flags.commit);
|
|
83
115
|
|
|
@@ -107,13 +139,31 @@ export async function prepareAgentExec(
|
|
|
107
139
|
message: flags.message,
|
|
108
140
|
detach: flags.detach,
|
|
109
141
|
verbose: flags.verbose,
|
|
110
|
-
model
|
|
142
|
+
model,
|
|
111
143
|
autoApproveAll: flags.autoApprove,
|
|
112
144
|
mode: flags.mode,
|
|
113
145
|
serviceTier: flags.serviceTier,
|
|
146
|
+
thinking: flags.thinking,
|
|
114
147
|
};
|
|
115
148
|
}
|
|
116
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Best-effort read of the caller's default model for native-harness runs
|
|
152
|
+
* (CLI sessions are native — there is no --harness flag yet, so the cursor
|
|
153
|
+
* default is deliberately not consulted). Any failure — network, auth, a
|
|
154
|
+
* backend without IdentityAccount — resolves to "" and the run proceeds
|
|
155
|
+
* exactly as an unfilled --model does today: a missing preference must
|
|
156
|
+
* never fail a run.
|
|
157
|
+
*/
|
|
158
|
+
async function resolveModelFromAccountPreference(client: Stigmer): Promise<string> {
|
|
159
|
+
try {
|
|
160
|
+
const account = await client.identityAccount.whoAmI();
|
|
161
|
+
return account.spec?.preferences?.defaultNativeModel ?? "";
|
|
162
|
+
} catch {
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
117
167
|
/**
|
|
118
168
|
* Parse the `--approve-default` flag to an ApprovalAction. Empty means "no
|
|
119
169
|
* default" (UNSPECIFIED). Mirrors Go's approval.ParseAction, including the
|
|
@@ -162,3 +212,18 @@ export function validateServiceTier(tier: string): asserts tier is ServiceTierFl
|
|
|
162
212
|
);
|
|
163
213
|
}
|
|
164
214
|
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Validate the `--thinking` flag, the tier check's twin (#772). Empty means
|
|
218
|
+
* "platform default" (which resolves to disabled — never the provider
|
|
219
|
+
* account default). The server refuses enabled on models without the
|
|
220
|
+
* registry thinking capability; this check only catches spelling errors
|
|
221
|
+
* before a network round trip.
|
|
222
|
+
*/
|
|
223
|
+
export function validateThinking(mode: string): asserts mode is ThinkingFlag {
|
|
224
|
+
if (mode !== "" && mode !== "disabled" && mode !== "enabled") {
|
|
225
|
+
throw new UsageError(
|
|
226
|
+
`invalid --thinking value "${mode}": must be "disabled" or "enabled"`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|