@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 +35 -11
- package/dist/memory/advanced-memory.js +3 -0
- package/dist/memory/agent-memory.js +2 -0
- package/dist/memory/simple-memory.js +3 -0
- package/dist/package.json +1 -1
- package/dist/stream-markers.js +25 -0
- package/package.json +1 -1
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
|
|
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) {
|
package/dist/package.json
CHANGED
package/dist/stream-markers.js
CHANGED
|
@@ -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 = "";
|