maestro-agent-sdk 0.1.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/LICENSE +21 -0
- package/NOTICE +24 -0
- package/README.md +133 -0
- package/dist/agents/contracts.d.ts +49 -0
- package/dist/agents/contracts.d.ts.map +1 -0
- package/dist/agents/contracts.js +2 -0
- package/dist/agents/contracts.js.map +1 -0
- package/dist/agents/rollout/shared.d.ts +24 -0
- package/dist/agents/rollout/shared.d.ts.map +1 -0
- package/dist/agents/rollout/shared.js +105 -0
- package/dist/agents/rollout/shared.js.map +1 -0
- package/dist/core/agent.d.ts +71 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +22 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/loop.d.ts +26 -0
- package/dist/core/loop.d.ts.map +1 -0
- package/dist/core/loop.js +317 -0
- package/dist/core/loop.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +79 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +176 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/pool-cache.d.ts +103 -0
- package/dist/mcp/pool-cache.d.ts.map +1 -0
- package/dist/mcp/pool-cache.js +249 -0
- package/dist/mcp/pool-cache.js.map +1 -0
- package/dist/mcp/pool.d.ts +65 -0
- package/dist/mcp/pool.d.ts.map +1 -0
- package/dist/mcp/pool.js +86 -0
- package/dist/mcp/pool.js.map +1 -0
- package/dist/media/file-events.d.ts +8 -0
- package/dist/media/file-events.d.ts.map +1 -0
- package/dist/media/file-events.js +15 -0
- package/dist/media/file-events.js.map +1 -0
- package/dist/memory/active-task-template.d.ts +34 -0
- package/dist/memory/active-task-template.d.ts.map +1 -0
- package/dist/memory/active-task-template.js +63 -0
- package/dist/memory/active-task-template.js.map +1 -0
- package/dist/memory/compressor.d.ts +87 -0
- package/dist/memory/compressor.d.ts.map +1 -0
- package/dist/memory/compressor.js +164 -0
- package/dist/memory/compressor.js.map +1 -0
- package/dist/memory/hash.d.ts +17 -0
- package/dist/memory/hash.d.ts.map +1 -0
- package/dist/memory/hash.js +20 -0
- package/dist/memory/hash.js.map +1 -0
- package/dist/memory/prune.d.ts +117 -0
- package/dist/memory/prune.d.ts.map +1 -0
- package/dist/memory/prune.js +416 -0
- package/dist/memory/prune.js.map +1 -0
- package/dist/memory/reminder.d.ts +57 -0
- package/dist/memory/reminder.d.ts.map +1 -0
- package/dist/memory/reminder.js +57 -0
- package/dist/memory/reminder.js.map +1 -0
- package/dist/memory/scrubber.d.ts +28 -0
- package/dist/memory/scrubber.d.ts.map +1 -0
- package/dist/memory/scrubber.js +147 -0
- package/dist/memory/scrubber.js.map +1 -0
- package/dist/memory/token-estimate.d.ts +10 -0
- package/dist/memory/token-estimate.d.ts.map +1 -0
- package/dist/memory/token-estimate.js +69 -0
- package/dist/memory/token-estimate.js.map +1 -0
- package/dist/platform/config.d.ts +12 -0
- package/dist/platform/config.d.ts.map +1 -0
- package/dist/platform/config.js +54 -0
- package/dist/platform/config.js.map +1 -0
- package/dist/platform/jsonl.d.ts +15 -0
- package/dist/platform/jsonl.d.ts.map +1 -0
- package/dist/platform/jsonl.js +80 -0
- package/dist/platform/jsonl.js.map +1 -0
- package/dist/platform/lifecycle.d.ts +22 -0
- package/dist/platform/lifecycle.d.ts.map +1 -0
- package/dist/platform/lifecycle.js +60 -0
- package/dist/platform/lifecycle.js.map +1 -0
- package/dist/platform/logger.d.ts +26 -0
- package/dist/platform/logger.d.ts.map +1 -0
- package/dist/platform/logger.js +41 -0
- package/dist/platform/logger.js.map +1 -0
- package/dist/platform/mcp-config.d.ts +15 -0
- package/dist/platform/mcp-config.d.ts.map +1 -0
- package/dist/platform/mcp-config.js +8 -0
- package/dist/platform/mcp-config.js.map +1 -0
- package/dist/provider.d.ts +81 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +444 -0
- package/dist/provider.js.map +1 -0
- package/dist/providers/anthropic.d.ts +132 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +518 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/base.d.ts +140 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +2 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/deepseek.d.ts +118 -0
- package/dist/providers/deepseek.d.ts.map +1 -0
- package/dist/providers/deepseek.js +467 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/registry.d.ts +3 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +94 -0
- package/dist/registry.js.map +1 -0
- package/dist/session-store.d.ts +133 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +277 -0
- package/dist/session-store.js.map +1 -0
- package/dist/skills/curator.d.ts +104 -0
- package/dist/skills/curator.d.ts.map +1 -0
- package/dist/skills/curator.js +162 -0
- package/dist/skills/curator.js.map +1 -0
- package/dist/skills/index-builder.d.ts +42 -0
- package/dist/skills/index-builder.d.ts.map +1 -0
- package/dist/skills/index-builder.js +94 -0
- package/dist/skills/index-builder.js.map +1 -0
- package/dist/skills/loader.d.ts +107 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +286 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/preprocess.d.ts +45 -0
- package/dist/skills/preprocess.d.ts.map +1 -0
- package/dist/skills/preprocess.js +126 -0
- package/dist/skills/preprocess.js.map +1 -0
- package/dist/skills/usage.d.ts +75 -0
- package/dist/skills/usage.d.ts.map +1 -0
- package/dist/skills/usage.js +147 -0
- package/dist/skills/usage.js.map +1 -0
- package/dist/state/todos.d.ts +95 -0
- package/dist/state/todos.d.ts.map +1 -0
- package/dist/state/todos.js +198 -0
- package/dist/state/todos.js.map +1 -0
- package/dist/storage/conversations.d.ts +28 -0
- package/dist/storage/conversations.d.ts.map +1 -0
- package/dist/storage/conversations.js +8 -0
- package/dist/storage/conversations.js.map +1 -0
- package/dist/sub-agent/runner.d.ts +78 -0
- package/dist/sub-agent/runner.d.ts.map +1 -0
- package/dist/sub-agent/runner.js +215 -0
- package/dist/sub-agent/runner.js.map +1 -0
- package/dist/tools/builtin/agent.d.ts +33 -0
- package/dist/tools/builtin/agent.d.ts.map +1 -0
- package/dist/tools/builtin/agent.js +76 -0
- package/dist/tools/builtin/agent.js.map +1 -0
- package/dist/tools/builtin/bash.d.ts +11 -0
- package/dist/tools/builtin/bash.d.ts.map +1 -0
- package/dist/tools/builtin/bash.js +91 -0
- package/dist/tools/builtin/bash.js.map +1 -0
- package/dist/tools/builtin/edit.d.ts +21 -0
- package/dist/tools/builtin/edit.d.ts.map +1 -0
- package/dist/tools/builtin/edit.js +238 -0
- package/dist/tools/builtin/edit.js.map +1 -0
- package/dist/tools/builtin/read.d.ts +17 -0
- package/dist/tools/builtin/read.d.ts.map +1 -0
- package/dist/tools/builtin/read.js +139 -0
- package/dist/tools/builtin/read.js.map +1 -0
- package/dist/tools/builtin/sandbox.d.ts +16 -0
- package/dist/tools/builtin/sandbox.d.ts.map +1 -0
- package/dist/tools/builtin/sandbox.js +58 -0
- package/dist/tools/builtin/sandbox.js.map +1 -0
- package/dist/tools/builtin/skill_view.d.ts +37 -0
- package/dist/tools/builtin/skill_view.d.ts.map +1 -0
- package/dist/tools/builtin/skill_view.js +82 -0
- package/dist/tools/builtin/skill_view.js.map +1 -0
- package/dist/tools/builtin/todo_write.d.ts +29 -0
- package/dist/tools/builtin/todo_write.d.ts.map +1 -0
- package/dist/tools/builtin/todo_write.js +96 -0
- package/dist/tools/builtin/todo_write.js.map +1 -0
- package/dist/tools/builtin/web_fetch.d.ts +10 -0
- package/dist/tools/builtin/web_fetch.d.ts.map +1 -0
- package/dist/tools/builtin/web_fetch.js +150 -0
- package/dist/tools/builtin/web_fetch.js.map +1 -0
- package/dist/tools/builtin/write.d.ts +35 -0
- package/dist/tools/builtin/write.d.ts.map +1 -0
- package/dist/tools/builtin/write.js +70 -0
- package/dist/tools/builtin/write.js.map +1 -0
- package/dist/tools/file-state.d.ts +99 -0
- package/dist/tools/file-state.d.ts.map +1 -0
- package/dist/tools/file-state.js +133 -0
- package/dist/tools/file-state.js.map +1 -0
- package/dist/tools/hooks/sandbox-fs.d.ts +25 -0
- package/dist/tools/hooks/sandbox-fs.d.ts.map +1 -0
- package/dist/tools/hooks/sandbox-fs.js +48 -0
- package/dist/tools/hooks/sandbox-fs.js.map +1 -0
- package/dist/tools/registry.d.ts +102 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +93 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro session store + rollout encoder.
|
|
3
|
+
*
|
|
4
|
+
* Unlike claude/codex (which delegate persistence to vendor SDKs whose
|
|
5
|
+
* rollouts live at `~/.claude/projects/` and `~/.codex/sessions/`), Maestro
|
|
6
|
+
* is a pure in-process loop that owns its own conversation history. We
|
|
7
|
+
* persist it ourselves at `~/.maestro/sessions/<sessionId>.jsonl`, one
|
|
8
|
+
* ProviderMessage per line, so a follow-up `maestroProvider` call with the
|
|
9
|
+
* same `opts.sessionId` can rebuild the `messages` array and continue
|
|
10
|
+
* exactly where the previous turn left off — same contract as the SDK-backed
|
|
11
|
+
* providers, just our own storage layer.
|
|
12
|
+
*
|
|
13
|
+
* The JSONL is round-tripped verbatim: tool_use / tool_result content blocks
|
|
14
|
+
* survive a save→load cycle so multi-turn tool histories don't degrade into
|
|
15
|
+
* `[Tool: ...]` text annotations (those only happen on cross-agent rollouts,
|
|
16
|
+
* which by definition came from another provider's log).
|
|
17
|
+
*
|
|
18
|
+
* Two writer paths share the same file format:
|
|
19
|
+
* - `saveMaestroSession` — verbatim dump from the live loop.
|
|
20
|
+
* - `writeMaestroRollout` — synthesized from a provider-agnostic
|
|
21
|
+
* `ConversationEntry` log (used by `set_agent` cross-agent bridging and
|
|
22
|
+
* by `forkSession`). Pairs are flattened to `{role, content: text}`.
|
|
23
|
+
*/
|
|
24
|
+
import { type ChatPair } from "./agents/rollout/shared.js";
|
|
25
|
+
import type { ProviderMessage } from "./providers/base.js";
|
|
26
|
+
import type { ConversationEntry } from "./storage/conversations.js";
|
|
27
|
+
/** Root directory for maestro session/rollout files. */
|
|
28
|
+
export declare function maestroSessionsDir(): string;
|
|
29
|
+
/** Absolute path of the JSONL backing a given sessionId. */
|
|
30
|
+
export declare function maestroSessionPath(sessionId: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Load the persisted ProviderMessage[] for a sessionId, or `null` if no
|
|
33
|
+
* file exists (i.e. this is a fresh session). Malformed lines are skipped
|
|
34
|
+
* with a warning rather than crashing the loop — a corrupt entry in the
|
|
35
|
+
* middle of history is better than losing a working session entirely.
|
|
36
|
+
*/
|
|
37
|
+
export declare function loadMaestroSession(sessionId: string): ProviderMessage[] | null;
|
|
38
|
+
/**
|
|
39
|
+
* Overwrite (or create) the persisted session file with the provided
|
|
40
|
+
* messages. Each turn's final state is written atomically — partial-history
|
|
41
|
+
* writes can leave the next resume looking at a stale prefix.
|
|
42
|
+
*/
|
|
43
|
+
export declare function saveMaestroSession(sessionId: string, messages: ProviderMessage[]): void;
|
|
44
|
+
/**
|
|
45
|
+
* Remove a session's backing file and drop its in-memory file-state tracker.
|
|
46
|
+
* ENOENT on the unlink is silently ignored. Tracker drop is unconditional —
|
|
47
|
+
* no-op when the session never registered a tracker.
|
|
48
|
+
*/
|
|
49
|
+
export declare function deleteMaestroSession(sessionId: string): void;
|
|
50
|
+
export interface MaestroRolloutOptions {
|
|
51
|
+
/** Working directory the resumed Maestro session will report. Validated
|
|
52
|
+
* against the workspace roots so callers can't smuggle arbitrary cwds. */
|
|
53
|
+
cwd: string;
|
|
54
|
+
/** Optional override; default = freshly generated UUIDv4. */
|
|
55
|
+
sessionId?: string;
|
|
56
|
+
/** Pairs to encode. If omitted, derived from `entries` via extractChatPairs. */
|
|
57
|
+
pairs?: ChatPair[];
|
|
58
|
+
/** When `pairs` is omitted, the source UnifiedEvent log to digest. */
|
|
59
|
+
entries?: ConversationEntry[];
|
|
60
|
+
}
|
|
61
|
+
export interface MaestroRolloutResult {
|
|
62
|
+
sessionId: string;
|
|
63
|
+
rolloutPath: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Materialize a Maestro session JSONL from the provided dialogue and place it
|
|
67
|
+
* at `~/.maestro/sessions/<sessionId>.jsonl`. A subsequent `maestroProvider`
|
|
68
|
+
* call with `opts.sessionId === sessionId` will load this file and treat it
|
|
69
|
+
* as conversation history — same path the live loop persists to.
|
|
70
|
+
*
|
|
71
|
+
* Used by `set_agent` cross-agent bridging (X → maestro) and by
|
|
72
|
+
* `maestroRegistry.forkSession`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function writeMaestroRollout(opts: MaestroRolloutOptions): MaestroRolloutResult;
|
|
75
|
+
/**
|
|
76
|
+
* Validate a message read from disk has the minimum shape Maestro needs.
|
|
77
|
+
* Used by the loader to drop entries that would crash the provider call.
|
|
78
|
+
* Returns the message typed if valid, or null if it should be dropped.
|
|
79
|
+
*
|
|
80
|
+
* Kept conservative: role must be "user" or "assistant", content must be
|
|
81
|
+
* a non-empty string or a content-block array. Anything else came from a
|
|
82
|
+
* different schema version (or a corrupt write) and is safer to drop than
|
|
83
|
+
* to feed back to the model.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isWellFormedMessage(value: unknown): value is ProviderMessage;
|
|
86
|
+
/**
|
|
87
|
+
* Drop trailing entries that would make the next Anthropic resume invalid.
|
|
88
|
+
*
|
|
89
|
+
* Two failure modes the live loop can leave behind when it exits without a
|
|
90
|
+
* clean drain (abort signal, provider crash, hard fetch error):
|
|
91
|
+
* - A final `user` whose content is the plain-text prompt that was never
|
|
92
|
+
* answered. Resuming would feed it to the model a second time.
|
|
93
|
+
* - A final `assistant` carrying `tool_use` blocks without a paired user
|
|
94
|
+
* turn of `tool_result` blocks. Anthropic rejects this with
|
|
95
|
+
* `400 messages.N: invalid_request_error` ("each `tool_use` block must
|
|
96
|
+
* have a corresponding `tool_result`").
|
|
97
|
+
*
|
|
98
|
+
* The trim walks back from the tail, dropping orphan turns until it lands
|
|
99
|
+
* on a consistent prefix. A user turn that already contains `tool_result`
|
|
100
|
+
* blocks is a valid stopping point (the model just hasn't been asked yet to
|
|
101
|
+
* react to those results — Anthropic treats that as a fresh starting point).
|
|
102
|
+
*
|
|
103
|
+
* Returning an empty array is fine: it means the only thing the loop pushed
|
|
104
|
+
* before failing was the new user prompt, and the next resume should treat
|
|
105
|
+
* the session as starting from the previously persisted state.
|
|
106
|
+
*/
|
|
107
|
+
export declare function trimToSafePrefix(messages: ProviderMessage[]): ProviderMessage[];
|
|
108
|
+
/**
|
|
109
|
+
* Sweep `~/.maestro/sessions/` and unlink JSONL files whose mtime is older
|
|
110
|
+
* than `maxAgeMs` (default 30 days).
|
|
111
|
+
*
|
|
112
|
+
* Why a TTL instead of `purgeTopicLogs`-style targeted cleanup: a maestro
|
|
113
|
+
* session is identified only by UUID, and that UUID can be referenced from
|
|
114
|
+
* multiple places (conversation log, fork rollouts, set_agent bridges).
|
|
115
|
+
* A definitive "is this session still in use?" check would require
|
|
116
|
+
* cross-referencing every user's conversation manifest on disk on every
|
|
117
|
+
* cleanup pass — expensive and brittle. Instead we rely on the simple
|
|
118
|
+
* "any active session writes its file every turn, so mtime tracks use"
|
|
119
|
+
* invariant. Sessions a user hasn't touched for a month are forgotten
|
|
120
|
+
* either way; deleting the file just reclaims disk and avoids a slow
|
|
121
|
+
* directory if the user accumulates thousands of UUIDs over time.
|
|
122
|
+
*
|
|
123
|
+
* Safe on first boot (directory missing → returns 0). Per-file unlink
|
|
124
|
+
* errors are logged and skipped rather than abort the sweep — one
|
|
125
|
+
* permission glitch shouldn't block the rest.
|
|
126
|
+
*
|
|
127
|
+
* Returns { scanned, removed } so the caller can log the result.
|
|
128
|
+
*/
|
|
129
|
+
export declare function cleanupStaleMaestroSessions(maxAgeMs?: number): {
|
|
130
|
+
scanned: number;
|
|
131
|
+
removed: number;
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,OAAO,EAEL,KAAK,QAAQ,EAGd,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAwB,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAK9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,wDAAwD;AACxD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,4DAA4D;AAC5D,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,GAAG,IAAI,CAa9E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,IAAI,CAGvF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAe5D;AAqBD,MAAM,WAAW,qBAAqB;IACpC;+EAC2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IACnB,sEAAsE;IACtE,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,qBAAqB,GAAG,oBAAoB,CAarF;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAS5E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAgC/E;AAKD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,GAAE,MAAuC,GAAG;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAgDA"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro session store + rollout encoder.
|
|
3
|
+
*
|
|
4
|
+
* Unlike claude/codex (which delegate persistence to vendor SDKs whose
|
|
5
|
+
* rollouts live at `~/.claude/projects/` and `~/.codex/sessions/`), Maestro
|
|
6
|
+
* is a pure in-process loop that owns its own conversation history. We
|
|
7
|
+
* persist it ourselves at `~/.maestro/sessions/<sessionId>.jsonl`, one
|
|
8
|
+
* ProviderMessage per line, so a follow-up `maestroProvider` call with the
|
|
9
|
+
* same `opts.sessionId` can rebuild the `messages` array and continue
|
|
10
|
+
* exactly where the previous turn left off — same contract as the SDK-backed
|
|
11
|
+
* providers, just our own storage layer.
|
|
12
|
+
*
|
|
13
|
+
* The JSONL is round-tripped verbatim: tool_use / tool_result content blocks
|
|
14
|
+
* survive a save→load cycle so multi-turn tool histories don't degrade into
|
|
15
|
+
* `[Tool: ...]` text annotations (those only happen on cross-agent rollouts,
|
|
16
|
+
* which by definition came from another provider's log).
|
|
17
|
+
*
|
|
18
|
+
* Two writer paths share the same file format:
|
|
19
|
+
* - `saveMaestroSession` — verbatim dump from the live loop.
|
|
20
|
+
* - `writeMaestroRollout` — synthesized from a provider-agnostic
|
|
21
|
+
* `ConversationEntry` log (used by `set_agent` cross-agent bridging and
|
|
22
|
+
* by `forkSession`). Pairs are flattened to `{role, content: text}`.
|
|
23
|
+
*/
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
25
|
+
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { assertUuidLike, ensureCwdExists, extractChatPairs, } from "./agents/rollout/shared.js";
|
|
29
|
+
import { dropTodoStore } from "./state/todos.js";
|
|
30
|
+
import { dropFileStateTracker } from "./tools/file-state.js";
|
|
31
|
+
import { parseJsonlText, writeJsonlFile } from "./platform/jsonl.js";
|
|
32
|
+
import { logger } from "./platform/logger.js";
|
|
33
|
+
/** Root directory for maestro session/rollout files. */
|
|
34
|
+
export function maestroSessionsDir() {
|
|
35
|
+
return join(homedir(), ".maestro", "sessions");
|
|
36
|
+
}
|
|
37
|
+
/** Absolute path of the JSONL backing a given sessionId. */
|
|
38
|
+
export function maestroSessionPath(sessionId) {
|
|
39
|
+
return join(maestroSessionsDir(), `${sessionId}.jsonl`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Load the persisted ProviderMessage[] for a sessionId, or `null` if no
|
|
43
|
+
* file exists (i.e. this is a fresh session). Malformed lines are skipped
|
|
44
|
+
* with a warning rather than crashing the loop — a corrupt entry in the
|
|
45
|
+
* middle of history is better than losing a working session entirely.
|
|
46
|
+
*/
|
|
47
|
+
export function loadMaestroSession(sessionId) {
|
|
48
|
+
const path = maestroSessionPath(sessionId);
|
|
49
|
+
if (!existsSync(path))
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const raw = readFileSync(path, "utf8");
|
|
53
|
+
return parseJsonlText(raw);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
logger.warn({ err, sessionId, path }, "loadMaestroSession: read/parse failed, starting fresh session");
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Overwrite (or create) the persisted session file with the provided
|
|
62
|
+
* messages. Each turn's final state is written atomically — partial-history
|
|
63
|
+
* writes can leave the next resume looking at a stale prefix.
|
|
64
|
+
*/
|
|
65
|
+
export function saveMaestroSession(sessionId, messages) {
|
|
66
|
+
assertUuidLike("sessionId", sessionId);
|
|
67
|
+
writeJsonlFile(maestroSessionPath(sessionId), messages);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Remove a session's backing file and drop its in-memory file-state tracker.
|
|
71
|
+
* ENOENT on the unlink is silently ignored. Tracker drop is unconditional —
|
|
72
|
+
* no-op when the session never registered a tracker.
|
|
73
|
+
*/
|
|
74
|
+
export function deleteMaestroSession(sessionId) {
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(maestroSessionPath(sessionId));
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
if (e?.code !== "ENOENT") {
|
|
80
|
+
logger.warn({ err: e, sessionId }, "deleteMaestroSession: unlink failed (non-ENOENT)");
|
|
81
|
+
// Still drop the in-memory caches — caller treats the session as gone.
|
|
82
|
+
dropFileStateTracker(sessionId);
|
|
83
|
+
dropTodoStore(sessionId);
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
dropFileStateTracker(sessionId);
|
|
88
|
+
// Drops the in-memory store AND unlinks the on-disk `.todos.json` sidecar.
|
|
89
|
+
dropTodoStore(sessionId);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Flatten chat pairs (from cross-agent extractChatPairs output) into the
|
|
93
|
+
* provider message shape Maestro feeds back into `provider.complete()`.
|
|
94
|
+
*
|
|
95
|
+
* Tool annotations baked into `assistantText` by extractChatPairs remain
|
|
96
|
+
* inline — Maestro doesn't reconstruct synthetic tool_use IDs across SDKs
|
|
97
|
+
* (same trade-off as the codex rollout encoder). The model still sees what
|
|
98
|
+
* was called and what it returned, just as plain text rather than structured
|
|
99
|
+
* blocks.
|
|
100
|
+
*/
|
|
101
|
+
function pairsToMessages(pairs) {
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const pair of pairs) {
|
|
104
|
+
out.push({ role: "user", content: pair.userText });
|
|
105
|
+
out.push({ role: "assistant", content: pair.assistantText });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Materialize a Maestro session JSONL from the provided dialogue and place it
|
|
111
|
+
* at `~/.maestro/sessions/<sessionId>.jsonl`. A subsequent `maestroProvider`
|
|
112
|
+
* call with `opts.sessionId === sessionId` will load this file and treat it
|
|
113
|
+
* as conversation history — same path the live loop persists to.
|
|
114
|
+
*
|
|
115
|
+
* Used by `set_agent` cross-agent bridging (X → maestro) and by
|
|
116
|
+
* `maestroRegistry.forkSession`.
|
|
117
|
+
*/
|
|
118
|
+
export function writeMaestroRollout(opts) {
|
|
119
|
+
const sessionId = opts.sessionId ?? randomUUID();
|
|
120
|
+
assertUuidLike("sessionId", sessionId);
|
|
121
|
+
ensureCwdExists(opts.cwd);
|
|
122
|
+
const pairs = opts.pairs ?? extractChatPairs(opts.entries ?? []);
|
|
123
|
+
const messages = pairsToMessages(pairs);
|
|
124
|
+
const path = maestroSessionPath(sessionId);
|
|
125
|
+
writeJsonlFile(path, messages);
|
|
126
|
+
logger.info({ sessionId, path, pairs: pairs.length }, "writeMaestroRollout: synthetic session placed");
|
|
127
|
+
return { sessionId, rolloutPath: path };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Validate a message read from disk has the minimum shape Maestro needs.
|
|
131
|
+
* Used by the loader to drop entries that would crash the provider call.
|
|
132
|
+
* Returns the message typed if valid, or null if it should be dropped.
|
|
133
|
+
*
|
|
134
|
+
* Kept conservative: role must be "user" or "assistant", content must be
|
|
135
|
+
* a non-empty string or a content-block array. Anything else came from a
|
|
136
|
+
* different schema version (or a corrupt write) and is safer to drop than
|
|
137
|
+
* to feed back to the model.
|
|
138
|
+
*/
|
|
139
|
+
export function isWellFormedMessage(value) {
|
|
140
|
+
if (!value || typeof value !== "object")
|
|
141
|
+
return false;
|
|
142
|
+
const obj = value;
|
|
143
|
+
if (obj.role !== "user" && obj.role !== "assistant")
|
|
144
|
+
return false;
|
|
145
|
+
if (typeof obj.content === "string")
|
|
146
|
+
return true;
|
|
147
|
+
if (!Array.isArray(obj.content))
|
|
148
|
+
return false;
|
|
149
|
+
return obj.content.every((b) => b && typeof b === "object" && typeof b.type === "string");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Drop trailing entries that would make the next Anthropic resume invalid.
|
|
153
|
+
*
|
|
154
|
+
* Two failure modes the live loop can leave behind when it exits without a
|
|
155
|
+
* clean drain (abort signal, provider crash, hard fetch error):
|
|
156
|
+
* - A final `user` whose content is the plain-text prompt that was never
|
|
157
|
+
* answered. Resuming would feed it to the model a second time.
|
|
158
|
+
* - A final `assistant` carrying `tool_use` blocks without a paired user
|
|
159
|
+
* turn of `tool_result` blocks. Anthropic rejects this with
|
|
160
|
+
* `400 messages.N: invalid_request_error` ("each `tool_use` block must
|
|
161
|
+
* have a corresponding `tool_result`").
|
|
162
|
+
*
|
|
163
|
+
* The trim walks back from the tail, dropping orphan turns until it lands
|
|
164
|
+
* on a consistent prefix. A user turn that already contains `tool_result`
|
|
165
|
+
* blocks is a valid stopping point (the model just hasn't been asked yet to
|
|
166
|
+
* react to those results — Anthropic treats that as a fresh starting point).
|
|
167
|
+
*
|
|
168
|
+
* Returning an empty array is fine: it means the only thing the loop pushed
|
|
169
|
+
* before failing was the new user prompt, and the next resume should treat
|
|
170
|
+
* the session as starting from the previously persisted state.
|
|
171
|
+
*/
|
|
172
|
+
export function trimToSafePrefix(messages) {
|
|
173
|
+
let end = messages.length;
|
|
174
|
+
while (end > 0) {
|
|
175
|
+
const last = messages[end - 1];
|
|
176
|
+
// Final user with plain-text content = unanswered prompt.
|
|
177
|
+
if (last.role === "user" && typeof last.content === "string") {
|
|
178
|
+
end--;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
// Final assistant containing any tool_use block without a matching
|
|
182
|
+
// tool_result on the NEXT turn → orphan; drop it. The loop then re-
|
|
183
|
+
// evaluates whatever preceded it.
|
|
184
|
+
//
|
|
185
|
+
// A final assistant with ONLY text (no tool_use) is a valid stopping
|
|
186
|
+
// point — the model produced a final answer and no tool round is open.
|
|
187
|
+
// We correctly hit the `break` below, not the `continue` above. Only
|
|
188
|
+
// tool_use creates a dangling obligation for the next API call.
|
|
189
|
+
if (last.role === "assistant" && Array.isArray(last.content)) {
|
|
190
|
+
const hasToolUse = last.content.some((b) => b.type === "tool_use");
|
|
191
|
+
if (hasToolUse) {
|
|
192
|
+
end--;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Final user with tool_result blocks is valid — Anthropic accepts a
|
|
197
|
+
// history that ends after the tool round and lets the next API call
|
|
198
|
+
// produce the assistant turn.
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
return messages.slice(0, end);
|
|
202
|
+
}
|
|
203
|
+
/** Default retention: sessions untouched for 30 days are auto-purged. */
|
|
204
|
+
const DEFAULT_MAESTRO_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
205
|
+
/**
|
|
206
|
+
* Sweep `~/.maestro/sessions/` and unlink JSONL files whose mtime is older
|
|
207
|
+
* than `maxAgeMs` (default 30 days).
|
|
208
|
+
*
|
|
209
|
+
* Why a TTL instead of `purgeTopicLogs`-style targeted cleanup: a maestro
|
|
210
|
+
* session is identified only by UUID, and that UUID can be referenced from
|
|
211
|
+
* multiple places (conversation log, fork rollouts, set_agent bridges).
|
|
212
|
+
* A definitive "is this session still in use?" check would require
|
|
213
|
+
* cross-referencing every user's conversation manifest on disk on every
|
|
214
|
+
* cleanup pass — expensive and brittle. Instead we rely on the simple
|
|
215
|
+
* "any active session writes its file every turn, so mtime tracks use"
|
|
216
|
+
* invariant. Sessions a user hasn't touched for a month are forgotten
|
|
217
|
+
* either way; deleting the file just reclaims disk and avoids a slow
|
|
218
|
+
* directory if the user accumulates thousands of UUIDs over time.
|
|
219
|
+
*
|
|
220
|
+
* Safe on first boot (directory missing → returns 0). Per-file unlink
|
|
221
|
+
* errors are logged and skipped rather than abort the sweep — one
|
|
222
|
+
* permission glitch shouldn't block the rest.
|
|
223
|
+
*
|
|
224
|
+
* Returns { scanned, removed } so the caller can log the result.
|
|
225
|
+
*/
|
|
226
|
+
export function cleanupStaleMaestroSessions(maxAgeMs = DEFAULT_MAESTRO_SESSION_TTL_MS) {
|
|
227
|
+
const dir = maestroSessionsDir();
|
|
228
|
+
let scanned = 0;
|
|
229
|
+
let removed = 0;
|
|
230
|
+
let entries;
|
|
231
|
+
try {
|
|
232
|
+
entries = readdirSync(dir);
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
// Directory doesn't exist yet (no maestro session has ever been written)
|
|
236
|
+
// → nothing to clean.
|
|
237
|
+
if (e?.code === "ENOENT") {
|
|
238
|
+
return { scanned: 0, removed: 0 };
|
|
239
|
+
}
|
|
240
|
+
logger.warn({ err: e, dir }, "cleanupStaleMaestroSessions: readdir failed");
|
|
241
|
+
return { scanned: 0, removed: 0 };
|
|
242
|
+
}
|
|
243
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
244
|
+
for (const name of entries) {
|
|
245
|
+
if (!name.endsWith(".jsonl"))
|
|
246
|
+
continue;
|
|
247
|
+
scanned++;
|
|
248
|
+
const path = join(dir, name);
|
|
249
|
+
// saveMaestroSession writes `${sessionsDir}/${sessionId}.jsonl`, so the
|
|
250
|
+
// inverse is a suffix trim. Tracker drop happens after a successful
|
|
251
|
+
// unlink so partial-cleanup errors don't strand a tracker for a still-
|
|
252
|
+
// present file.
|
|
253
|
+
const sessionId = name.slice(0, -".jsonl".length);
|
|
254
|
+
try {
|
|
255
|
+
const stat = statSync(path);
|
|
256
|
+
if (stat.mtimeMs >= cutoff)
|
|
257
|
+
continue;
|
|
258
|
+
unlinkSync(path);
|
|
259
|
+
removed++;
|
|
260
|
+
dropFileStateTracker(sessionId);
|
|
261
|
+
// Also unlinks the on-disk `.todos.json` sidecar.
|
|
262
|
+
dropTodoStore(sessionId);
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
if (e?.code !== "ENOENT") {
|
|
266
|
+
logger.warn({ err: e, path }, "cleanupStaleMaestroSessions: per-file unlink failed (continuing)");
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// ENOENT mid-loop = raced. File gone → caches moot.
|
|
270
|
+
dropFileStateTracker(sessionId);
|
|
271
|
+
dropTodoStore(sessionId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return { scanned, removed };
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=session-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { SkillEntry } from "../skills/loader.js";
|
|
2
|
+
import { type SkillCounters } from "../skills/usage.js";
|
|
3
|
+
/**
|
|
4
|
+
* Skill Curator — assigns lifecycle states (active / stale / archived) to
|
|
5
|
+
* every loaded skill based on the usage sidecar, then filters the catalog
|
|
6
|
+
* fed to the system prompt so attention isn't shredded by 100 dormant
|
|
7
|
+
* entries.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle:
|
|
10
|
+
*
|
|
11
|
+
* - active — recently viewed OR brand-new. Surface in the index.
|
|
12
|
+
* - stale — has been viewed at least once but hasn't been touched in
|
|
13
|
+
* a long time. Still in the index, but at the bottom (and
|
|
14
|
+
* marked) so the model can recognize "this exists" without
|
|
15
|
+
* being nudged toward it.
|
|
16
|
+
* - archived — old + never viewed. Dropped from the index entirely.
|
|
17
|
+
* The skill file stays on disk; the model can still reach
|
|
18
|
+
* it via skill_view if the user explicitly names it, but
|
|
19
|
+
* it stops costing tokens on every turn.
|
|
20
|
+
*
|
|
21
|
+
* The `bundled` vs `agent-created` provenance bit (upstream's curator
|
|
22
|
+
* tracks this) is approximated by the SKILL.md's parent path: anything
|
|
23
|
+
* under the upstream snapshot (`/Users/maestrobot/__KEEP_MAESTRO_AGENT__/skills/`)
|
|
24
|
+
* is `bundled`, otherwise `agent-created`. Bundled skills are never
|
|
25
|
+
* archived — they're shipped intentionally and removing them would silently
|
|
26
|
+
* break the next user's expectation.
|
|
27
|
+
*
|
|
28
|
+
* The state file lives at `${DATA_DIR}/agents/maestro/skills/state.json`.
|
|
29
|
+
* Like the usage sidecar it's process-local + atomic-write — Clawgram is
|
|
30
|
+
* single-process and the curator runs once per turn at most.
|
|
31
|
+
*
|
|
32
|
+
* Upstream reference: `/Users/maestrobot/__KEEP_MAESTRO_AGENT__/agent/curator.py`
|
|
33
|
+
* — we ship the rule-based transitions only. The LLM-review pass (which
|
|
34
|
+
* proposes merges between near-duplicate skills) is intentionally deferred
|
|
35
|
+
* to a later phase; needs a cost budget and operator-confirmed action.
|
|
36
|
+
*/
|
|
37
|
+
export type SkillLifecycle = "active" | "stale" | "archived";
|
|
38
|
+
export interface SkillState {
|
|
39
|
+
lifecycle: SkillLifecycle;
|
|
40
|
+
/** ISO timestamp of the last transition (any direction). */
|
|
41
|
+
changedTs: string;
|
|
42
|
+
/** Provenance hint — bundled skills are protected from archival. */
|
|
43
|
+
bundled: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface SkillStateFile {
|
|
46
|
+
schemaVersion: 1;
|
|
47
|
+
states: Record<string, SkillState>;
|
|
48
|
+
}
|
|
49
|
+
/** How many days since `lastTouchedTs` before a previously-viewed skill is
|
|
50
|
+
* marked `stale`. Default 30. */
|
|
51
|
+
export declare const STALE_AFTER_DAYS = 30;
|
|
52
|
+
/** How many days since `firstSeenTs` (and never viewed) before an unused
|
|
53
|
+
* agent-created skill is archived. Default 60. Bundled skills are exempt. */
|
|
54
|
+
export declare const ARCHIVE_AFTER_DAYS = 60;
|
|
55
|
+
/** Default state file path. Overridable via `MAESTRO_SKILL_STATE_PATH`. */
|
|
56
|
+
export declare function defaultStatePath(): string;
|
|
57
|
+
/** Read the state file synchronously, or return an empty one on
|
|
58
|
+
* ENOENT / schema mismatch / parse error. Cached in-process. */
|
|
59
|
+
export declare function loadState(path?: string): SkillStateFile;
|
|
60
|
+
/**
|
|
61
|
+
* Decide the lifecycle for one skill given its counters + provenance + a
|
|
62
|
+
* reference `now`. Pure function — no I/O. Drives both the in-memory
|
|
63
|
+
* filter and the state-file persistence.
|
|
64
|
+
*
|
|
65
|
+
* Rules:
|
|
66
|
+
* - bundled + never viewed → "active" (operator shipped it for a reason)
|
|
67
|
+
* - viewCount > 0 + recent → "active"
|
|
68
|
+
* - viewCount > 0 + N days old → "stale"
|
|
69
|
+
* - agent-created + never viewed + M days old → "archived"
|
|
70
|
+
* - default (just-loaded, no record yet) → "active"
|
|
71
|
+
*/
|
|
72
|
+
export declare function decideLifecycle(counters: SkillCounters | undefined, bundled: boolean, now?: Date): SkillLifecycle;
|
|
73
|
+
export interface CurateOptions {
|
|
74
|
+
/** Use a fixed `now` for reproducible tests. */
|
|
75
|
+
now?: Date;
|
|
76
|
+
/** Override the state path for tests. */
|
|
77
|
+
statePath?: string;
|
|
78
|
+
/** Override the usage path for tests. */
|
|
79
|
+
usagePath?: string;
|
|
80
|
+
/** Skip writing the state file (read-only inspection). */
|
|
81
|
+
readOnly?: boolean;
|
|
82
|
+
}
|
|
83
|
+
export interface CuratedSkill {
|
|
84
|
+
skill: SkillEntry;
|
|
85
|
+
state: SkillState;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Walk the loaded catalog, compute each skill's lifecycle from the usage
|
|
89
|
+
* sidecar, persist the assignments, and return only the `active` + `stale`
|
|
90
|
+
* entries (callers feed these into the system prompt index builder).
|
|
91
|
+
*
|
|
92
|
+
* Archived skills are kept on disk but dropped from the returned list so
|
|
93
|
+
* the per-turn system prompt stays slim. `skill_view(name=...)` for an
|
|
94
|
+
* archived skill still works — the model just won't be prompted with it.
|
|
95
|
+
*
|
|
96
|
+
* The state-file update is idempotent: skills that didn't change lifecycle
|
|
97
|
+
* since the last call get a no-op write (still atomic).
|
|
98
|
+
*/
|
|
99
|
+
export declare function curateSkills(skills: SkillEntry[], opts?: CurateOptions): CuratedSkill[];
|
|
100
|
+
/** Test-only: drop the in-process state cache. Disk file is NOT erased —
|
|
101
|
+
* tests that need filesystem isolation should point
|
|
102
|
+
* `MAESTRO_SKILL_STATE_PATH` at a tmp file and remove it themselves. */
|
|
103
|
+
export declare function __resetForTests(): void;
|
|
104
|
+
//# sourceMappingURL=curator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"curator.d.ts","sourceRoot":"","sources":["../../src/skills/curator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAI/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAE7D,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,cAAc,CAAC;IAC1B,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,CAAC,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACpC;AAID;kCACkC;AAClC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC;8EAC8E;AAC9E,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAIrC,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CAKzC;AAWD;iEACiE;AACjE,wBAAgB,SAAS,CAAC,IAAI,GAAE,MAA2B,GAAG,cAAc,CAM3E;AAoCD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,aAAa,GAAG,SAAS,EACnC,OAAO,EAAE,OAAO,EAChB,GAAG,GAAE,IAAiB,GACrB,cAAc,CAYhB;AAED,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,IAAI,GAAE,aAAkB,GAAG,YAAY,EAAE,CAoD3F;AAED;;yEAEyE;AACzE,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|