glm-acp-agent 1.5.0 → 1.6.1

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.
Files changed (36) hide show
  1. package/README.md +3 -2
  2. package/dist/cli-args.d.ts +8 -0
  3. package/dist/cli-args.d.ts.map +1 -0
  4. package/dist/cli-args.js +34 -0
  5. package/dist/cli-args.js.map +1 -0
  6. package/dist/index.js +5 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/protocol/agent.d.ts +29 -3
  9. package/dist/protocol/agent.d.ts.map +1 -1
  10. package/dist/protocol/agent.js +192 -59
  11. package/dist/protocol/agent.js.map +1 -1
  12. package/dist/protocol/connection.d.ts +2 -1
  13. package/dist/protocol/connection.d.ts.map +1 -1
  14. package/dist/protocol/connection.js +2 -2
  15. package/dist/protocol/connection.js.map +1 -1
  16. package/dist/tests/agent.test.js +228 -1
  17. package/dist/tests/agent.test.js.map +1 -1
  18. package/dist/tests/cli-args.test.d.ts +2 -0
  19. package/dist/tests/cli-args.test.d.ts.map +1 -0
  20. package/dist/tests/cli-args.test.js +56 -0
  21. package/dist/tests/cli-args.test.js.map +1 -0
  22. package/dist/tests/session-mcp-client.test.d.ts +2 -0
  23. package/dist/tests/session-mcp-client.test.d.ts.map +1 -0
  24. package/dist/tests/session-mcp-client.test.js +349 -0
  25. package/dist/tests/session-mcp-client.test.js.map +1 -0
  26. package/dist/tests/vision-mcp-client.test.js +159 -2
  27. package/dist/tests/vision-mcp-client.test.js.map +1 -1
  28. package/dist/tools/session-mcp-client.d.ts +64 -1
  29. package/dist/tools/session-mcp-client.d.ts.map +1 -1
  30. package/dist/tools/session-mcp-client.js +287 -56
  31. package/dist/tools/session-mcp-client.js.map +1 -1
  32. package/dist/tools/vision-mcp-client.d.ts +19 -0
  33. package/dist/tools/vision-mcp-client.d.ts.map +1 -1
  34. package/dist/tools/vision-mcp-client.js +213 -50
  35. package/dist/tools/vision-mcp-client.js.map +1 -1
  36. package/package.json +2 -2
package/README.md CHANGED
@@ -26,13 +26,13 @@ Built-in web tools use Coding Plan-compatible MCP endpoints, not the general `/a
26
26
 
27
27
  - **Full ACP compliance** – implements `initialize`, `authenticate`, `session/new`, `session/set_mode`, `session/prompt`, `session/cancel`, `session/close`, `session/list`, `session/load`, `session/fork`, `session/resume`, and `session/set_model`
28
28
  - **Streaming** – assistant text and reasoning tokens are forwarded as incremental ACP chunks
29
- - **Tool calling** – agentic loop with up to 20 turns of GLM function calling
29
+ - **Tool calling** – agentic loop with a configurable cap of GLM function-calling turns (default 20; see `ACP_GLM_MAX_TURNS` / `--max-turns`)
30
30
  - **Thinking mode** – GLM's `reasoning_content` tokens are surfaced as `agent_thought_chunk` blocks so the client can show the model's chain of thought
31
31
  - **Session permission modes** – supports `default`, `accept_edits`, and `bypass_permissions` via `session/set_mode`. Clients like DevFlow can use this to toggle between prompting for every edit, auto-approving edits while prompting for commands, or bypassing permissions entirely.
32
32
  - **Per-session model switching** – `session/set_model` lets clients change the active GLM model mid-conversation; `session/new` returns the curated `availableModels` list
33
33
  - **Image input via Coding Plan-native vision or Vision MCP** – `promptCapabilities.image` is advertised; the advertised coding models route pasted ACP image blocks through Z.AI Vision MCP (`@z_ai/mcp-server`), while `glm-5v-turbo` sessions — opt-in via `ACP_GLM_AVAILABLE_MODELS`, since it is no longer on the Coding Plan allowlist — send supported image parts directly to the model. Direct chat-image-only models (e.g. `glm-4v-plus`) are intentionally not used.
34
34
  - **Session persistence** – conversations are written to `~/.local/state/glm-acp-agent/sessions/` and can be reloaded via `session/load`, branched via `session/fork`, or resumed without replay via `session/resume`
35
- - **Six built-in tools** (see below)
35
+ - **Seven built-in tools** (see below)
36
36
  - **Self-sufficient local tools** – file reads/writes, directory listings, and shell commands run in the agent process, so they do not depend on ACP client `fs` or `terminal` capabilities
37
37
  - **Configurable permissions** – `write_file` and `run_command` behavior depends on the active session mode (prompts by default)
38
38
  - **Protocol-correct stop reasons** – maps model and runtime conditions to ACP `end_turn`, `max_tokens`, `max_turn_requests`, `refusal`, and `cancelled`
@@ -143,6 +143,7 @@ The agent reads its configuration from environment variables, plus an optional c
143
143
  | `ACP_GLM_AVAILABLE_MODELS` | No | built-in list | Comma-separated list of model ids advertised in `session/set_model` |
144
144
  | `ACP_GLM_BASE_URL` | No | `https://api.z.ai/api/coding/paas/v4` | Override the API base URL |
145
145
  | `ACP_GLM_MAX_TOKENS` | No | `8192` | Cap on `max_tokens` for each completion |
146
+ | `ACP_GLM_MAX_TURNS` | No | `20` | Max model/tool turns per prompt (also settable via `--max-turns`) |
146
147
  | `ACP_GLM_THINKING` | No | auto-detected | Force thinking mode `true` / `false` |
147
148
  | `ACP_GLM_SESSION_DIR` | No | `$XDG_STATE_HOME/glm-acp-agent/sessions` | Where session JSON files are persisted |
148
149
  | `ACP_GLM_DEBUG` | No | — | Set to `true` or `1` to enable verbose debug logging to stderr (shows model selection, API key resolution, tool calls, and usage stats) |
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Parse `--max-turns <n>` (or `--max-turns=<n>`) from CLI args.
3
+ * Returns `undefined` only when the flag is absent; invalid input selects the
4
+ * default (20), never `$ACP_GLM_MAX_TURNS` — explicit-but-bad CLI input must
5
+ * not silently fall through to the env var.
6
+ */
7
+ export declare function parseMaxTurnsFlag(argv: string[]): number | undefined;
8
+ //# sourceMappingURL=cli-args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CA2BpE"}
@@ -0,0 +1,34 @@
1
+ import { DEFAULT_MAX_TURNS } from "./protocol/agent.js";
2
+ /**
3
+ * Parse `--max-turns <n>` (or `--max-turns=<n>`) from CLI args.
4
+ * Returns `undefined` only when the flag is absent; invalid input selects the
5
+ * default (20), never `$ACP_GLM_MAX_TURNS` — explicit-but-bad CLI input must
6
+ * not silently fall through to the env var.
7
+ */
8
+ export function parseMaxTurnsFlag(argv) {
9
+ const idx = argv.indexOf("--max-turns");
10
+ if (idx !== -1) {
11
+ if (idx + 1 >= argv.length) {
12
+ process.stderr.write("glm-acp-agent: --max-turns requires a value\n");
13
+ return DEFAULT_MAX_TURNS;
14
+ }
15
+ const parsed = Number(argv[idx + 1]);
16
+ if (Number.isFinite(parsed) && Math.floor(parsed) >= 1) {
17
+ return Math.floor(parsed);
18
+ }
19
+ process.stderr.write(`glm-acp-agent: ignoring invalid --max-turns "${argv[idx + 1]}"\n`);
20
+ return DEFAULT_MAX_TURNS;
21
+ }
22
+ const eq = argv.find((a) => a.startsWith("--max-turns="));
23
+ if (eq !== undefined) {
24
+ const raw = eq.slice("--max-turns=".length);
25
+ const parsed = Number(raw);
26
+ if (Number.isFinite(parsed) && Math.floor(parsed) >= 1) {
27
+ return Math.floor(parsed);
28
+ }
29
+ process.stderr.write(`glm-acp-agent: ignoring invalid --max-turns "${raw}"\n`);
30
+ return DEFAULT_MAX_TURNS;
31
+ }
32
+ return undefined;
33
+ }
34
+ //# sourceMappingURL=cli-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAc;IAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IACxC,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;QACf,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;YACtE,OAAO,iBAAiB,CAAC;QAC3B,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YACvD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gDAAgD,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CACnE,CAAC;QACF,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC;IAC1D,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YACvD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gDAAgD,GAAG,KAAK,CAAC,CAAC;QAC/E,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
package/dist/index.js CHANGED
@@ -10,8 +10,10 @@
10
10
  * Z_AI_API_KEY - API key for the Z.AI / Zhipu AI service. If unset,
11
11
  * falls back to the credentials file written by --setup.
12
12
  * ACP_GLM_MODEL - (optional) Override the default model (default: glm-5.3)
13
+ * ACP_GLM_MAX_TURNS - (optional) Max model/tool turns per prompt (default: 20)
13
14
  */
14
15
  import { startConnection } from "./protocol/connection.js";
16
+ import { parseMaxTurnsFlag } from "./cli-args.js";
15
17
  import { runSetup } from "./setup.js";
16
18
  const args = process.argv.slice(2);
17
19
  if (args.includes("--setup")) {
@@ -30,11 +32,13 @@ else if (args.includes("--help") || args.includes("-h")) {
30
32
  "Usage:",
31
33
  " glm-acp-agent Start the ACP stdio loop (run by an ACP client)",
32
34
  " glm-acp-agent --setup Interactively store your Z.AI API key on disk",
35
+ " glm-acp-agent --max-turns <n> Max model/tool turns per prompt",
33
36
  " glm-acp-agent --help Show this message",
34
37
  "",
35
38
  "Environment variables:",
36
39
  " Z_AI_API_KEY API key (overrides the stored credentials)",
37
40
  " ACP_GLM_MODEL Default model id (e.g. glm-5.3)",
41
+ " ACP_GLM_MAX_TURNS Max model/tool turns per prompt (default 20)",
38
42
  " ACP_GLM_AVAILABLE_MODELS Comma-separated list of advertised models",
39
43
  " ACP_GLM_BASE_URL Override the Z.AI API base URL",
40
44
  " ACP_GLM_MAX_TOKENS Per-call max output tokens (default 8192)",
@@ -47,7 +51,7 @@ else if (args.includes("--help") || args.includes("-h")) {
47
51
  process.exit(0);
48
52
  }
49
53
  else {
50
- const connection = startConnection();
54
+ const connection = startConnection({ maxTurns: parseMaxTurnsFlag(args) });
51
55
  // Keep the process alive until the connection closes
52
56
  connection.closed
53
57
  .then(() => {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;IAC7B,QAAQ,EAAE;SACP,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;SAC3B,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,OAAO,IAAI,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;QACE,uDAAuD;QACvD,EAAE;QACF,QAAQ;QACR,2EAA2E;QAC3E,yEAAyE;QACzE,6CAA6C;QAC7C,EAAE;QACF,wBAAwB;QACxB,6EAA6E;QAC7E,kEAAkE;QAClE,4EAA4E;QAC5E,iEAAiE;QACjE,4EAA4E;QAC5E,qEAAqE;QACrE,6GAA6G;QAC7G,4EAA4E;QAC5E,4FAA4F;QAC5F,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;KAAM,CAAC;IACN,MAAM,UAAU,GAAG,eAAe,EAAE,CAAC;IAErC,qDAAqD;IACrD,UAAU,CAAC,MAAM;SACd,IAAI,CAAC,GAAG,EAAE;QACT,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,OAAO,IAAI,CAAC,CAAC;QAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACP,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;IAC7B,QAAQ,EAAE;SACP,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;SAC3B,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,OAAO,IAAI,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACP,CAAC;KAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;QACE,uDAAuD;QACvD,EAAE;QACF,QAAQ;QACR,2EAA2E;QAC3E,yEAAyE;QACzE,kEAAkE;QAClE,6CAA6C;QAC7C,EAAE;QACF,wBAAwB;QACxB,6EAA6E;QAC7E,kEAAkE;QAClE,+EAA+E;QAC/E,4EAA4E;QAC5E,iEAAiE;QACjE,4EAA4E;QAC5E,qEAAqE;QACrE,6GAA6G;QAC7G,4EAA4E;QAC5E,4FAA4F;QAC5F,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;KAAM,CAAC;IACN,MAAM,UAAU,GAAG,eAAe,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAE1E,qDAAqD;IACrD,UAAU,CAAC,MAAM;SACd,IAAI,CAAC,GAAG,EAAE;QACT,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,OAAO,IAAI,CAAC,CAAC;QAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -15,7 +15,10 @@ export interface GlmAcpAgentOptions {
15
15
  glm?: {
16
16
  streamChat: (messages: GlmMessage[], signal?: AbortSignal, options?: StreamChatOptions) => AsyncIterable<GlmStreamChunk>;
17
17
  };
18
- /** Maximum number of model/tool turns per single prompt. Default 20. */
18
+ /**
19
+ * Maximum number of model/tool turns per single prompt. Default 20,
20
+ * overridable via `$ACP_GLM_MAX_TURNS`.
21
+ */
19
22
  maxTurns?: number;
20
23
  /**
21
24
  * Override the session store (used in tests). When undefined the agent
@@ -39,6 +42,7 @@ export interface GlmAcpAgentOptions {
39
42
  * GLM series models (via `GlmClient`), providing a full prompt loop with
40
43
  * tool-calling and streaming support.
41
44
  */
45
+ export declare const DEFAULT_MAX_TURNS = 20;
42
46
  export declare class GlmAcpAgent implements Agent {
43
47
  private connection;
44
48
  private sessions;
@@ -56,8 +60,30 @@ export declare class GlmAcpAgent implements Agent {
56
60
  newSession(params: NewSessionRequest): Promise<NewSessionResponse>;
57
61
  unstable_setSessionModel(params: SetSessionModelRequest): Promise<SetSessionModelResponse>;
58
62
  /**
59
- * Build the `thought_level` SessionConfigOption for the given model. The
60
- * available levels depend on the model (see {@link getThoughtLevels}).
63
+ * Switch a session to a new model id — shared by `session/set_model` and the
64
+ * `model` config option. Uncatalogued ids are allowed on purpose (Z.AI may
65
+ * offer models we haven't catalogued), but log a stderr hint. Persists,
66
+ * notifies clients via `session_info_update`, and pushes a
67
+ * `config_option_update` because the valid thought levels may differ between
68
+ * models (e.g. switching from 5.3 to 4.7 drops the effort ladder).
69
+ */
70
+ private applySessionModel;
71
+ /**
72
+ * Build the SessionConfigOptions we advertise: `thought_level` (levels depend
73
+ * on the model, see {@link getThoughtLevels}), `mode`, and `model`.
74
+ *
75
+ * The `mode` option mirrors {@link modesState} as a `category: "mode"`
76
+ * selector. Clients that render config options (Zed) suppress the legacy mode
77
+ * selector once any config option is advertised, so the permission mode would
78
+ * otherwise be unreachable from their UI. `currentMode` is always read from
79
+ * the live session state, so a mode set through `session/set_mode` shows up
80
+ * here too.
81
+ *
82
+ * The `model` option mirrors {@link modelsState} as a `category: "model"`
83
+ * selector for the same reason: clients that render config options suppress
84
+ * the legacy model selector too, so the active model would otherwise be
85
+ * unreachable. `currentModel` is read from the live session state, so a model
86
+ * set through `session/set_model` shows up here as well.
61
87
  */
62
88
  private configOptionsState;
63
89
  setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>;
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/protocol/agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/protocol/agent.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,35 @@ import { debug, error, isDebugEnabled } from "../llm/logger.js";
20
20
  * files in their AGENTS.md.
21
21
  */
22
22
  const PROJECT_CONTEXT_CAP_CHARS = 8 * 1024;
23
+ /**
24
+ * The session modes we advertise, in the order clients should display them.
25
+ *
26
+ * Single source of truth for both the ACP `modes` state (`session/set_mode`)
27
+ * and the `mode`-category SessionConfigOption. Clients like Zed suppress the
28
+ * legacy mode selector as soon as an agent advertises any config option, so
29
+ * the same list has to reach the UI through both channels.
30
+ */
31
+ const SESSION_MODES = [
32
+ {
33
+ id: "default",
34
+ name: "Ask for permission",
35
+ description: "Prompt before edits and commands.",
36
+ },
37
+ {
38
+ id: "accept_edits",
39
+ name: "Auto-approve edits",
40
+ description: "Edits run without prompting. Commands still prompt.",
41
+ },
42
+ {
43
+ id: "bypass_permissions",
44
+ name: "Bypass all permissions",
45
+ description: "Edits and commands run without prompting.",
46
+ },
47
+ ];
48
+ const SESSION_MODE_IDS = SESSION_MODES.map((mode) => mode.id);
49
+ function isSessionModeId(value) {
50
+ return typeof value === "string" && SESSION_MODE_IDS.includes(value);
51
+ }
23
52
  /**
24
53
  * Display names for thought levels shown in client UIs. Kept as an exhaustive
25
54
  * map (not capitalize-first-letter) so `xhigh` renders as "X-High" and adding
@@ -42,6 +71,25 @@ const THOUGHT_LEVEL_NAMES = {
42
71
  * GLM series models (via `GlmClient`), providing a full prompt loop with
43
72
  * tool-calling and streaming support.
44
73
  */
74
+ export const DEFAULT_MAX_TURNS = 20;
75
+ /**
76
+ * Resolve the fallback maxTurns from `$ACP_GLM_MAX_TURNS` when the caller did
77
+ * not pass an explicit value. Returns `undefined` when unset or invalid (the
78
+ * constructor then applies the default, and logs a warning on invalid input).
79
+ * Values that floor below 1 (e.g. 0.5) count as invalid — flooring them to 0
80
+ * would just be silently replaced by the default in the constructor.
81
+ */
82
+ function envMaxTurns() {
83
+ const raw = process.env["ACP_GLM_MAX_TURNS"];
84
+ if (raw === undefined || raw === "")
85
+ return undefined;
86
+ const parsed = Number(raw);
87
+ if (!Number.isFinite(parsed) || Math.floor(parsed) < 1) {
88
+ process.stderr.write(`glm-acp-agent: ignoring invalid ACP_GLM_MAX_TURNS="${raw}"\n`);
89
+ return undefined;
90
+ }
91
+ return Math.floor(parsed);
92
+ }
45
93
  export class GlmAcpAgent {
46
94
  connection;
47
95
  sessions = new Map();
@@ -54,11 +102,10 @@ export class GlmAcpAgent {
54
102
  constructor(connection, options = {}) {
55
103
  this.connection = connection;
56
104
  this._glm = options.glm ?? null;
57
- const candidateMaxTurns = options.maxTurns ?? 20;
105
+ const candidateMaxTurns = options.maxTurns ?? envMaxTurns() ?? DEFAULT_MAX_TURNS;
106
+ const floored = Math.floor(candidateMaxTurns);
58
107
  this.maxTurns =
59
- Number.isFinite(candidateMaxTurns) && candidateMaxTurns > 0
60
- ? Math.floor(candidateMaxTurns)
61
- : 20;
108
+ Number.isFinite(floored) && floored >= 1 ? floored : DEFAULT_MAX_TURNS;
62
109
  this.sessionStore =
63
110
  options.sessionStore === null
64
111
  ? null
@@ -193,7 +240,7 @@ export class GlmAcpAgent {
193
240
  sessionId,
194
241
  models: this.modelsState(model),
195
242
  modes: this.modesState("default"),
196
- configOptions: this.configOptionsState(model, thoughtLevel),
243
+ configOptions: this.configOptionsState(model, thoughtLevel, "default"),
197
244
  };
198
245
  }
199
246
  async unstable_setSessionModel(params) {
@@ -201,49 +248,68 @@ export class GlmAcpAgent {
201
248
  if (!session) {
202
249
  throw new Error(`Session not found: ${params.sessionId}`);
203
250
  }
251
+ await this.applySessionModel(params.sessionId, session, params.modelId);
252
+ return {};
253
+ }
254
+ /**
255
+ * Switch a session to a new model id — shared by `session/set_model` and the
256
+ * `model` config option. Uncatalogued ids are allowed on purpose (Z.AI may
257
+ * offer models we haven't catalogued), but log a stderr hint. Persists,
258
+ * notifies clients via `session_info_update`, and pushes a
259
+ * `config_option_update` because the valid thought levels may differ between
260
+ * models (e.g. switching from 5.3 to 4.7 drops the effort ladder).
261
+ */
262
+ async applySessionModel(sessionId, session, modelId) {
204
263
  const available = getAvailableModels();
205
- const known = available.find((m) => m.modelId === params.modelId);
264
+ const known = available.find((m) => m.modelId === modelId);
206
265
  if (!known) {
207
- // Allow the caller to pick any model id even if not in the curated list
208
- // (Z.AI may offer models we haven't catalogued), but log a stderr hint.
209
- process.stderr.write(`[glm-acp-agent] warning: model "${params.modelId}" is not in the advertised list; using as-is.\n`);
266
+ process.stderr.write(`[glm-acp-agent] warning: model "${modelId}" is not in the advertised list; using as-is.\n`);
210
267
  }
211
- session.model = params.modelId;
212
- // The valid thought levels differ per model (e.g. 5.3 offers the full
213
- // effort ladder while others only offer none/on), so clamp the current
214
- // level to what the new model supports.
215
- session.thoughtLevel = resolveThoughtLevel(params.modelId, session.thoughtLevel);
268
+ session.model = modelId;
269
+ // The valid thought levels differ per model, so clamp the current level to
270
+ // what the new model supports.
271
+ session.thoughtLevel = resolveThoughtLevel(modelId, session.thoughtLevel);
216
272
  session.updatedAt = new Date().toISOString();
217
273
  // Persist immediately so a fork/reload before the next prompt doesn't
218
- // resurrect the previous model or thought level (matches setSessionConfigOption).
219
- this.persistSession(params.sessionId, session);
274
+ // resurrect the previous model or thought level.
275
+ this.persistSession(sessionId, session);
220
276
  // Notify clients so any UI that displays the active model refreshes
221
277
  // immediately, instead of waiting for the next prompt to complete.
222
278
  await safeSessionUpdate(this.connection, {
223
- sessionId: params.sessionId,
279
+ sessionId,
224
280
  update: {
225
281
  sessionUpdate: "session_info_update",
226
282
  updatedAt: session.updatedAt,
227
283
  },
228
284
  });
229
- // Push updated thought_level options — the set of valid levels may
230
- // differ between models (e.g. switching from 5.3 to 4.7 drops the
231
- // effort ladder).
232
285
  await safeSessionUpdate(this.connection, {
233
- sessionId: params.sessionId,
286
+ sessionId,
234
287
  update: {
235
288
  sessionUpdate: "config_option_update",
236
- configOptions: this.configOptionsState(session.model, session.thoughtLevel),
289
+ configOptions: this.configOptionsState(session.model, session.thoughtLevel, session.mode),
237
290
  },
238
291
  });
239
- return {};
240
292
  }
241
293
  /**
242
- * Build the `thought_level` SessionConfigOption for the given model. The
243
- * available levels depend on the model (see {@link getThoughtLevels}).
294
+ * Build the SessionConfigOptions we advertise: `thought_level` (levels depend
295
+ * on the model, see {@link getThoughtLevels}), `mode`, and `model`.
296
+ *
297
+ * The `mode` option mirrors {@link modesState} as a `category: "mode"`
298
+ * selector. Clients that render config options (Zed) suppress the legacy mode
299
+ * selector once any config option is advertised, so the permission mode would
300
+ * otherwise be unreachable from their UI. `currentMode` is always read from
301
+ * the live session state, so a mode set through `session/set_mode` shows up
302
+ * here too.
303
+ *
304
+ * The `model` option mirrors {@link modelsState} as a `category: "model"`
305
+ * selector for the same reason: clients that render config options suppress
306
+ * the legacy model selector too, so the active model would otherwise be
307
+ * unreachable. `currentModel` is read from the live session state, so a model
308
+ * set through `session/set_model` shows up here as well.
244
309
  */
245
- configOptionsState(model, thoughtLevel) {
310
+ configOptionsState(model, thoughtLevel, currentMode) {
246
311
  const levels = getThoughtLevels(model);
312
+ const modelOptions = availableModelsWith(model);
247
313
  return [
248
314
  {
249
315
  id: "thought_level",
@@ -257,6 +323,30 @@ export class GlmAcpAgent {
257
323
  name: THOUGHT_LEVEL_NAMES[level],
258
324
  })),
259
325
  },
326
+ {
327
+ id: "mode",
328
+ name: "Mode",
329
+ description: "Tool permission mode",
330
+ category: "mode",
331
+ type: "select",
332
+ currentValue: currentMode,
333
+ options: SESSION_MODES.map((mode) => ({
334
+ value: mode.id,
335
+ name: mode.name,
336
+ })),
337
+ },
338
+ {
339
+ id: "model",
340
+ name: "Model",
341
+ description: "GLM model for this session",
342
+ category: "model",
343
+ type: "select",
344
+ currentValue: model,
345
+ options: modelOptions.map((m) => ({
346
+ value: m.modelId,
347
+ name: m.name,
348
+ })),
349
+ },
260
350
  ];
261
351
  }
262
352
  async setSessionConfigOption(params) {
@@ -264,6 +354,39 @@ export class GlmAcpAgent {
264
354
  if (!session) {
265
355
  throw new Error(`Session not found: ${params.sessionId}`);
266
356
  }
357
+ if (params.configId === "mode") {
358
+ // Same reject-don't-coerce policy as thought_level below.
359
+ if (!isSessionModeId(params.value)) {
360
+ throw new Error(`Invalid mode value: ${String(params.value)}`);
361
+ }
362
+ session.mode = params.value;
363
+ session.updatedAt = new Date().toISOString();
364
+ this.persistSession(params.sessionId, session);
365
+ // Mirror setSessionMode so clients tracking the ACP mode state (rather
366
+ // than the config option) stay in sync with the dropdown.
367
+ await safeSessionUpdate(this.connection, {
368
+ sessionId: params.sessionId,
369
+ update: {
370
+ sessionUpdate: "current_mode_update",
371
+ currentModeId: session.mode,
372
+ },
373
+ });
374
+ return {
375
+ configOptions: this.configOptionsState(session.model, session.thoughtLevel, session.mode),
376
+ };
377
+ }
378
+ if (params.configId === "model") {
379
+ // Same reject-don't-coerce policy as thought_level below, but only for
380
+ // values that aren't model ids at all. Uncatalogued ids are allowed on
381
+ // purpose, mirroring unstable_setSessionModel / ACP_GLM_MODEL.
382
+ if (typeof params.value !== "string" || params.value.length === 0) {
383
+ throw new Error(`Invalid model value: ${String(params.value)}`);
384
+ }
385
+ await this.applySessionModel(params.sessionId, session, params.value);
386
+ return {
387
+ configOptions: this.configOptionsState(session.model, session.thoughtLevel, session.mode),
388
+ };
389
+ }
267
390
  if (params.configId !== "thought_level") {
268
391
  throw new Error(`Unknown config option: ${params.configId}`);
269
392
  }
@@ -280,7 +403,7 @@ export class GlmAcpAgent {
280
403
  session.updatedAt = new Date().toISOString();
281
404
  this.persistSession(params.sessionId, session);
282
405
  return {
283
- configOptions: this.configOptionsState(session.model, session.thoughtLevel),
406
+ configOptions: this.configOptionsState(session.model, session.thoughtLevel, session.mode),
284
407
  };
285
408
  }
286
409
  /**
@@ -294,35 +417,12 @@ export class GlmAcpAgent {
294
417
  * represent the selection the agent is actually using.
295
418
  */
296
419
  modelsState(currentModelId) {
297
- const available = getAvailableModels();
298
- if (available.some((m) => m.modelId === currentModelId)) {
299
- return { availableModels: available, currentModelId };
300
- }
301
- return {
302
- availableModels: [...available, { modelId: currentModelId, name: currentModelId }],
303
- currentModelId,
304
- };
420
+ return { availableModels: availableModelsWith(currentModelId), currentModelId };
305
421
  }
306
422
  /** Build the SessionModeState we advertise on session create/load/resume/fork. */
307
423
  modesState(currentModeId) {
308
424
  return {
309
- availableModes: [
310
- {
311
- id: "default",
312
- name: "Ask for permission",
313
- description: "Prompt before edits and commands.",
314
- },
315
- {
316
- id: "accept_edits",
317
- name: "Auto-approve edits",
318
- description: "Edits run without prompting. Commands still prompt.",
319
- },
320
- {
321
- id: "bypass_permissions",
322
- name: "Bypass all permissions",
323
- description: "Edits and commands run without prompting.",
324
- },
325
- ],
425
+ availableModes: SESSION_MODES.map((mode) => ({ ...mode })),
326
426
  currentModeId,
327
427
  };
328
428
  }
@@ -331,9 +431,8 @@ export class GlmAcpAgent {
331
431
  if (!session) {
332
432
  throw new Error(`Session not found: ${params.sessionId}`);
333
433
  }
334
- const validModeIds = ["default", "accept_edits", "bypass_permissions"];
335
- if (!validModeIds.includes(params.modeId)) {
336
- throw new Error(`Invalid modeId: ${params.modeId}. Valid modes are: ${validModeIds.join(", ")}`);
434
+ if (!isSessionModeId(params.modeId)) {
435
+ throw new Error(`Invalid modeId: ${params.modeId}. Valid modes are: ${SESSION_MODE_IDS.join(", ")}`);
337
436
  }
338
437
  const newMode = params.modeId;
339
438
  session.mode = newMode;
@@ -346,6 +445,16 @@ export class GlmAcpAgent {
346
445
  currentModeId: newMode,
347
446
  },
348
447
  });
448
+ // Config options are a separate state channel: re-publish them so a mode
449
+ // dropdown (category "mode") doesn't keep a stale currentValue when the
450
+ // mode changes through the classic ACP path.
451
+ await safeSessionUpdate(this.connection, {
452
+ sessionId: params.sessionId,
453
+ update: {
454
+ sessionUpdate: "config_option_update",
455
+ configOptions: this.configOptionsState(session.model, session.thoughtLevel, session.mode),
456
+ },
457
+ });
349
458
  return {};
350
459
  }
351
460
  // ---------------------------------------------------------------------------
@@ -559,7 +668,7 @@ export class GlmAcpAgent {
559
668
  return {
560
669
  models: this.modelsState(persisted.model),
561
670
  modes: this.modesState(persisted.mode),
562
- configOptions: this.configOptionsState(restored.model, restored.thoughtLevel),
671
+ configOptions: this.configOptionsState(restored.model, restored.thoughtLevel, restored.mode),
563
672
  };
564
673
  }
565
674
  async unstable_forkSession(params) {
@@ -591,7 +700,7 @@ export class GlmAcpAgent {
591
700
  sessionId: newSessionId,
592
701
  models: this.modelsState(forked.model),
593
702
  modes: this.modesState(forked.mode),
594
- configOptions: this.configOptionsState(forked.model, forked.thoughtLevel),
703
+ configOptions: this.configOptionsState(forked.model, forked.thoughtLevel, forked.mode),
595
704
  };
596
705
  }
597
706
  async resumeSession(params) {
@@ -618,7 +727,7 @@ export class GlmAcpAgent {
618
727
  return {
619
728
  models: this.modelsState(persisted.model),
620
729
  modes: this.modesState(persisted.mode),
621
- configOptions: this.configOptionsState(restored.model, restored.thoughtLevel),
730
+ configOptions: this.configOptionsState(restored.model, restored.thoughtLevel, restored.mode),
622
731
  };
623
732
  }
624
733
  // ---------------------------------------------------------------------------
@@ -825,7 +934,18 @@ export class GlmAcpAgent {
825
934
  // Loop and continue – GLM expects a follow-up completion now that it has
826
935
  // tool results.
827
936
  }
828
- // Reached MAX_TURNS without resolution.
937
+ // Reached MAX_TURNS without resolution. Tell the user why we stopped —
938
+ // without this, hitting the cap is indistinguishable from a normal end.
939
+ await this.connection.sessionUpdate({
940
+ sessionId,
941
+ update: {
942
+ sessionUpdate: "agent_message_chunk",
943
+ content: {
944
+ type: "text",
945
+ text: `\n[stopped: reached the ${this.maxTurns}-turn limit — send a message to continue]`,
946
+ },
947
+ },
948
+ });
829
949
  return { stopReason: "max_turn_requests", usage: lastUsage };
830
950
  }
831
951
  mapStopReason(stopReason) {
@@ -1009,6 +1129,19 @@ function stringifyUserMessage(content) {
1009
1129
  .filter((text) => text.length > 0)
1010
1130
  .join("\n");
1011
1131
  }
1132
+ /**
1133
+ * The advertised model list, always including `currentModelId` — a session
1134
+ * restored from disk can be pinned to a de-listed id (`glm-5.2` was the
1135
+ * previous default), and `ACP_GLM_MODEL` / `session/set_model` / the `model`
1136
+ * config option all accept uncatalogued ids on purpose. Dropping the active id
1137
+ * from the list would leave pickers unable to represent the selection in use.
1138
+ */
1139
+ function availableModelsWith(currentModelId) {
1140
+ const available = getAvailableModels();
1141
+ return available.some((m) => m.modelId === currentModelId)
1142
+ ? available
1143
+ : [...available, { modelId: currentModelId, name: currentModelId }];
1144
+ }
1012
1145
  /**
1013
1146
  * Read an `AGENTS.md` (preferred) or `CLAUDE.md` from the session's cwd, returning
1014
1147
  * its contents capped to {@link PROJECT_CONTEXT_CAP_CHARS} characters. Read errors