auggy 0.3.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/CHANGELOG.md +96 -0
- package/LICENSE +201 -0
- package/README.md +161 -0
- package/package.json +76 -0
- package/src/agent-card.ts +39 -0
- package/src/agent.ts +283 -0
- package/src/agentmail-client.ts +138 -0
- package/src/augments/bash/index.ts +463 -0
- package/src/augments/bash/skill/SKILL.md +156 -0
- package/src/augments/budgets/budget-store.ts +513 -0
- package/src/augments/budgets/index.ts +134 -0
- package/src/augments/budgets/preamble.ts +93 -0
- package/src/augments/budgets/types.ts +89 -0
- package/src/augments/file-memory/index.ts +71 -0
- package/src/augments/filesystem/index.ts +533 -0
- package/src/augments/filesystem/skill/SKILL.md +142 -0
- package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
- package/src/augments/layered-memory/extractor/buffer.ts +56 -0
- package/src/augments/layered-memory/extractor/frequency.ts +79 -0
- package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
- package/src/augments/layered-memory/extractor/parse.ts +75 -0
- package/src/augments/layered-memory/extractor/prompt.md +26 -0
- package/src/augments/layered-memory/index.ts +757 -0
- package/src/augments/layered-memory/skill/SKILL.md +153 -0
- package/src/augments/layered-memory/storage/migrations/README.md +16 -0
- package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
- package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
- package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
- package/src/augments/layered-memory/storage/types.ts +98 -0
- package/src/augments/link/index.ts +489 -0
- package/src/augments/link/translate.ts +261 -0
- package/src/augments/notify/adapters/agentmail.ts +70 -0
- package/src/augments/notify/adapters/telegram.ts +60 -0
- package/src/augments/notify/adapters/webhook.ts +55 -0
- package/src/augments/notify/index.ts +284 -0
- package/src/augments/notify/skill/SKILL.md +150 -0
- package/src/augments/org-context/index.ts +721 -0
- package/src/augments/org-context/skill/SKILL.md +96 -0
- package/src/augments/skills/index.ts +103 -0
- package/src/augments/supabase-memory/index.ts +151 -0
- package/src/augments/telegram-transport/index.ts +312 -0
- package/src/augments/telegram-transport/polling.ts +55 -0
- package/src/augments/telegram-transport/webhook.ts +56 -0
- package/src/augments/turn-control/index.ts +61 -0
- package/src/augments/turn-control/skill/SKILL.md +155 -0
- package/src/augments/visitor-auth/email-validation.ts +66 -0
- package/src/augments/visitor-auth/index.ts +779 -0
- package/src/augments/visitor-auth/rate-limiter.ts +90 -0
- package/src/augments/visitor-auth/skill/SKILL.md +55 -0
- package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
- package/src/augments/visitor-auth/storage/types.ts +164 -0
- package/src/augments/visitor-auth/types.ts +123 -0
- package/src/augments/visitor-auth/verify-page.ts +179 -0
- package/src/augments/web-fetch/index.ts +331 -0
- package/src/augments/web-fetch/skill/SKILL.md +100 -0
- package/src/cli/agent-index.ts +289 -0
- package/src/cli/augment-catalog.ts +320 -0
- package/src/cli/augment-resolver.ts +597 -0
- package/src/cli/commands/add-skill.ts +194 -0
- package/src/cli/commands/add.ts +87 -0
- package/src/cli/commands/chat.ts +207 -0
- package/src/cli/commands/create.ts +462 -0
- package/src/cli/commands/dev.ts +139 -0
- package/src/cli/commands/eval.ts +180 -0
- package/src/cli/commands/ls.ts +66 -0
- package/src/cli/commands/remove.ts +95 -0
- package/src/cli/commands/restart.ts +40 -0
- package/src/cli/commands/start.ts +123 -0
- package/src/cli/commands/status.ts +104 -0
- package/src/cli/commands/stop.ts +84 -0
- package/src/cli/commands/visitors-revoke.ts +155 -0
- package/src/cli/commands/visitors.ts +101 -0
- package/src/cli/config-parser.ts +1034 -0
- package/src/cli/engine-resolver.ts +68 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/model-picker.ts +89 -0
- package/src/cli/pid-registry.ts +146 -0
- package/src/cli/plist-generator.ts +117 -0
- package/src/cli/resolve-config.ts +56 -0
- package/src/cli/scaffold-skills.ts +158 -0
- package/src/cli/scaffold.ts +291 -0
- package/src/cli/skill-frontmatter.ts +51 -0
- package/src/cli/skill-validator.ts +151 -0
- package/src/cli/types.ts +228 -0
- package/src/cli/yaml-helpers.ts +66 -0
- package/src/engines/_shared/cost.ts +55 -0
- package/src/engines/_shared/schema-normalize.ts +75 -0
- package/src/engines/anthropic/pricing.ts +117 -0
- package/src/engines/anthropic.ts +483 -0
- package/src/engines/openai/pricing.ts +67 -0
- package/src/engines/openai.ts +446 -0
- package/src/engines/openrouter/pricing.ts +83 -0
- package/src/engines/openrouter.ts +185 -0
- package/src/helpers.ts +24 -0
- package/src/http.ts +387 -0
- package/src/index.ts +165 -0
- package/src/kernel/capability-table.ts +172 -0
- package/src/kernel/context-allocator.ts +161 -0
- package/src/kernel/history-manager.ts +198 -0
- package/src/kernel/lifecycle-manager.ts +106 -0
- package/src/kernel/output-validator.ts +35 -0
- package/src/kernel/preamble.ts +23 -0
- package/src/kernel/route-collector.ts +97 -0
- package/src/kernel/timeout.ts +21 -0
- package/src/kernel/tool-selector.ts +47 -0
- package/src/kernel/trace-emitter.ts +66 -0
- package/src/kernel/transport-queue.ts +147 -0
- package/src/kernel/turn-loop.ts +1148 -0
- package/src/memory/context-synthesis.ts +83 -0
- package/src/memory/memory-bus.ts +61 -0
- package/src/memory/registry.ts +80 -0
- package/src/memory/tools.ts +320 -0
- package/src/memory/types.ts +8 -0
- package/src/parts.ts +30 -0
- package/src/scaffold-templates/identity.md +31 -0
- package/src/telegram-client.ts +145 -0
- package/src/tokenizer.ts +14 -0
- package/src/transports/ag-ui-events.ts +253 -0
- package/src/transports/visitor-token.ts +82 -0
- package/src/transports/web-transport.ts +948 -0
- package/src/types.ts +1009 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Mount Permissions Reference
|
|
2
|
+
|
|
3
|
+
## Permission matrix
|
|
4
|
+
|
|
5
|
+
| Operation | Read-only | Writable | Writable + Deletable |
|
|
6
|
+
|-----------|-----------|----------|---------------------|
|
|
7
|
+
| `fs_read` | ✓ | ✓ | ✓ |
|
|
8
|
+
| `fs_list` | ✓ | ✓ | ✓ |
|
|
9
|
+
| `fs_search` | ✓ | ✓ | ✓ |
|
|
10
|
+
| `fs_write` | ✗ | ✓ | ✓ |
|
|
11
|
+
| `fs_mkdir` | ✗ | ✓ | ✓ |
|
|
12
|
+
| `fs_remove` | ✗ | ✗ | ✓ |
|
|
13
|
+
|
|
14
|
+
## Mount configuration shape
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
{
|
|
18
|
+
name: string; // logical name — first path segment
|
|
19
|
+
path: string; // physical path on disk
|
|
20
|
+
writable?: boolean; // default false
|
|
21
|
+
deletable?: boolean; // default false (requires writable: true)
|
|
22
|
+
maxReadSize?: number; // default 256KB (262144 bytes)
|
|
23
|
+
maxWriteSize?: number; // default 1MB (1048576 bytes)
|
|
24
|
+
searchExcludes?: string[]; // default [".git", "node_modules", ".next", "__pycache__", ".DS_Store"]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Typical mount configurations
|
|
29
|
+
|
|
30
|
+
### Skills (read-only)
|
|
31
|
+
```typescript
|
|
32
|
+
{ name: "skills", path: "./augments", writable: false }
|
|
33
|
+
```
|
|
34
|
+
For reading SKILL.md files, references, and examples. NEVER writable — prevents the agent from modifying its own behavioral teaching.
|
|
35
|
+
|
|
36
|
+
### Workspace (writable + deletable)
|
|
37
|
+
```typescript
|
|
38
|
+
{ name: "workspace", path: "./workspace", writable: true, deletable: true }
|
|
39
|
+
```
|
|
40
|
+
Agent's personal working directory. Full read/write/delete access. Used for notes, drafts, intermediate work products, scratch files.
|
|
41
|
+
|
|
42
|
+
### Repository (read-only)
|
|
43
|
+
```typescript
|
|
44
|
+
{ name: "repo", path: "/repos/platform", writable: false }
|
|
45
|
+
```
|
|
46
|
+
External code repository mounted for review or analysis. Read-only to prevent accidental modification.
|
|
47
|
+
|
|
48
|
+
### Output (writable, not deletable)
|
|
49
|
+
```typescript
|
|
50
|
+
{ name: "output", path: "/shared/reports", writable: true, deletable: false }
|
|
51
|
+
```
|
|
52
|
+
Shared directory for publishing reports. The agent can create and update files but cannot delete published output.
|
|
53
|
+
|
|
54
|
+
## Size limits
|
|
55
|
+
|
|
56
|
+
| Limit | Default | What it protects |
|
|
57
|
+
|-------|---------|-----------------|
|
|
58
|
+
| `maxReadSize` | 256KB | Prevents large files from consuming the context window. Files over this limit are truncated with a `[truncated]` marker. |
|
|
59
|
+
| `maxWriteSize` | 1MB | Prevents the agent from writing arbitrarily large files to disk. |
|
|
60
|
+
| `fs_search` max results | 100 (configurable up to 1000) | Prevents glob expansion on huge directories from returning overwhelming results. |
|
|
61
|
+
|
|
62
|
+
## Binary file handling
|
|
63
|
+
|
|
64
|
+
The following extensions are detected as binary and rejected by `fs_read`:
|
|
65
|
+
|
|
66
|
+
**Images:** .png, .jpg, .jpeg, .gif, .bmp, .ico, .webp, .svg
|
|
67
|
+
**Documents:** .pdf
|
|
68
|
+
**Archives:** .zip, .gz, .tar, .bz2, .7z, .rar
|
|
69
|
+
**Media:** .mp3, .mp4, .avi, .mov, .wav, .flac
|
|
70
|
+
**Fonts:** .woff, .woff2, .ttf, .otf, .eot
|
|
71
|
+
**Compiled:** .exe, .dll, .so, .dylib, .o, .a, .wasm, .pyc, .class
|
|
72
|
+
|
|
73
|
+
Binary files return: `Error: Binary file (.ext, size). Use fs_list to see metadata.`
|
|
74
|
+
|
|
75
|
+
## Security boundaries
|
|
76
|
+
|
|
77
|
+
1. **Path traversal**: All paths are resolved via `fs.realpath()` (follows symlinks) and checked against the mount root using a `path.relative()`-based containment helper. Paths that escape the mount (relative result of `..` or absolute path for cross-drive) are rejected. The `relative()`-based check handles both prefix-collision siblings (`/var/data/work` does not accept `/var/data/workspace/...`) and root-level mounts (`/` on POSIX accepts nested children correctly).
|
|
78
|
+
2. **Symlink escape**: Symlinks that point outside the mount boundary are detected and rejected before the file is read, using the same containment check.
|
|
79
|
+
3. **Mount isolation**: Each mount is an independent security boundary. No cross-mount path references are possible.
|
|
80
|
+
4. **No absolute paths**: The agent always uses logical paths (`mount-name/...`). Physical paths are never exposed.
|
|
81
|
+
5. **Trust-level tool gating**: Mutation tools (`fs_write`, `fs_mkdir`, `fs_remove`) are structurally hidden from untrusted peers by the kernel's capability table before the model sees them. The authenticated level loses `fs_remove`. Operator and facility peers see all tools. This runs at tool-selection time; mount-level `writable` / `deletable` flags are a complementary check that runs inside the tool.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Transcript } from "../../../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-peer in-memory buffer used by the `session-end-only` extraction
|
|
5
|
+
* mode (Decision 3 of the memorist design). When the frequency
|
|
6
|
+
* dispatcher returns "buffer" for a turn, the auto-save handler
|
|
7
|
+
* appends that turn's `Transcript` here. At a configured boundary —
|
|
8
|
+
* session end, idle threshold, or operator-triggered flush — the
|
|
9
|
+
* augment calls `flush(peerId)` and runs extraction on the accumulated
|
|
10
|
+
* snapshot.
|
|
11
|
+
*
|
|
12
|
+
* Process-local: a restart drops all buffered transcripts. That's the
|
|
13
|
+
* intended trade-off — buffered visitor traffic is operationally
|
|
14
|
+
* low-stakes (anonymous public peers), and persistence would require
|
|
15
|
+
* its own retention/compaction story disproportionate to the value.
|
|
16
|
+
*/
|
|
17
|
+
export interface ExtractionBuffer {
|
|
18
|
+
/** Append a completed turn's transcript to the peer's buffer. */
|
|
19
|
+
append(peerId: string, transcript: Transcript): void;
|
|
20
|
+
/**
|
|
21
|
+
* Drain and return the peer's buffered transcripts. Subsequent
|
|
22
|
+
* `peek` returns an empty array until the next `append`.
|
|
23
|
+
*/
|
|
24
|
+
flush(peerId: string): Transcript[];
|
|
25
|
+
/** Read-only view of currently buffered transcripts for a peer. */
|
|
26
|
+
peek(peerId: string): readonly Transcript[];
|
|
27
|
+
/** Drop the peer's buffer without returning it (e.g. on `forget`). */
|
|
28
|
+
clear(peerId: string): void;
|
|
29
|
+
/** Number of distinct peers with at least one buffered transcript. */
|
|
30
|
+
size(): number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createBuffer(): ExtractionBuffer {
|
|
34
|
+
const store = new Map<string, Transcript[]>();
|
|
35
|
+
return {
|
|
36
|
+
append(peerId, transcript) {
|
|
37
|
+
const list = store.get(peerId) ?? [];
|
|
38
|
+
list.push(transcript);
|
|
39
|
+
store.set(peerId, list);
|
|
40
|
+
},
|
|
41
|
+
flush(peerId) {
|
|
42
|
+
const list = store.get(peerId) ?? [];
|
|
43
|
+
store.delete(peerId);
|
|
44
|
+
return list;
|
|
45
|
+
},
|
|
46
|
+
peek(peerId) {
|
|
47
|
+
return store.get(peerId) ?? [];
|
|
48
|
+
},
|
|
49
|
+
clear(peerId) {
|
|
50
|
+
store.delete(peerId);
|
|
51
|
+
},
|
|
52
|
+
size() {
|
|
53
|
+
return store.size;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { TrustLevel } from "../../../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-trust-level extraction frequency knobs (Decision 3 of the memorist
|
|
5
|
+
* design). Operators choose how aggressively auto-save extracts per cohort:
|
|
6
|
+
*
|
|
7
|
+
* - "every-turn": extract after every completed turn
|
|
8
|
+
* - "every-N-turns": extract after turns where turnIndex % N === 0
|
|
9
|
+
* - "session-end-only": buffer transcripts; flush at session boundary
|
|
10
|
+
* - "never": skip extraction entirely for this cohort
|
|
11
|
+
*/
|
|
12
|
+
export type ExtractionFrequency = "every-turn" | "every-N-turns" | "session-end-only" | "never";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Outcome of the frequency check for a single completed turn:
|
|
16
|
+
*
|
|
17
|
+
* - "extract": run extraction now (immediate write path)
|
|
18
|
+
* - "buffer": append the transcript to the per-peer buffer (deferred)
|
|
19
|
+
* - "skip": do nothing for this turn
|
|
20
|
+
*/
|
|
21
|
+
export type ExtractionDecision = "extract" | "buffer" | "skip";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Nested configuration shape consumed by the dispatcher. `public` splits
|
|
25
|
+
* into `recognized` (visitor-token holders) and `anonymous` (no token /
|
|
26
|
+
* fresh visitor) so operators can be more aggressive with returning
|
|
27
|
+
* recognized visitors than with first-touch traffic.
|
|
28
|
+
*/
|
|
29
|
+
export interface ExtractionFrequencyConfig {
|
|
30
|
+
creator?: ExtractionFrequency;
|
|
31
|
+
agent?: ExtractionFrequency;
|
|
32
|
+
public?: {
|
|
33
|
+
recognized?: ExtractionFrequency;
|
|
34
|
+
anonymous?: ExtractionFrequency;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Minimal peer descriptor used by the dispatcher. The full PeerIdentity
|
|
40
|
+
* carries more fields, but only trust + public substate matter for
|
|
41
|
+
* frequency selection.
|
|
42
|
+
*/
|
|
43
|
+
export interface PeerInput {
|
|
44
|
+
trustLevel: TrustLevel;
|
|
45
|
+
publicSubstate?: "recognized" | "anonymous";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pure dispatch: given a peer, the current turn index for that peer, and
|
|
50
|
+
* the operator's frequency config, return whether to extract immediately,
|
|
51
|
+
* buffer, or skip. No side effects, no I/O.
|
|
52
|
+
*/
|
|
53
|
+
export function shouldExtract(
|
|
54
|
+
peer: PeerInput,
|
|
55
|
+
turnIndex: number,
|
|
56
|
+
config: ExtractionFrequencyConfig,
|
|
57
|
+
everyNTurns: number,
|
|
58
|
+
): ExtractionDecision {
|
|
59
|
+
const freq = resolveFrequency(peer, config);
|
|
60
|
+
if (freq === "never") return "skip";
|
|
61
|
+
if (freq === "every-turn") return "extract";
|
|
62
|
+
if (freq === "session-end-only") return "buffer";
|
|
63
|
+
if (freq === "every-N-turns") return turnIndex % everyNTurns === 0 ? "extract" : "skip";
|
|
64
|
+
return "skip";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveFrequency(peer: PeerInput, config: ExtractionFrequencyConfig): ExtractionFrequency {
|
|
68
|
+
if (peer.trustLevel === "creator") return config.creator ?? "every-turn";
|
|
69
|
+
if (peer.trustLevel === "agent") return config.agent ?? "every-N-turns";
|
|
70
|
+
if (peer.trustLevel === "public") {
|
|
71
|
+
const sub = peer.publicSubstate ?? "anonymous";
|
|
72
|
+
return config.public?.[sub] ?? (sub === "recognized" ? "every-turn" : "session-end-only");
|
|
73
|
+
}
|
|
74
|
+
// Defensive — unknown trust levels (shouldn't happen given the union)
|
|
75
|
+
// skip extraction rather than fall through to a default that might
|
|
76
|
+
// accidentally write under an unintended peer cohort.
|
|
77
|
+
console.warn(`[layered-memory] unknown trust level: ${peer.trustLevel}; skipping extraction`);
|
|
78
|
+
return "never";
|
|
79
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Transcript } from "../../../types";
|
|
2
|
+
import { type ExtractedFact, parseExtractionResponse } from "./parse";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal extraction-engine surface. The auto-save handler does NOT need
|
|
6
|
+
* the full `ModelClient` shape (assembled prompts, tool definitions, token
|
|
7
|
+
* counters). It needs a single string-in / string-out completion paired
|
|
8
|
+
* with a USD cost so callers can roll the spend into budgets.
|
|
9
|
+
*
|
|
10
|
+
* Decoupling here lets the augment swap in either:
|
|
11
|
+
* - a thin adapter wrapping the agent's primary engine (Phase 2c+), or
|
|
12
|
+
* - a dedicated cheaper extraction model (per Decision 6 of the
|
|
13
|
+
* memorist design — extraction can ride a Haiku-priced engine while
|
|
14
|
+
* the user-facing agent runs Sonnet).
|
|
15
|
+
*
|
|
16
|
+
* The shape is intentionally narrow: no streaming, no tool calls. The
|
|
17
|
+
* extraction LLM emits a JSON array; the handler parses it via parse.ts.
|
|
18
|
+
*/
|
|
19
|
+
export interface ExtractionEngine {
|
|
20
|
+
complete(prompt: string): Promise<{ text: string; costUsd: number }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ExtractionInput {
|
|
24
|
+
transcript: Transcript;
|
|
25
|
+
engine: ExtractionEngine;
|
|
26
|
+
/**
|
|
27
|
+
* Prompt template containing the literal token `{{TRANSCRIPT}}` which
|
|
28
|
+
* the handler replaces with a rendered transcript string. Operators
|
|
29
|
+
* may override the bundled `prompt.md` via
|
|
30
|
+
* `layeredMemory.options.autoSave.promptTemplate`.
|
|
31
|
+
*/
|
|
32
|
+
promptTemplate: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Outcome of one extraction call. `costUsd` always carries the engine's
|
|
37
|
+
* reported spend even on failure, so the augment can attribute it to the
|
|
38
|
+
* originating turn's budget when integration ships in Phase 2c.
|
|
39
|
+
*
|
|
40
|
+
* Engine-call failures (network, rate limit) report `costUsd: 0` because
|
|
41
|
+
* no completion happened; parse failures keep the engine's costUsd
|
|
42
|
+
* because the model already billed for the (malformed) response.
|
|
43
|
+
*/
|
|
44
|
+
export type ExtractionResult =
|
|
45
|
+
| { success: true; facts: ExtractedFact[]; costUsd: number }
|
|
46
|
+
| { success: false; error: string; costUsd: number };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run a single extraction turn: render prompt → call engine → parse JSON.
|
|
50
|
+
*
|
|
51
|
+
* Never throws. Engine errors and parse errors map to
|
|
52
|
+
* `{ success: false, error, costUsd }` so the calling
|
|
53
|
+
* `scheduleAfterTurn` hook can log and skip without poisoning the
|
|
54
|
+
* background-work path. Per ADR-027, extraction failures are explicitly
|
|
55
|
+
* best-effort — they never affect the user-facing turn.
|
|
56
|
+
*/
|
|
57
|
+
export async function handleExtractionTurn(input: ExtractionInput): Promise<ExtractionResult> {
|
|
58
|
+
const transcriptText = renderTranscript(input.transcript);
|
|
59
|
+
const prompt = input.promptTemplate.replace("{{TRANSCRIPT}}", transcriptText);
|
|
60
|
+
|
|
61
|
+
let response: { text: string; costUsd: number };
|
|
62
|
+
try {
|
|
63
|
+
response = await input.engine.complete(prompt);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: (err as Error).message,
|
|
68
|
+
costUsd: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const parsed = parseExtractionResponse(response.text);
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: parsed.error,
|
|
77
|
+
costUsd: response.costUsd,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
facts: parsed.facts,
|
|
84
|
+
costUsd: response.costUsd,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Flatten the transcript into a plain-text rendering suitable for the
|
|
90
|
+
* extraction prompt. Currently only emits text parts — file/data parts
|
|
91
|
+
* carry binary or structured payloads the extraction model can't reason
|
|
92
|
+
* about uniformly. Keeping this minimal also avoids accidentally leaking
|
|
93
|
+
* tool-call internals into the prompt body.
|
|
94
|
+
*/
|
|
95
|
+
function renderTranscript(t: Transcript): string {
|
|
96
|
+
const lines: string[] = [];
|
|
97
|
+
for (const part of t.parts) {
|
|
98
|
+
if (part.kind === "text") {
|
|
99
|
+
lines.push(part.text);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed shape of a single extracted fact. Mirrors the JSON schema the
|
|
3
|
+
* extraction prompt instructs the model to emit. The parser strips any
|
|
4
|
+
* fields not in this list — forward compatibility for prompt revisions
|
|
5
|
+
* that introduce new fields without coordinating with downstream code.
|
|
6
|
+
*/
|
|
7
|
+
export interface ExtractedFact {
|
|
8
|
+
subject: string;
|
|
9
|
+
predicate: string;
|
|
10
|
+
object: string;
|
|
11
|
+
confidence: number;
|
|
12
|
+
isVerbatim: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ParseResult =
|
|
16
|
+
| { success: true; facts: ExtractedFact[] }
|
|
17
|
+
| { success: false; error: string };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Defensive JSON parser for the extraction LLM's response. The model
|
|
21
|
+
* should emit a top-level JSON array of fact objects per `prompt.md`,
|
|
22
|
+
* but real models occasionally drift (extra prose, missing fields,
|
|
23
|
+
* type mismatches). This parser:
|
|
24
|
+
*
|
|
25
|
+
* - Returns `{ success: false, error }` on any failure mode rather
|
|
26
|
+
* than throwing, so the auto-save handler can log and skip without
|
|
27
|
+
* killing the injected turn's tool-call execution.
|
|
28
|
+
* - Validates each entry's shape strictly: all five required fields
|
|
29
|
+
* must be present and the right primitive type. One bad entry
|
|
30
|
+
* fails the whole batch — partial writes would leave inconsistent
|
|
31
|
+
* storage, and the cost of a re-extraction is bounded.
|
|
32
|
+
* - Strips unknown keys to keep the storage schema clean across
|
|
33
|
+
* prompt-template revisions.
|
|
34
|
+
*/
|
|
35
|
+
export function parseExtractionResponse(raw: string): ParseResult {
|
|
36
|
+
let parsed: unknown;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
return { success: false, error: `failed to parse JSON: ${(err as Error).message}` };
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(parsed)) {
|
|
43
|
+
return { success: false, error: "extraction output is not a JSON array" };
|
|
44
|
+
}
|
|
45
|
+
const facts: ExtractedFact[] = [];
|
|
46
|
+
for (const [i, item] of parsed.entries()) {
|
|
47
|
+
if (item === null || typeof item !== "object") {
|
|
48
|
+
return { success: false, error: `entry ${i} is not an object` };
|
|
49
|
+
}
|
|
50
|
+
const e = item as Record<string, unknown>;
|
|
51
|
+
if (typeof e.subject !== "string") {
|
|
52
|
+
return { success: false, error: `entry ${i} missing/invalid subject` };
|
|
53
|
+
}
|
|
54
|
+
if (typeof e.predicate !== "string") {
|
|
55
|
+
return { success: false, error: `entry ${i} missing/invalid predicate` };
|
|
56
|
+
}
|
|
57
|
+
if (typeof e.object !== "string") {
|
|
58
|
+
return { success: false, error: `entry ${i} missing/invalid object` };
|
|
59
|
+
}
|
|
60
|
+
if (typeof e.confidence !== "number") {
|
|
61
|
+
return { success: false, error: `entry ${i} missing/invalid confidence` };
|
|
62
|
+
}
|
|
63
|
+
if (typeof e.isVerbatim !== "boolean") {
|
|
64
|
+
return { success: false, error: `entry ${i} missing/invalid isVerbatim` };
|
|
65
|
+
}
|
|
66
|
+
facts.push({
|
|
67
|
+
subject: e.subject,
|
|
68
|
+
predicate: e.predicate,
|
|
69
|
+
object: e.object,
|
|
70
|
+
confidence: e.confidence,
|
|
71
|
+
isVerbatim: e.isVerbatim,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { success: true, facts };
|
|
75
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
You are a memory extractor. Given a conversation transcript between an agent and a peer, identify durable facts about THE PEER (not about the agent or other entities) that would be useful in future conversations.
|
|
2
|
+
|
|
3
|
+
# Transcript
|
|
4
|
+
{{TRANSCRIPT}}
|
|
5
|
+
|
|
6
|
+
# Output
|
|
7
|
+
|
|
8
|
+
Return a JSON array of fact objects. Each fact has these REQUIRED fields:
|
|
9
|
+
- subject: typically "peer", may be more specific
|
|
10
|
+
- predicate: a short verb-phrase (e.g. "name", "prefers", "works_at", "team", "asked_to_remember")
|
|
11
|
+
- object: the value
|
|
12
|
+
- confidence: a number 0-1, your confidence the fact is durable + accurate
|
|
13
|
+
- isVerbatim: true ONLY if the peer's exact phrasing matters and is captured exactly; otherwise false
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
[
|
|
17
|
+
{"subject": "peer", "predicate": "name", "object": "Sam", "confidence": 0.95, "isVerbatim": true},
|
|
18
|
+
{"subject": "peer", "predicate": "prefers", "object": "dark mode", "confidence": 0.8, "isVerbatim": false}
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# Rules
|
|
22
|
+
|
|
23
|
+
- Extract durable facts only — preferences, names, commitments, recurring topics. Skip transient ("today I'm tired"), agent-side facts ("the agent said hi"), or third-party gossip.
|
|
24
|
+
- DO NOT extract secrets, API keys, passwords, or anything the peer explicitly marked confidential. Skip credentials and sensitive PII entirely.
|
|
25
|
+
- If the conversation has nothing extractable, return [].
|
|
26
|
+
- Output ONLY the JSON array. No prose, no markdown, no explanation.
|