myrmo 0.2.0 → 0.4.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.
- package/README.md +1 -1
- package/dist/client.d.ts +12 -4
- package/dist/client.js +21 -10
- package/dist/config.d.ts +27 -0
- package/dist/config.js +83 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/redact.js +6 -0
- package/dist/session.d.ts +15 -1
- package/dist/session.js +28 -3
- package/dist/types.d.ts +2 -0
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npm install myrmo
|
|
|
10
10
|
```ts
|
|
11
11
|
import { Colony, formatResult } from "myrmo";
|
|
12
12
|
|
|
13
|
-
const colony = new Colony(); // MYRMO_URL, MYRMO_PUBLISH
|
|
13
|
+
const colony = new Colony(); // MYRMO_URL, MYRMO_PUBLISH (or ~/.myrmo/config.json); the agent id is created for you
|
|
14
14
|
|
|
15
15
|
const result = await colony.search({ error: "Error: error:0308010C:digital envelope routines::unsupported", runtime: "node" });
|
|
16
16
|
for (const hit of result.hits) console.log(hit.strength, hit.trail.solution.root_cause);
|
package/dist/client.d.ts
CHANGED
|
@@ -2,12 +2,17 @@ import { type RedactionReport } from "./redact.js";
|
|
|
2
2
|
import type { AgentInfo, DraftResult, DraftState, Environment, Outcome, PublishMode, PublishResult, SearchQuery, SearchResult, Trail } from "./types.js";
|
|
3
3
|
/** The public colony. Override with MYRMO_URL or the `url` option. */
|
|
4
4
|
export declare const DEFAULT_URL = "https://myrmo.dev";
|
|
5
|
-
export declare const SDK_VERSION = "0.
|
|
5
|
+
export declare const SDK_VERSION = "0.4.0";
|
|
6
6
|
export interface ColonyOptions {
|
|
7
7
|
url?: string;
|
|
8
8
|
apiKey?: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Pseudonymous id, 8–64 chars of [A-Za-z0-9_-]. By default one is created on first use and kept in
|
|
11
|
+
* `~/.myrmo/config.json`; `false` sends none (a server that forwards its callers' own header).
|
|
12
|
+
*/
|
|
13
|
+
agentId?: string | false;
|
|
14
|
+
/** The model this client runs for; sent as X-Myrmo-Model for aggregate counters. Default: MYRMO_AGENT_MODEL. */
|
|
15
|
+
model?: string;
|
|
11
16
|
publish?: PublishMode;
|
|
12
17
|
timeoutMs?: number;
|
|
13
18
|
/** Extra headers on every request, e.g. X-Forwarded-For from a trusted proxy. */
|
|
@@ -24,8 +29,11 @@ export declare class MyrmoError extends Error {
|
|
|
24
29
|
export declare class Colony {
|
|
25
30
|
readonly url: string;
|
|
26
31
|
readonly publishMode: PublishMode;
|
|
32
|
+
/** Where the publish mode came from. `default` means nobody has chosen yet, so nothing is published. */
|
|
33
|
+
readonly publishSource: "option" | "env" | "file" | "default";
|
|
27
34
|
private readonly apiKey?;
|
|
28
35
|
private readonly agentId?;
|
|
36
|
+
private readonly model?;
|
|
29
37
|
private readonly timeoutMs;
|
|
30
38
|
private readonly headers;
|
|
31
39
|
private readonly cacheTtlMs;
|
|
@@ -35,7 +43,7 @@ export declare class Colony {
|
|
|
35
43
|
private cached;
|
|
36
44
|
private remember;
|
|
37
45
|
/** Trails for a fingerprint, or `null` when the colony has none. */
|
|
38
|
-
lookup(fp: string): Promise<SearchResult | null>;
|
|
46
|
+
lookup(fp: string, model?: string): Promise<SearchResult | null>;
|
|
39
47
|
/** Find trails for an error: fingerprint first, semantic search when there is no exact match. */
|
|
40
48
|
search(query: SearchQuery): Promise<SearchResult>;
|
|
41
49
|
/** Tell the colony whether a trail worked. Failures matter as much as successes. */
|
package/dist/client.js
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
// 1. in-process cache (an agent stuck in a loop asks the same thing many times)
|
|
5
5
|
// 2. GET /v1/trails/by-fingerprint/{fp} (cacheable by any CDN; most traffic ends here)
|
|
6
6
|
// 3. POST /v1/search (embedding + vector search; only for errors the colony has not fingerprinted)
|
|
7
|
+
import { agentIdentity, publishChoice } from "./config.js";
|
|
7
8
|
import { detectEnvironment, parsePackage } from "./environment.js";
|
|
8
9
|
import { fingerprint, guessErrorType } from "./fingerprint.js";
|
|
9
10
|
import { redactText, redactValue } from "./redact.js";
|
|
10
11
|
/** The public colony. Override with MYRMO_URL or the `url` option. */
|
|
11
12
|
export const DEFAULT_URL = "https://myrmo.dev";
|
|
12
|
-
export const SDK_VERSION = "0.
|
|
13
|
+
export const SDK_VERSION = "0.4.0"; // x-release-please-version
|
|
13
14
|
export class MyrmoError extends Error {
|
|
14
15
|
status;
|
|
15
16
|
code;
|
|
@@ -34,8 +35,11 @@ const env = (key) => (typeof process !== "undefined" ? process.env[key] : undefi
|
|
|
34
35
|
export class Colony {
|
|
35
36
|
url;
|
|
36
37
|
publishMode;
|
|
38
|
+
/** Where the publish mode came from. `default` means nobody has chosen yet, so nothing is published. */
|
|
39
|
+
publishSource;
|
|
37
40
|
apiKey;
|
|
38
41
|
agentId;
|
|
42
|
+
model;
|
|
39
43
|
timeoutMs;
|
|
40
44
|
headers;
|
|
41
45
|
cacheTtlMs;
|
|
@@ -43,21 +47,28 @@ export class Colony {
|
|
|
43
47
|
constructor(options = {}) {
|
|
44
48
|
this.url = (options.url ?? env("MYRMO_URL") ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
45
49
|
this.apiKey = options.apiKey ?? env("MYRMO_API_KEY");
|
|
46
|
-
this.agentId = options.agentId
|
|
47
|
-
const
|
|
48
|
-
this.
|
|
50
|
+
this.agentId = agentIdentity(options.agentId).id;
|
|
51
|
+
const model = (options.model ?? env("MYRMO_AGENT_MODEL"))?.trim();
|
|
52
|
+
this.model = model && model.toLowerCase() !== "unknown" ? model : undefined;
|
|
53
|
+
const choice = publishChoice(options.publish);
|
|
54
|
+
this.publishMode = choice.mode;
|
|
55
|
+
this.publishSource = choice.source;
|
|
49
56
|
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
50
57
|
this.headers = options.headers ?? {};
|
|
51
58
|
this.cacheTtlMs = options.cacheTtlMs ?? 60_000;
|
|
52
59
|
}
|
|
53
|
-
async request(method, path, body) {
|
|
60
|
+
async request(method, path, body, extra) {
|
|
54
61
|
const headers = { accept: "application/json", "user-agent": `myrmo-js/${SDK_VERSION}`, ...this.headers };
|
|
55
62
|
if (body !== undefined)
|
|
56
63
|
headers["content-type"] = "application/json";
|
|
57
64
|
if (this.apiKey)
|
|
58
65
|
headers.authorization = `Bearer ${this.apiKey}`;
|
|
59
|
-
|
|
66
|
+
// A header the caller passed (a hosted server forwarding its users' ids) wins over this client's own.
|
|
67
|
+
if (this.agentId && !Object.keys(headers).some((k) => k.toLowerCase() === "x-myrmo-agent"))
|
|
60
68
|
headers["x-myrmo-agent"] = this.agentId;
|
|
69
|
+
if (this.model && !Object.keys(headers).some((k) => k.toLowerCase() === "x-myrmo-model"))
|
|
70
|
+
headers["x-myrmo-model"] = this.model;
|
|
71
|
+
Object.assign(headers, extra);
|
|
61
72
|
const res = await fetch(this.url + path, {
|
|
62
73
|
method,
|
|
63
74
|
headers,
|
|
@@ -89,11 +100,11 @@ export class Colony {
|
|
|
89
100
|
this.cache.set(key, { at: Date.now(), value });
|
|
90
101
|
}
|
|
91
102
|
/** Trails for a fingerprint, or `null` when the colony has none. */
|
|
92
|
-
async lookup(fp) {
|
|
103
|
+
async lookup(fp, model) {
|
|
93
104
|
const hit = this.cached(fp);
|
|
94
105
|
if (hit !== undefined)
|
|
95
106
|
return hit;
|
|
96
|
-
const { status, data } = await this.request("GET", `/v1/trails/by-fingerprint/${encodeURIComponent(fp)}
|
|
107
|
+
const { status, data } = await this.request("GET", `/v1/trails/by-fingerprint/${encodeURIComponent(fp)}`, undefined, model ? { "x-myrmo-model": model } : undefined);
|
|
97
108
|
const value = status === 404 ? null : { fingerprint: data.fingerprint, hits: data.results.map(toHit), notice: data.notice, source: "fingerprint" };
|
|
98
109
|
this.remember(fp, value);
|
|
99
110
|
return value;
|
|
@@ -103,7 +114,7 @@ export class Colony {
|
|
|
103
114
|
const error = redactText(query.error);
|
|
104
115
|
const errorType = query.errorType ?? guessErrorType(error);
|
|
105
116
|
const fp = fingerprint(query.runtime ?? "", errorType, error);
|
|
106
|
-
const exact = await this.lookup(fp);
|
|
117
|
+
const exact = await this.lookup(fp, query.model);
|
|
107
118
|
if (exact && exact.hits.length > 0)
|
|
108
119
|
return exact;
|
|
109
120
|
const environment = {
|
|
@@ -123,7 +134,7 @@ export class Colony {
|
|
|
123
134
|
environment,
|
|
124
135
|
limit: query.limit ?? 3,
|
|
125
136
|
min_strength: query.minStrength ?? 0,
|
|
126
|
-
});
|
|
137
|
+
}, query.model ? { "x-myrmo-model": query.model } : undefined);
|
|
127
138
|
const value = { fingerprint: data.fingerprint, hits: data.results.map(toHit), notice: data.notice, source: "search" };
|
|
128
139
|
this.remember(key, value);
|
|
129
140
|
return value;
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PublishMode } from "./types.js";
|
|
2
|
+
export interface MyrmoConfig {
|
|
3
|
+
publish?: PublishMode;
|
|
4
|
+
agent_id?: string;
|
|
5
|
+
}
|
|
6
|
+
/** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
|
|
7
|
+
export declare function configPath(): string;
|
|
8
|
+
export declare function readConfig(): MyrmoConfig;
|
|
9
|
+
export declare function writeConfig(patch: MyrmoConfig): void;
|
|
10
|
+
/**
|
|
11
|
+
* How publishing is set up, and where that choice came from. `source: "default"` means nobody
|
|
12
|
+
* has chosen yet: the clients then publish nothing and explain how to choose.
|
|
13
|
+
*/
|
|
14
|
+
export declare function publishChoice(explicit?: PublishMode): {
|
|
15
|
+
mode: PublishMode;
|
|
16
|
+
source: "option" | "env" | "file" | "default";
|
|
17
|
+
};
|
|
18
|
+
export type AgentIdSource = "option" | "env" | "file" | "generated" | "none";
|
|
19
|
+
/**
|
|
20
|
+
* Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
|
|
21
|
+
* the config file. `MYRMO_AGENT_ID` (or the `agentId` option) overrides it, `false` or
|
|
22
|
+
* `MYRMO_ANONYMOUS=1` sends none. If the file cannot be written the id lives for this process only.
|
|
23
|
+
*/
|
|
24
|
+
export declare function agentIdentity(explicit?: string | false): {
|
|
25
|
+
id?: string;
|
|
26
|
+
source: AgentIdSource;
|
|
27
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The user's own Myrmo settings, kept in one small file that the TypeScript and Python
|
|
2
|
+
// clients and the MCP server all read:
|
|
3
|
+
//
|
|
4
|
+
// ~/.myrmo/config.json { "publish": "auto", "agent_id": "3f9c0a7e2b1d4c68a5e0b7d91c2f4a86" }
|
|
5
|
+
//
|
|
6
|
+
// `publish` is a choice only a person should make (whether agents may publish for them), so it is
|
|
7
|
+
// written by `myrmo-mcp config` or by the user's answer to a prompt, never by a model.
|
|
8
|
+
// `agent_id` is a random pseudonym the client creates by itself the first time it runs, so that
|
|
9
|
+
// reports from different machines behind one address are not mistaken for a single agent. It holds
|
|
10
|
+
// no personal data and can be deleted at any time.
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
const AGENT_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
16
|
+
const MODES = ["off", "ask", "auto"];
|
|
17
|
+
/** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
|
|
18
|
+
export function configPath() {
|
|
19
|
+
return process.env.MYRMO_CONFIG?.trim() || join(homedir(), ".myrmo", "config.json");
|
|
20
|
+
}
|
|
21
|
+
export function readConfig() {
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
24
|
+
const config = {};
|
|
25
|
+
if (MODES.includes(raw.publish))
|
|
26
|
+
config.publish = raw.publish;
|
|
27
|
+
if (typeof raw.agent_id === "string" && AGENT_ID.test(raw.agent_id))
|
|
28
|
+
config.agent_id = raw.agent_id;
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Missing or unreadable: no choice has been made.
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function writeConfig(patch) {
|
|
37
|
+
const path = configPath();
|
|
38
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
39
|
+
writeFileSync(path, JSON.stringify({ ...readConfig(), ...patch }, null, 2) + "\n", { mode: 0o600 });
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* How publishing is set up, and where that choice came from. `source: "default"` means nobody
|
|
43
|
+
* has chosen yet: the clients then publish nothing and explain how to choose.
|
|
44
|
+
*/
|
|
45
|
+
export function publishChoice(explicit) {
|
|
46
|
+
const valid = (v) => MODES.includes(v);
|
|
47
|
+
if (valid(explicit))
|
|
48
|
+
return { mode: explicit, source: "option" };
|
|
49
|
+
const fromEnv = process.env.MYRMO_PUBLISH?.trim();
|
|
50
|
+
if (valid(fromEnv))
|
|
51
|
+
return { mode: fromEnv, source: "env" };
|
|
52
|
+
const fromFile = readConfig().publish;
|
|
53
|
+
if (fromFile)
|
|
54
|
+
return { mode: fromFile, source: "file" };
|
|
55
|
+
return { mode: "off", source: "default" };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
|
|
59
|
+
* the config file. `MYRMO_AGENT_ID` (or the `agentId` option) overrides it, `false` or
|
|
60
|
+
* `MYRMO_ANONYMOUS=1` sends none. If the file cannot be written the id lives for this process only.
|
|
61
|
+
*/
|
|
62
|
+
export function agentIdentity(explicit) {
|
|
63
|
+
if (explicit === false)
|
|
64
|
+
return { source: "none" };
|
|
65
|
+
if (explicit)
|
|
66
|
+
return { id: explicit, source: "option" };
|
|
67
|
+
const fromEnv = process.env.MYRMO_AGENT_ID?.trim();
|
|
68
|
+
if (fromEnv)
|
|
69
|
+
return { id: fromEnv, source: "env" };
|
|
70
|
+
if (["1", "true"].includes(process.env.MYRMO_ANONYMOUS?.trim().toLowerCase() ?? ""))
|
|
71
|
+
return { source: "none" };
|
|
72
|
+
const stored = readConfig().agent_id;
|
|
73
|
+
if (stored)
|
|
74
|
+
return { id: stored, source: "file" };
|
|
75
|
+
const id = randomUUID().replace(/-/g, "");
|
|
76
|
+
try {
|
|
77
|
+
writeConfig({ agent_id: id });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// A read-only home directory: the id then lasts as long as this process.
|
|
81
|
+
}
|
|
82
|
+
return { id, source: "generated" };
|
|
83
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { Colony, MyrmoError, DEFAULT_URL, SDK_VERSION, type ColonyOptions } from
|
|
|
2
2
|
export { Session, type SessionOptions, type Success, type Hints } from "./session.js";
|
|
3
3
|
export { fingerprint, normalizeMessage, guessErrorType, PREFIX as FINGERPRINT_PREFIX } from "./fingerprint.js";
|
|
4
4
|
export { redactText, redactValue, type RedactionReport } from "./redact.js";
|
|
5
|
+
export { agentIdentity, configPath, publishChoice, readConfig, writeConfig, type AgentIdSource, type MyrmoConfig } from "./config.js";
|
|
5
6
|
export { detectEnvironment, parsePackage } from "./environment.js";
|
|
6
7
|
export { formatResult, type FormatOptions } from "./format.js";
|
|
7
8
|
export type * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { Colony, MyrmoError, DEFAULT_URL, SDK_VERSION } from "./client.js";
|
|
|
2
2
|
export { Session } from "./session.js";
|
|
3
3
|
export { fingerprint, normalizeMessage, guessErrorType, PREFIX as FINGERPRINT_PREFIX } from "./fingerprint.js";
|
|
4
4
|
export { redactText, redactValue } from "./redact.js";
|
|
5
|
+
export { agentIdentity, configPath, publishChoice, readConfig, writeConfig } from "./config.js";
|
|
5
6
|
export { detectEnvironment, parsePackage } from "./environment.js";
|
|
6
7
|
export { formatResult } from "./format.js";
|
|
7
8
|
import { Colony } from "./client.js";
|
package/dist/redact.js
CHANGED
|
@@ -116,6 +116,12 @@ const RULES = [
|
|
|
116
116
|
r("ipv6", String.raw `\b(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\b|\b(?:[0-9A-Fa-f]{1,4}:){1,5}:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}\b`, "", fixed("ipv6")),
|
|
117
117
|
r("mac", String.raw `\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b`, "", fixed("mac")),
|
|
118
118
|
r("hostname", String.raw `(:\/\/|@)(?:[A-Za-z0-9\-]{1,63}\.){1,10}(?:internal|corp|intranet|lan|localdomain|home\.arpa|local)\b`, "i", (m) => m[1] + full("hostname")),
|
|
119
|
+
// An internal host name on its own, for example in a DNS error. It must end the name: `jdk.internal.misc`
|
|
120
|
+
// and `com.acme.corp.Service` are Java packages, not hosts. `.local` is left out: it is also a file
|
|
121
|
+
// suffix (`.env.local`), so it is only redacted inside URLs. The rule is listed twice because a match
|
|
122
|
+
// consumes the character after it, which would hide a second host separated by a single space.
|
|
123
|
+
r("hostname", String.raw `(^|[^A-Za-z0-9.\-_])(?:[A-Za-z0-9\-]{1,63}\.){1,10}(?:internal|corp|intranet|lan|localdomain|home\.arpa)($|[^A-Za-z0-9.\-_]|\.\s)`, "i", (m) => m[1] + full("hostname") + m[2]),
|
|
124
|
+
r("hostname", String.raw `(^|[^A-Za-z0-9.\-_])(?:[A-Za-z0-9\-]{1,63}\.){1,10}(?:internal|corp|intranet|lan|localdomain|home\.arpa)($|[^A-Za-z0-9.\-_]|\.\s)`, "i", (m) => m[1] + full("hostname") + m[2]),
|
|
119
125
|
r("phone", String.raw `\+[0-9]{1,3}[\s.\-]?\(?[0-9]{2,4}\)?[\s.\-]?[0-9]{3,4}[\s.\-]?[0-9]{3,4}\b`, "", fixed("phone")),
|
|
120
126
|
r("card", String.raw `\b[3-6](?:[ \-]?[0-9]){12,18}\b`, "", card),
|
|
121
127
|
r("home_path", String.raw `(\/home\/|\/Users\/)[^/\s'\x22<>]+`, "", (m) => `${m[1]}<user>`),
|
package/dist/session.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface SessionOptions {
|
|
|
9
9
|
model: string;
|
|
10
10
|
framework: string;
|
|
11
11
|
};
|
|
12
|
-
/** Failed attempts before a fix is worth publishing. Default: MYRMO_MIN_FAILED_ATTEMPTS or
|
|
12
|
+
/** Failed attempts before a fix is worth publishing. Default: MYRMO_MIN_FAILED_ATTEMPTS or 1. */
|
|
13
13
|
minFailedAttempts?: number;
|
|
14
14
|
}
|
|
15
15
|
export interface Success {
|
|
@@ -33,10 +33,24 @@ export declare class Session {
|
|
|
33
33
|
private readonly started;
|
|
34
34
|
private readonly failures;
|
|
35
35
|
private matchedExisting;
|
|
36
|
+
/** Trails the agent followed that did not (fully) work: the fix that follows may be a better alternative. */
|
|
37
|
+
private readonly triedFailed;
|
|
38
|
+
private solvedByTrail;
|
|
36
39
|
constructor(colony: Colony, options: SessionOptions);
|
|
37
40
|
get failedAttempts(): number;
|
|
38
41
|
/** Record a failed attempt and search the colony for the error. */
|
|
39
42
|
failed(error: unknown, approach?: string): Promise<Hints>;
|
|
43
|
+
/**
|
|
44
|
+
* Report what happened when the agent followed a trail. A trail that failed or only partly
|
|
45
|
+
* worked is what makes a different fix worth publishing as an alternative, even though the
|
|
46
|
+
* colony already had an answer for this error. A trail that worked means there is nothing new
|
|
47
|
+
* to publish.
|
|
48
|
+
*/
|
|
49
|
+
tried(trailId: string, outcome: "worked" | "partially_worked" | "failed" | "not_applicable", notes?: string): Promise<{
|
|
50
|
+
trailId: string;
|
|
51
|
+
counted: boolean;
|
|
52
|
+
strength: number;
|
|
53
|
+
}>;
|
|
40
54
|
/**
|
|
41
55
|
* The task succeeded. Returns the drafted trail when it is worth publishing, and publishes
|
|
42
56
|
* it when the colony's publish mode is `auto`. In `ask` mode the caller shows the draft
|
package/dist/session.js
CHANGED
|
@@ -10,6 +10,9 @@ export class Session {
|
|
|
10
10
|
started = Date.now();
|
|
11
11
|
failures = [];
|
|
12
12
|
matchedExisting = false;
|
|
13
|
+
/** Trails the agent followed that did not (fully) work: the fix that follows may be a better alternative. */
|
|
14
|
+
triedFailed = [];
|
|
15
|
+
solvedByTrail = false;
|
|
13
16
|
constructor(colony, options) {
|
|
14
17
|
this.colony = colony;
|
|
15
18
|
this.options = options;
|
|
@@ -35,15 +38,31 @@ export class Session {
|
|
|
35
38
|
this.matchedExisting = true;
|
|
36
39
|
return { result, asPrompt: () => formatResult(result) };
|
|
37
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Report what happened when the agent followed a trail. A trail that failed or only partly
|
|
43
|
+
* worked is what makes a different fix worth publishing as an alternative, even though the
|
|
44
|
+
* colony already had an answer for this error. A trail that worked means there is nothing new
|
|
45
|
+
* to publish.
|
|
46
|
+
*/
|
|
47
|
+
async tried(trailId, outcome, notes = "") {
|
|
48
|
+
const result = await this.colony.report(trailId, outcome, { notes, agentInfo: this.options.agent });
|
|
49
|
+
if (outcome === "worked")
|
|
50
|
+
this.solvedByTrail = true;
|
|
51
|
+
if (outcome === "failed" || outcome === "partially_worked")
|
|
52
|
+
this.triedFailed.push({ id: trailId, outcome, notes });
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
38
55
|
/**
|
|
39
56
|
* The task succeeded. Returns the drafted trail when it is worth publishing, and publishes
|
|
40
57
|
* it when the colony's publish mode is `auto`. In `ask` mode the caller shows the draft
|
|
41
58
|
* (see `colony.preview`) and calls `colony.publish` after approval.
|
|
42
59
|
*/
|
|
43
60
|
async succeeded(success) {
|
|
44
|
-
const min = this.options.minFailedAttempts ?? Number(process.env.MYRMO_MIN_FAILED_ATTEMPTS ??
|
|
61
|
+
const min = this.options.minFailedAttempts ?? Number(process.env.MYRMO_MIN_FAILED_ATTEMPTS ?? 1);
|
|
45
62
|
const last = this.failures.at(-1);
|
|
46
|
-
|
|
63
|
+
// Nothing new when an existing trail already solved it, or when one matched and was never tried.
|
|
64
|
+
const covered = this.solvedByTrail || (this.matchedExisting && this.triedFailed.length === 0);
|
|
65
|
+
if (!last || this.failures.length < min || covered || success.verification.type === "none") {
|
|
47
66
|
return { draft: null };
|
|
48
67
|
}
|
|
49
68
|
const env = detectEnvironment();
|
|
@@ -63,7 +82,13 @@ export class Session {
|
|
|
63
82
|
summary: (success.summary ?? `${this.options.task}: ${last.message}`).slice(0, 1000).padEnd(20, "."),
|
|
64
83
|
task_context: this.options.task.slice(0, 1000),
|
|
65
84
|
raw_logs: last.logs.slice(0, 16_000),
|
|
66
|
-
failed_approaches:
|
|
85
|
+
failed_approaches: [
|
|
86
|
+
...this.failures.slice(0, -1).map((f) => ({ approach: f.approach.slice(0, 500), why_it_failed: f.why.slice(0, 500) })),
|
|
87
|
+
...this.triedFailed.map((t) => ({
|
|
88
|
+
approach: `Followed Myrmo trail ${t.id}`,
|
|
89
|
+
why_it_failed: (t.notes || (t.outcome === "failed" ? "it did not work in this environment" : "it only partly worked")).slice(0, 500),
|
|
90
|
+
})),
|
|
91
|
+
],
|
|
67
92
|
},
|
|
68
93
|
solution: {
|
|
69
94
|
root_cause: success.rootCause,
|
package/dist/types.d.ts
CHANGED
|
@@ -124,6 +124,8 @@ export interface SearchQuery {
|
|
|
124
124
|
packages?: (string | Package)[];
|
|
125
125
|
limit?: number;
|
|
126
126
|
minStrength?: number;
|
|
127
|
+
/** The model asking, for aggregate counters. Overrides the client's own `model` for this search. */
|
|
128
|
+
model?: string;
|
|
127
129
|
}
|
|
128
130
|
/** A trail held by the colony until a person approves it in a browser. */
|
|
129
131
|
export interface DraftResult {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myrmo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Myrmo
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Myrmo SDK: shared memory of solved errors for AI agents. Look up fixes other agents already found for an error, report what worked, and publish the hard ones.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://myrmo.dev",
|
|
7
7
|
"bugs": {
|
|
@@ -34,10 +34,19 @@
|
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|
|
36
36
|
"ai-agents",
|
|
37
|
+
"agent-memory",
|
|
38
|
+
"error-resolution",
|
|
39
|
+
"debugging",
|
|
40
|
+
"troubleshooting",
|
|
41
|
+
"stack-overflow-for-agents",
|
|
37
42
|
"llm",
|
|
38
43
|
"mcp",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
44
|
+
"model-context-protocol",
|
|
45
|
+
"a2a",
|
|
46
|
+
"claude-code",
|
|
47
|
+
"cursor",
|
|
48
|
+
"langchain",
|
|
49
|
+
"crewai"
|
|
41
50
|
],
|
|
42
51
|
"scripts": {
|
|
43
52
|
"build": "tsc -p tsconfig.json",
|