@tjamescouch/gro 1.3.9 → 1.3.11

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/dist/main.js CHANGED
@@ -22,7 +22,7 @@ import { groError, asError, isGroError, errorLogFields } from "./errors.js";
22
22
  import { bashToolDefinition, executeBash } from "./tools/bash.js";
23
23
  import { agentpatchToolDefinition, executeAgentpatch } from "./tools/agentpatch.js";
24
24
  import { groVersionToolDefinition, executeGroVersion, getGroVersion } from "./tools/version.js";
25
- import { createMarkerParser } from "./stream-markers.js";
25
+ import { createMarkerParser, extractMarkers } from "./stream-markers.js";
26
26
  const VERSION = getGroVersion();
27
27
  // ---------------------------------------------------------------------------
28
28
  // Graceful shutdown state — module-level so signal handlers can save sessions.
@@ -520,19 +520,23 @@ async function executeTurn(driver, memory, mcp, cfg, sessionId) {
520
520
  let brokeCleanly = false;
521
521
  let idleNudges = 0;
522
522
  for (let round = 0; round < cfg.maxToolRounds; round++) {
523
+ // Shared marker handler — used by both streaming parser and tool-arg scanner
524
+ const handleMarker = (marker) => {
525
+ if (marker.name === "model-change") {
526
+ const newModel = resolveModelAlias(marker.arg);
527
+ Logger.info(`Stream marker: model-change '${marker.arg}' → ${newModel}`);
528
+ activeModel = newModel;
529
+ cfg.model = newModel; // persist across turns
530
+ memory.setModel(newModel); // persist in session metadata on save
531
+ }
532
+ else {
533
+ Logger.debug(`Stream marker: ${marker.name}('${marker.arg}')`);
534
+ }
535
+ };
523
536
  // Create a fresh marker parser per round so partial state doesn't leak
524
537
  const markerParser = createMarkerParser({
525
538
  onToken: rawOnToken,
526
- onMarker(marker) {
527
- if (marker.name === "model-change") {
528
- const newModel = resolveModelAlias(marker.arg);
529
- Logger.info(`Stream marker: model-change '${marker.arg}' → ${newModel}`);
530
- activeModel = newModel;
531
- }
532
- else {
533
- Logger.debug(`Stream marker: ${marker.name}('${marker.arg}')`);
534
- }
535
- },
539
+ onMarker: handleMarker,
536
540
  });
537
541
  const output = await driver.chat(memory.messages(), {
538
542
  model: activeModel,
@@ -593,6 +597,14 @@ async function executeTurn(driver, memory, mcp, cfg, sessionId) {
593
597
  Logger.debug(`Failed to parse args for ${fnName}: ${asError(e).message}, using empty args`);
594
598
  fnArgs = {};
595
599
  }
600
+ // Scan tool call string args for stream markers (e.g. model sends
601
+ // @@model-change('haiku')@@ inside an agentchat_send message).
602
+ // Strip markers from args so they don't leak into tool output.
603
+ for (const key of Object.keys(fnArgs)) {
604
+ if (typeof fnArgs[key] === "string") {
605
+ fnArgs[key] = extractMarkers(fnArgs[key], handleMarker);
606
+ }
607
+ }
596
608
  Logger.debug(`Tool call: ${fnName}(${JSON.stringify(fnArgs)})`);
597
609
  let result;
598
610
  try {
@@ -684,6 +696,12 @@ async function singleShot(cfg, driver, mcp, sessionId, positionalArgs) {
684
696
  // Resume existing session if requested
685
697
  if (cfg.continueSession || cfg.resumeSession) {
686
698
  await memory.load(sessionId);
699
+ const sess = loadSession(sessionId);
700
+ if (sess?.meta.model && sess.meta.model !== cfg.model) {
701
+ Logger.info(`Restoring model from session: ${cfg.model} → ${sess.meta.model}`);
702
+ cfg.model = sess.meta.model;
703
+ memory.setModel(sess.meta.model);
704
+ }
687
705
  }
688
706
  await memory.add({ role: "user", from: "User", content: prompt });
689
707
  let text;
@@ -733,6 +751,12 @@ async function interactive(cfg, driver, mcp, sessionId) {
733
751
  if (sess) {
734
752
  const msgCount = sess.messages.filter((m) => m.role !== "system").length;
735
753
  Logger.info(C.gray(`Resumed session ${sessionId} (${msgCount} messages)`));
754
+ // Restore model from session metadata (e.g. after a stream marker model-change)
755
+ if (sess.meta.model && sess.meta.model !== cfg.model) {
756
+ Logger.info(`Restoring model from session: ${cfg.model} → ${sess.meta.model}`);
757
+ cfg.model = sess.meta.model;
758
+ memory.setModel(sess.meta.model);
759
+ }
736
760
  }
737
761
  }
738
762
  const rl = readline.createInterface({
@@ -24,6 +24,9 @@ export class AdvancedMemory extends AgentMemory {
24
24
  this.keepRecentPerLane = Math.max(1, Math.floor(args.keepRecentPerLane ?? 4));
25
25
  this.keepRecentTools = Math.max(0, Math.floor(args.keepRecentTools ?? 3));
26
26
  }
27
+ setModel(model) {
28
+ this.model = model;
29
+ }
27
30
  async load(id) {
28
31
  const session = loadSession(id);
29
32
  if (session) {
@@ -22,6 +22,8 @@ export class AgentMemory {
22
22
  await this.onAfterAdd();
23
23
  }
24
24
  }
25
+ /** Update the active model (used by stream markers to persist model changes across turns). */
26
+ setModel(_model) { }
25
27
  messages() {
26
28
  return [...this.messagesBuffer];
27
29
  }
@@ -15,6 +15,9 @@ export class SimpleMemory extends AgentMemory {
15
15
  this.provider = provider;
16
16
  this.model = model;
17
17
  }
18
+ setModel(model) {
19
+ this.model = model;
20
+ }
18
21
  async load(id) {
19
22
  const session = loadSession(id);
20
23
  if (session) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tjamescouch/gro",
3
- "version": "1.3.9",
3
+ "version": "1.3.11",
4
4
  "description": "Provider-agnostic LLM runtime with context management",
5
5
  "bin": {
6
6
  "gro": "./dist/main.js"
@@ -27,6 +27,31 @@ import { Logger } from "./logger.js";
27
27
  const MARKER_RE = /@@([a-zA-Z][a-zA-Z0-9_-]*)\((?:'([^']*)'|"([^"]*)"|([^)]*?))\)@@/g;
28
28
  /** Partial marker detection — we might be mid-stream in a marker */
29
29
  const PARTIAL_MARKER_RE = /@@[a-zA-Z][a-zA-Z0-9_-]*(?:\([^)]*)?$/;
30
+ /**
31
+ * Scan a string for markers, fire the handler for each, and return cleaned text.
32
+ * Unlike the streaming parser, this operates on a complete string (e.g. tool call arguments).
33
+ */
34
+ export function extractMarkers(text, onMarker) {
35
+ let cleaned = "";
36
+ let lastIndex = 0;
37
+ const regex = new RegExp(MARKER_RE.source, "g");
38
+ let match;
39
+ while ((match = regex.exec(text)) !== null) {
40
+ cleaned += text.slice(lastIndex, match.index);
41
+ const marker = {
42
+ name: match[1],
43
+ arg: match[2] ?? match[3] ?? match[4] ?? "",
44
+ raw: match[0],
45
+ };
46
+ try {
47
+ onMarker(marker);
48
+ }
49
+ catch { /* handled by caller */ }
50
+ lastIndex = match.index + match[0].length;
51
+ }
52
+ cleaned += text.slice(lastIndex);
53
+ return cleaned;
54
+ }
30
55
  export function createMarkerParser(opts) {
31
56
  const { onMarker, onToken } = opts;
32
57
  let buffer = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tjamescouch/gro",
3
- "version": "1.3.9",
3
+ "version": "1.3.11",
4
4
  "description": "Provider-agnostic LLM runtime with context management",
5
5
  "bin": {
6
6
  "gro": "./dist/main.js"