glm-acp-agent 1.1.3 → 1.2.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.
Files changed (37) hide show
  1. package/README.md +118 -51
  2. package/dist/index.js +2 -2
  3. package/dist/llm/glm-client.d.ts +59 -1
  4. package/dist/llm/glm-client.d.ts.map +1 -1
  5. package/dist/llm/glm-client.js +135 -15
  6. package/dist/llm/glm-client.js.map +1 -1
  7. package/dist/protocol/agent.d.ts +16 -3
  8. package/dist/protocol/agent.d.ts.map +1 -1
  9. package/dist/protocol/agent.js +422 -49
  10. package/dist/protocol/agent.js.map +1 -1
  11. package/dist/protocol/image-preprocessor.js +1 -1
  12. package/dist/protocol/image-preprocessor.js.map +1 -1
  13. package/dist/protocol/session-store.d.ts +14 -2
  14. package/dist/protocol/session-store.d.ts.map +1 -1
  15. package/dist/protocol/session-store.js +27 -6
  16. package/dist/protocol/session-store.js.map +1 -1
  17. package/dist/protocol/system-prompt.d.ts.map +1 -1
  18. package/dist/protocol/system-prompt.js +1 -3
  19. package/dist/protocol/system-prompt.js.map +1 -1
  20. package/dist/tests/agent.test.js +395 -4
  21. package/dist/tests/agent.test.js.map +1 -1
  22. package/dist/tests/executor.test.js +69 -2
  23. package/dist/tests/executor.test.js.map +1 -1
  24. package/dist/tests/glm-client.test.js +152 -2
  25. package/dist/tests/glm-client.test.js.map +1 -1
  26. package/dist/tests/integration.test.js +50 -0
  27. package/dist/tests/integration.test.js.map +1 -1
  28. package/dist/tools/executor.d.ts +10 -1
  29. package/dist/tools/executor.d.ts.map +1 -1
  30. package/dist/tools/executor.js +77 -53
  31. package/dist/tools/executor.js.map +1 -1
  32. package/dist/tools/session-mcp-client.js +3 -1
  33. package/dist/tools/session-mcp-client.js.map +1 -1
  34. package/dist/tools/vision-mcp-client.d.ts.map +1 -1
  35. package/dist/tools/vision-mcp-client.js +6 -2
  36. package/dist/tools/vision-mcp-client.js.map +1 -1
  37. package/package.json +8 -4
@@ -620,7 +620,8 @@ test("system prompt includes image_handling fallback instructions", async () =>
620
620
  const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
621
621
  await agent.prompt({ sessionId, prompt: [{ type: "text", text: "hi" }] });
622
622
  assert.ok(ref.value.includes("<image_handling>"), "system prompt must contain image_handling section");
623
- assert.ok(ref.value.includes("image_analysis_error") || ref.value.includes("image_analysis"), "image_handling must mention the known annotation tags");
623
+ assert.ok(ref.value.toLowerCase().includes("native multimodal image content"), "image_handling must cover native multimodal image delivery");
624
+ assert.ok(ref.value.includes("image_analysis_error") && ref.value.includes("image_unsupported_format"), "image_handling must mention image annotation fallback tags");
624
625
  assert.ok(ref.value.toLowerCase().includes("client"), "image_handling must attribute missing image to client-side problem");
625
626
  });
626
627
  test("image_analysis tool is still listed when ACP_GLM_PROMPT_IMAGES=false", async () => {
@@ -675,13 +676,215 @@ test("authenticate is a no-op", async () => {
675
676
  const result = await agent.authenticate({ methodId: "z_ai_api_key" });
676
677
  assert.deepEqual(result, {});
677
678
  });
678
- test("setSessionMode is a no-op", async () => {
679
+ test("setSessionMode persists the mode and emits current_mode_update", async () => {
680
+ const { store, cleanup } = makeTempStore();
681
+ try {
682
+ const conn = createConnectionStub();
683
+ const agent = new GlmAcpAgent(conn, { sessionStore: store });
684
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
685
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
686
+ // Close the session to persist it
687
+ await agent.closeSession({ sessionId });
688
+ // Default mode is "default" after persistence
689
+ const initial = store.load(sessionId);
690
+ assert.equal(initial?.mode, "default");
691
+ // Load the session back
692
+ const loadResult = await agent.loadSession({ sessionId, cwd: "/tmp", mcpServers: [] });
693
+ assert.equal(loadResult.modes?.currentModeId, "default");
694
+ // Change to accept_edits
695
+ const before = conn.updates.length;
696
+ const result = await agent.setSessionMode({ sessionId, modeId: "accept_edits" });
697
+ assert.deepEqual(result, {});
698
+ // Check that the mode was persisted
699
+ const after = store.load(sessionId);
700
+ assert.equal(after?.mode, "accept_edits");
701
+ // Check that current_mode_update was emitted
702
+ const modeUpdates = conn.updates.slice(before).filter((u) => u.update.sessionUpdate === "current_mode_update");
703
+ assert.equal(modeUpdates.length, 1);
704
+ assert.equal(modeUpdates[0].update.currentModeId, "accept_edits");
705
+ }
706
+ finally {
707
+ cleanup();
708
+ }
709
+ });
710
+ test("setSessionMode rejects invalid mode ids", async () => {
679
711
  const conn = createConnectionStub();
680
712
  const agent = new GlmAcpAgent(conn, { sessionStore: null });
681
713
  await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
682
714
  const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
683
- const result = await agent.setSessionMode({ sessionId, modeId: "ask" });
684
- assert.deepEqual(result, {});
715
+ await assert.rejects(agent.setSessionMode({ sessionId, modeId: "invalid_mode" }), /Invalid modeId: invalid_mode/);
716
+ });
717
+ // ---------------------------------------------------------------------------
718
+ // Thought level / config options
719
+ // ---------------------------------------------------------------------------
720
+ test("newSession returns configOptions with thought_level selector for default model", async () => {
721
+ const conn = createConnectionStub();
722
+ const agent = new GlmAcpAgent(conn, { sessionStore: null });
723
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
724
+ const result = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
725
+ assert.ok(result.configOptions, "configOptions should be present");
726
+ const tl = result.configOptions.find((o) => o.category === "thought_level");
727
+ assert.ok(tl, "thought_level option should exist");
728
+ assert.equal(tl.type, "select");
729
+ // Default model is glm-5.2 → none/high/max, defaulting to max.
730
+ assert.equal(tl.currentValue, "max");
731
+ const values = tl.options.map((o) => o.value);
732
+ assert.deepEqual(values, ["none", "high", "max"]);
733
+ });
734
+ test("setSessionConfigOption updates thoughtLevel and returns updated options", async () => {
735
+ const conn = createConnectionStub();
736
+ const agent = new GlmAcpAgent(conn, { sessionStore: null });
737
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
738
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
739
+ const result = await agent.setSessionConfigOption({
740
+ sessionId,
741
+ configId: "thought_level",
742
+ value: "high",
743
+ });
744
+ assert.ok(result.configOptions);
745
+ const tl = result.configOptions.find((o) => o.id === "thought_level");
746
+ assert.equal(tl.currentValue, "high");
747
+ });
748
+ test("setSessionConfigOption auto-resolves values invalid for the current model", async () => {
749
+ const conn = createConnectionStub();
750
+ const agent = new GlmAcpAgent(conn, { sessionStore: null });
751
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
752
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
753
+ // Switch to glm-5.1 which only supports none/on.
754
+ await agent.unstable_setSessionModel({ sessionId, modelId: "glm-5.1" });
755
+ // "max" is invalid for glm-5.1 — should auto-resolve to "on".
756
+ const result = await agent.setSessionConfigOption({
757
+ sessionId,
758
+ configId: "thought_level",
759
+ value: "max",
760
+ });
761
+ const tl = result.configOptions.find((o) => o.id === "thought_level");
762
+ assert.equal(tl.currentValue, "on");
763
+ });
764
+ test("setSessionConfigOption rejects unknown config ids", async () => {
765
+ const conn = createConnectionStub();
766
+ const agent = new GlmAcpAgent(conn, { sessionStore: null });
767
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
768
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
769
+ await assert.rejects(agent.setSessionConfigOption({ sessionId, configId: "unknown", value: "x" }), /Unknown config option/);
770
+ });
771
+ test("setSessionConfigOption rejects values that aren't a known thought level", async () => {
772
+ const conn = createConnectionStub();
773
+ const agent = new GlmAcpAgent(conn, { sessionStore: null });
774
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
775
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
776
+ // "medium" is not a ThoughtLevel at all — reject rather than silently coerce.
777
+ await assert.rejects(agent.setSessionConfigOption({ sessionId, configId: "thought_level", value: "medium" }), /Invalid thought_level value/);
778
+ // A cross-model-but-known value ("max" while on the default glm-5.2) is fine.
779
+ const ok = await agent.setSessionConfigOption({
780
+ sessionId,
781
+ configId: "thought_level",
782
+ value: "max",
783
+ });
784
+ assert.equal(ok.configOptions.find((o) => o.id === "thought_level").currentValue, "max");
785
+ });
786
+ test("switching model updates thought_level options via config_option_update", async () => {
787
+ const conn = createConnectionStub();
788
+ const agent = new GlmAcpAgent(conn, { sessionStore: null });
789
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
790
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
791
+ // Start with glm-5.2 (default) → max, options none/high/max.
792
+ // Switch to glm-4.7 → thoughtLevel should resolve to "on".
793
+ await agent.unstable_setSessionModel({ sessionId, modelId: "glm-4.7" });
794
+ const configUpdates = conn.updates.filter((u) => u.update.sessionUpdate === "config_option_update");
795
+ assert.equal(configUpdates.length, 1);
796
+ const opts = configUpdates[0].update.configOptions;
797
+ const tl = opts.find((o) => o.id === "thought_level");
798
+ assert.equal(tl.currentValue, "on");
799
+ const values = tl.options.map((o) => o.value);
800
+ assert.deepEqual(values, ["none", "on"]);
801
+ });
802
+ test("thoughtLevel is forwarded to streamChat as reasoningEffort", async () => {
803
+ const conn = createConnectionStub();
804
+ let seenEffort;
805
+ const glm = {
806
+ async *streamChat(_messages, _signal, options) {
807
+ seenEffort = options?.reasoningEffort;
808
+ yield { text: "ok" };
809
+ yield { done: true, stopReason: "stop" };
810
+ },
811
+ };
812
+ const agent = new GlmAcpAgent(conn, { glm, sessionStore: null });
813
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
814
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
815
+ await agent.setSessionConfigOption({ sessionId, configId: "thought_level", value: "high" });
816
+ await agent.prompt({ sessionId, prompt: [{ type: "text", text: "hi" }] });
817
+ assert.equal(seenEffort, "high");
818
+ });
819
+ test("thoughtLevel round-trips through fork via persistence", async () => {
820
+ const dir = mkdtempSync(pathJoin(osTmpdir(), "glm-acp-thoughtlevel-"));
821
+ try {
822
+ const store = new SessionStore(dir);
823
+ const conn = createConnectionStub();
824
+ const agent = new GlmAcpAgent(conn, { sessionStore: store });
825
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
826
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
827
+ await agent.setSessionConfigOption({ sessionId, configId: "thought_level", value: "high" });
828
+ const forked = await agent.unstable_forkSession({ sessionId, cwd: "/tmp", mcpServers: [] });
829
+ const tl = forked.configOptions.find((o) => o.id === "thought_level");
830
+ assert.equal(tl.currentValue, "high");
831
+ }
832
+ finally {
833
+ rmSync(dir, { recursive: true, force: true });
834
+ }
835
+ });
836
+ test("model switch persists model + clamped thoughtLevel before any prompt (fork sees it)", async () => {
837
+ const dir = mkdtempSync(pathJoin(osTmpdir(), "glm-acp-switch-"));
838
+ try {
839
+ const store = new SessionStore(dir);
840
+ const conn = createConnectionStub();
841
+ const agent = new GlmAcpAgent(conn, { sessionStore: store });
842
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
843
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
844
+ // Switch model with no prompt in between: glm-5.2/max → glm-4.7, which
845
+ // clamps the level to "on". Both must survive a fork that reads from disk.
846
+ await agent.unstable_setSessionModel({ sessionId, modelId: "glm-4.7" });
847
+ const forked = await agent.unstable_forkSession({ sessionId, cwd: "/tmp", mcpServers: [] });
848
+ assert.equal(forked.models.currentModelId, "glm-4.7");
849
+ const tl = forked.configOptions.find((o) => o.id === "thought_level");
850
+ assert.equal(tl.currentValue, "on");
851
+ }
852
+ finally {
853
+ rmSync(dir, { recursive: true, force: true });
854
+ }
855
+ });
856
+ test("v2 sessions (no thoughtLevel) migrate and resolve to the model default on load", async () => {
857
+ const dir = mkdtempSync(pathJoin(osTmpdir(), "glm-acp-migrate-"));
858
+ const sessionId = "abcd1234-abcd-abcd-abcd-abcdabcd1234";
859
+ try {
860
+ // Write a raw v2 record: has `mode` but no `thoughtLevel`.
861
+ const v2 = {
862
+ schemaVersion: 2,
863
+ sessionId,
864
+ cwd: "/tmp",
865
+ messages: [{ role: "system", content: "you are a coding assistant" }],
866
+ title: "old session",
867
+ updatedAt: "2026-01-01T00:00:00.000Z",
868
+ model: "glm-5.1",
869
+ mode: "default",
870
+ };
871
+ writeFileSync(pathJoin(dir, `${sessionId}.json`), JSON.stringify(v2), "utf8");
872
+ // The store migrates the raw record, defaulting thoughtLevel to "max".
873
+ const store = new SessionStore(dir);
874
+ const migrated = store.load(sessionId);
875
+ assert.equal(migrated?.schemaVersion, 3);
876
+ assert.equal(migrated?.thoughtLevel, "max");
877
+ // On load the agent clamps that to the session's model (glm-5.1 → "on").
878
+ const conn = createConnectionStub();
879
+ const agent = new GlmAcpAgent(conn, { sessionStore: store });
880
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
881
+ const result = await agent.loadSession({ sessionId, cwd: "/tmp", mcpServers: [] });
882
+ const tl = result.configOptions.find((o) => o.id === "thought_level");
883
+ assert.equal(tl.currentValue, "on");
884
+ }
885
+ finally {
886
+ rmSync(dir, { recursive: true, force: true });
887
+ }
685
888
  });
686
889
  // ---------------------------------------------------------------------------
687
890
  // Prompt loop
@@ -1028,6 +1231,100 @@ test("prompt with image block runs Vision MCP preprocessing and feeds text into
1028
1231
  assert.match(capturedUser, /What is in this image\?/);
1029
1232
  assert.match(capturedUser, /<image_analysis index="1">[\s\S]*It is a kitten\.[\s\S]*<\/image_analysis>/);
1030
1233
  });
1234
+ test("prompt with image block on vision-native model sends native image_url content", async () => {
1235
+ const conn = createConnectionStub();
1236
+ let capturedUser;
1237
+ const glm = {
1238
+ async *streamChat(messages, _signal, options) {
1239
+ assert.equal(options?.model, "glm-5v-turbo");
1240
+ const userMsg = [...messages].reverse().find((m) => m.role === "user");
1241
+ capturedUser = userMsg?.content;
1242
+ yield { text: "ok" };
1243
+ yield { done: true, stopReason: "stop" };
1244
+ },
1245
+ };
1246
+ const visionCalls = [];
1247
+ const visionClient = {
1248
+ async callTool(_name, args) {
1249
+ visionCalls.push(args);
1250
+ return { content: [{ type: "text", text: "should not be used" }] };
1251
+ },
1252
+ async dispose() { },
1253
+ };
1254
+ const agent = new GlmAcpAgent(conn, { glm, visionClient, sessionStore: null });
1255
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
1256
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
1257
+ await agent.unstable_setSessionModel({ sessionId, modelId: "glm-5v-turbo" });
1258
+ await agent.prompt({
1259
+ sessionId,
1260
+ prompt: [
1261
+ { type: "text", text: "What is in this image?" },
1262
+ { type: "image", data: "", mimeType: "image/png", uri: "https://example.com/cat.png" },
1263
+ ],
1264
+ });
1265
+ assert.equal(visionCalls.length, 0);
1266
+ assert.deepEqual(capturedUser, [
1267
+ { type: "text", text: "What is in this image?" },
1268
+ { type: "image_url", image_url: { url: "https://example.com/cat.png" } },
1269
+ ]);
1270
+ });
1271
+ test("prompt with inline image data on vision-native model sends a data URI", async () => {
1272
+ const conn = createConnectionStub();
1273
+ let capturedUser;
1274
+ const glm = {
1275
+ async *streamChat(messages) {
1276
+ const userMsg = [...messages].reverse().find((m) => m.role === "user");
1277
+ capturedUser = userMsg?.content;
1278
+ yield { text: "ok" };
1279
+ yield { done: true, stopReason: "stop" };
1280
+ },
1281
+ };
1282
+ const agent = new GlmAcpAgent(conn, { glm, visionClient: null, sessionStore: null });
1283
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
1284
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
1285
+ await agent.unstable_setSessionModel({ sessionId, modelId: "glm-5v-turbo" });
1286
+ await agent.prompt({
1287
+ sessionId,
1288
+ prompt: [
1289
+ { type: "text", text: "Describe" },
1290
+ { type: "image", data: "AAAA", mimeType: "image/jpeg" },
1291
+ ],
1292
+ });
1293
+ assert.deepEqual(capturedUser, [
1294
+ { type: "text", text: "Describe" },
1295
+ { type: "image_url", image_url: { url: "data:image/jpeg;base64,AAAA" } },
1296
+ ]);
1297
+ });
1298
+ test("prompt with unsupported image MIME on vision-native model emits an inline annotation", async () => {
1299
+ const conn = createConnectionStub();
1300
+ let capturedUser;
1301
+ const glm = {
1302
+ async *streamChat(messages) {
1303
+ const userMsg = [...messages].reverse().find((m) => m.role === "user");
1304
+ capturedUser = userMsg?.content;
1305
+ yield { text: "ok" };
1306
+ yield { done: true, stopReason: "stop" };
1307
+ },
1308
+ };
1309
+ const agent = new GlmAcpAgent(conn, { glm, visionClient: null, sessionStore: null });
1310
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
1311
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
1312
+ await agent.unstable_setSessionModel({ sessionId, modelId: "glm-5v-turbo" });
1313
+ await agent.prompt({
1314
+ sessionId,
1315
+ prompt: [
1316
+ { type: "text", text: "Describe" },
1317
+ { type: "image", data: "AAAA", mimeType: "image/webp" },
1318
+ ],
1319
+ });
1320
+ assert.ok(Array.isArray(capturedUser));
1321
+ const text = capturedUser
1322
+ .filter((part) => part.type === "text")
1323
+ .map((part) => part.text)
1324
+ .join("\n");
1325
+ assert.match(text, /<image_unsupported_format index="1" mime="image\/webp">/);
1326
+ assert.match(text, /image\/jpeg, image\/jpg, image\/png/);
1327
+ });
1031
1328
  test("prompt with image block but no vision client falls back to a text annotation", async () => {
1032
1329
  const conn = createConnectionStub();
1033
1330
  let capturedUser;
@@ -1152,6 +1449,7 @@ test("loadSession restores messages and replays them as session updates", async
1152
1449
  title: "ping pong",
1153
1450
  updatedAt: "2026-01-01T00:00:00.000Z",
1154
1451
  model: "glm-5.1",
1452
+ mode: "default",
1155
1453
  });
1156
1454
  const conn = createConnectionStub();
1157
1455
  const agent = new GlmAcpAgent(conn, { sessionStore: store });
@@ -1213,6 +1511,7 @@ test("resumeSession restores in-memory state without replaying messages", async
1213
1511
  title: "ping pong",
1214
1512
  updatedAt: "2026-01-01T00:00:00.000Z",
1215
1513
  model: "glm-5.1",
1514
+ mode: "default",
1216
1515
  });
1217
1516
  const conn = createConnectionStub();
1218
1517
  const agent = new GlmAcpAgent(conn, { sessionStore: store });
@@ -1270,6 +1569,7 @@ test("unstable_forkSession works on a session that exists only on disk", async (
1270
1569
  title: "origin",
1271
1570
  updatedAt: "2026-01-01T00:00:00.000Z",
1272
1571
  model: "glm-4.5",
1572
+ mode: "default",
1273
1573
  });
1274
1574
  const conn = createConnectionStub();
1275
1575
  const agent = new GlmAcpAgent(conn, { sessionStore: store });
@@ -1311,6 +1611,7 @@ test("listSessions surfaces persisted-but-not-in-memory sessions", async () => {
1311
1611
  title: "Saved earlier",
1312
1612
  updatedAt: "2026-01-01T00:00:00.000Z",
1313
1613
  model: "glm-5.1",
1614
+ mode: "default",
1314
1615
  });
1315
1616
  const conn = createConnectionStub();
1316
1617
  const agent = new GlmAcpAgent(conn, { sessionStore: store });
@@ -1323,4 +1624,94 @@ test("listSessions surfaces persisted-but-not-in-memory sessions", async () => {
1323
1624
  cleanup();
1324
1625
  }
1325
1626
  });
1627
+ test("prompt performs emergency compaction and retries on 1261 error", async () => {
1628
+ const conn = createConnectionStub();
1629
+ let callCount = 0;
1630
+ let messagesInSecondCall = 0;
1631
+ const glm = {
1632
+ async *streamChat(messages) {
1633
+ callCount++;
1634
+ if (callCount === 1) {
1635
+ // Simulate a Z.AI context overflow error (1261).
1636
+ const err = new Error("Prompt exceeds max length");
1637
+ err.error = { code: 1261 };
1638
+ throw err;
1639
+ }
1640
+ else {
1641
+ messagesInSecondCall = messages.length;
1642
+ yield { text: "Recovered." };
1643
+ yield { done: true, stopReason: "stop" };
1644
+ }
1645
+ },
1646
+ };
1647
+ const agent = new GlmAcpAgent(conn, { glm, sessionStore: null });
1648
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
1649
+ // Seed the session with a bunch of messages to be compacted. Pin the session
1650
+ // to a 128K-window model so the seeded history triggers the proactive/
1651
+ // emergency compaction paths (the default model now has a 1M window).
1652
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
1653
+ await agent.unstable_setSessionModel({ sessionId, modelId: "glm-5.1" });
1654
+ const session = agent.sessions.get(sessionId);
1655
+ assert.ok(session, "expected in-memory session to exist");
1656
+ for (let i = 0; i < 50; i++) {
1657
+ session.messages.push({ role: "user", content: "Very long message filler ".repeat(1000) });
1658
+ session.messages.push({ role: "assistant", content: "Intermediate response filler ".repeat(1000) });
1659
+ }
1660
+ const messagesBefore = session.messages.length;
1661
+ const result = await agent.prompt({
1662
+ sessionId,
1663
+ prompt: [{ type: "text", text: "Final trigger" }],
1664
+ });
1665
+ assert.equal(result.stopReason, "end_turn");
1666
+ assert.equal(callCount, 2, "expected two streamChat calls (one failed, one retried)");
1667
+ assert.ok(messagesInSecondCall < messagesBefore, "expected history to be compacted in the second call");
1668
+ // The system prompt (index 0) and the last 10 messages should be preserved.
1669
+ assert.ok(messagesInSecondCall >= 11);
1670
+ });
1671
+ test("prompt fails fast when context overflow persists after emergency compaction", async () => {
1672
+ const conn = createConnectionStub();
1673
+ let callCount = 0;
1674
+ let secondOverflow = null;
1675
+ const glm = {
1676
+ async *streamChat() {
1677
+ callCount++;
1678
+ if (callCount > 2) {
1679
+ throw new Error("streamChat retried more than once");
1680
+ }
1681
+ // Simulate a persistent Z.AI context overflow error (1261).
1682
+ const err = new Error("Prompt still exceeds max length after compaction");
1683
+ err.error = { code: 1261 };
1684
+ if (callCount === 2) {
1685
+ secondOverflow = err;
1686
+ }
1687
+ yield await Promise.reject(err);
1688
+ },
1689
+ };
1690
+ const agent = new GlmAcpAgent(conn, { glm, sessionStore: null });
1691
+ await agent.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} });
1692
+ const { sessionId } = await agent.newSession({ cwd: "/tmp", mcpServers: [] });
1693
+ // Use a try-catch to assert that prompt surfaces the error rather than retrying forever.
1694
+ // The agent.prompt method catches internal errors and emits them as agent messages,
1695
+ // so we check the updates emitted to the connection.
1696
+ let caught;
1697
+ try {
1698
+ await agent.prompt({
1699
+ sessionId,
1700
+ prompt: [{ type: "text", text: "Trigger overflow" }],
1701
+ });
1702
+ }
1703
+ catch (error) {
1704
+ caught = error;
1705
+ }
1706
+ const errorMessages = conn.updates.filter((u) => {
1707
+ const update = u.update;
1708
+ return (update?.sessionUpdate === "agent_message_chunk" &&
1709
+ update.content?.text?.includes("Context overflow persisted"));
1710
+ });
1711
+ assert.equal(callCount, 2, "expected exactly two calls: initial and one retry after compaction");
1712
+ assert.ok(caught instanceof Error);
1713
+ assert.equal(caught.message, "Context overflow persisted after emergency compaction");
1714
+ assert.equal(caught.cause, secondOverflow);
1715
+ assert.equal(errorMessages.length, 1, "expected an error message reporting persistent overflow");
1716
+ });
1326
1717
  //# sourceMappingURL=agent.test.js.map