world-model-optimizer 0.2.0__py3-none-any.whl
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.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AgentMessage } from "../../types.ts";
|
|
3
|
+
import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage } from "../messages.ts";
|
|
4
|
+
import type {
|
|
5
|
+
ActiveToolsChangeEntry,
|
|
6
|
+
BranchSummaryEntry,
|
|
7
|
+
CompactionEntry,
|
|
8
|
+
CustomEntry,
|
|
9
|
+
CustomMessageEntry,
|
|
10
|
+
LabelEntry,
|
|
11
|
+
MessageEntry,
|
|
12
|
+
ModelChangeEntry,
|
|
13
|
+
SessionContext,
|
|
14
|
+
SessionInfoEntry,
|
|
15
|
+
SessionMetadata,
|
|
16
|
+
SessionStorage,
|
|
17
|
+
SessionTreeEntry,
|
|
18
|
+
ThinkingLevelChangeEntry,
|
|
19
|
+
} from "../types.ts";
|
|
20
|
+
import { SessionError } from "../types.ts";
|
|
21
|
+
|
|
22
|
+
export function buildSessionContext(pathEntries: SessionTreeEntry[]): SessionContext {
|
|
23
|
+
let thinkingLevel = "off";
|
|
24
|
+
let model: { provider: string; modelId: string } | null = null;
|
|
25
|
+
let activeToolNames: string[] | null = null;
|
|
26
|
+
let compaction: CompactionEntry | null = null;
|
|
27
|
+
|
|
28
|
+
for (const entry of pathEntries) {
|
|
29
|
+
if (entry.type === "thinking_level_change") {
|
|
30
|
+
thinkingLevel = entry.thinkingLevel;
|
|
31
|
+
} else if (entry.type === "model_change") {
|
|
32
|
+
model = { provider: entry.provider, modelId: entry.modelId };
|
|
33
|
+
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
34
|
+
model = { provider: entry.message.provider, modelId: entry.message.model };
|
|
35
|
+
} else if (entry.type === "active_tools_change") {
|
|
36
|
+
activeToolNames = [...entry.activeToolNames];
|
|
37
|
+
} else if (entry.type === "compaction") {
|
|
38
|
+
compaction = entry;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const messages: AgentMessage[] = [];
|
|
43
|
+
const appendMessage = (entry: SessionTreeEntry) => {
|
|
44
|
+
if (entry.type === "message") {
|
|
45
|
+
messages.push(entry.message as AgentMessage);
|
|
46
|
+
} else if (entry.type === "custom_message") {
|
|
47
|
+
messages.push(
|
|
48
|
+
createCustomMessage(
|
|
49
|
+
entry.customType,
|
|
50
|
+
entry.content as string | (TextContent | ImageContent)[],
|
|
51
|
+
entry.display,
|
|
52
|
+
entry.details,
|
|
53
|
+
entry.timestamp,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
} else if (entry.type === "branch_summary" && entry.summary) {
|
|
57
|
+
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (compaction) {
|
|
62
|
+
messages.push(createCompactionSummaryMessage(compaction.summary, compaction.tokensBefore, compaction.timestamp));
|
|
63
|
+
const compactionIdx = pathEntries.findIndex((e) => e.type === "compaction" && e.id === compaction.id);
|
|
64
|
+
let foundFirstKept = false;
|
|
65
|
+
for (let i = 0; i < compactionIdx; i++) {
|
|
66
|
+
const entry = pathEntries[i]!;
|
|
67
|
+
if (entry.id === compaction.firstKeptEntryId) foundFirstKept = true;
|
|
68
|
+
if (foundFirstKept) appendMessage(entry);
|
|
69
|
+
}
|
|
70
|
+
for (let i = compactionIdx + 1; i < pathEntries.length; i++) {
|
|
71
|
+
appendMessage(pathEntries[i]!);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
for (const entry of pathEntries) {
|
|
75
|
+
appendMessage(entry);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { messages, thinkingLevel, model, activeToolNames };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class Session<TMetadata extends SessionMetadata = SessionMetadata> {
|
|
83
|
+
private storage: SessionStorage<TMetadata>;
|
|
84
|
+
|
|
85
|
+
constructor(storage: SessionStorage<TMetadata>) {
|
|
86
|
+
this.storage = storage;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getMetadata(): Promise<TMetadata> {
|
|
90
|
+
return this.storage.getMetadata();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getStorage(): SessionStorage<TMetadata> {
|
|
94
|
+
return this.storage;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getLeafId(): Promise<string | null> {
|
|
98
|
+
return this.storage.getLeafId();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getEntry(id: string): Promise<SessionTreeEntry | undefined> {
|
|
102
|
+
return this.storage.getEntry(id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getEntries(): Promise<SessionTreeEntry[]> {
|
|
106
|
+
return this.storage.getEntries();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getBranch(fromId?: string): Promise<SessionTreeEntry[]> {
|
|
110
|
+
const leafId = fromId ?? (await this.storage.getLeafId());
|
|
111
|
+
return this.storage.getPathToRoot(leafId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async buildContext(): Promise<SessionContext> {
|
|
115
|
+
return buildSessionContext(await this.getBranch());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getLabel(id: string): Promise<string | undefined> {
|
|
119
|
+
return this.storage.getLabel(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getSessionName(): Promise<string | undefined> {
|
|
123
|
+
const entries = await this.storage.findEntries("session_info");
|
|
124
|
+
return entries[entries.length - 1]?.name?.trim() || undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async appendTypedEntry<TEntry extends SessionTreeEntry>(entry: TEntry): Promise<string> {
|
|
128
|
+
await this.storage.appendEntry(entry);
|
|
129
|
+
return entry.id;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async appendMessage(message: AgentMessage): Promise<string> {
|
|
133
|
+
return this.appendTypedEntry({
|
|
134
|
+
type: "message",
|
|
135
|
+
id: await this.storage.createEntryId(),
|
|
136
|
+
parentId: await this.storage.getLeafId(),
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
message,
|
|
139
|
+
} satisfies MessageEntry);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async appendThinkingLevelChange(thinkingLevel: string): Promise<string> {
|
|
143
|
+
return this.appendTypedEntry({
|
|
144
|
+
type: "thinking_level_change",
|
|
145
|
+
id: await this.storage.createEntryId(),
|
|
146
|
+
parentId: await this.storage.getLeafId(),
|
|
147
|
+
timestamp: new Date().toISOString(),
|
|
148
|
+
thinkingLevel,
|
|
149
|
+
} satisfies ThinkingLevelChangeEntry);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async appendModelChange(provider: string, modelId: string): Promise<string> {
|
|
153
|
+
return this.appendTypedEntry({
|
|
154
|
+
type: "model_change",
|
|
155
|
+
id: await this.storage.createEntryId(),
|
|
156
|
+
parentId: await this.storage.getLeafId(),
|
|
157
|
+
timestamp: new Date().toISOString(),
|
|
158
|
+
provider,
|
|
159
|
+
modelId,
|
|
160
|
+
} satisfies ModelChangeEntry);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async appendActiveToolsChange(activeToolNames: string[]): Promise<string> {
|
|
164
|
+
return this.appendTypedEntry({
|
|
165
|
+
type: "active_tools_change",
|
|
166
|
+
id: await this.storage.createEntryId(),
|
|
167
|
+
parentId: await this.storage.getLeafId(),
|
|
168
|
+
timestamp: new Date().toISOString(),
|
|
169
|
+
activeToolNames: [...activeToolNames],
|
|
170
|
+
} satisfies ActiveToolsChangeEntry);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async appendCompaction<T = unknown>(
|
|
174
|
+
summary: string,
|
|
175
|
+
firstKeptEntryId: string,
|
|
176
|
+
tokensBefore: number,
|
|
177
|
+
details?: T,
|
|
178
|
+
fromHook?: boolean,
|
|
179
|
+
): Promise<string> {
|
|
180
|
+
return this.appendTypedEntry({
|
|
181
|
+
type: "compaction",
|
|
182
|
+
id: await this.storage.createEntryId(),
|
|
183
|
+
parentId: await this.storage.getLeafId(),
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
summary,
|
|
186
|
+
firstKeptEntryId,
|
|
187
|
+
tokensBefore,
|
|
188
|
+
details,
|
|
189
|
+
fromHook,
|
|
190
|
+
} satisfies CompactionEntry<T>);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async appendCustomEntry(customType: string, data?: unknown): Promise<string> {
|
|
194
|
+
return this.appendTypedEntry({
|
|
195
|
+
type: "custom",
|
|
196
|
+
id: await this.storage.createEntryId(),
|
|
197
|
+
parentId: await this.storage.getLeafId(),
|
|
198
|
+
timestamp: new Date().toISOString(),
|
|
199
|
+
customType,
|
|
200
|
+
data,
|
|
201
|
+
} satisfies CustomEntry);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async appendCustomMessageEntry<T = unknown>(
|
|
205
|
+
customType: string,
|
|
206
|
+
content: string | (TextContent | ImageContent)[],
|
|
207
|
+
display: boolean,
|
|
208
|
+
details?: T,
|
|
209
|
+
): Promise<string> {
|
|
210
|
+
return this.appendTypedEntry({
|
|
211
|
+
type: "custom_message",
|
|
212
|
+
id: await this.storage.createEntryId(),
|
|
213
|
+
parentId: await this.storage.getLeafId(),
|
|
214
|
+
timestamp: new Date().toISOString(),
|
|
215
|
+
customType,
|
|
216
|
+
content,
|
|
217
|
+
display,
|
|
218
|
+
details,
|
|
219
|
+
} satisfies CustomMessageEntry<T>);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async appendLabel(targetId: string, label: string | undefined): Promise<string> {
|
|
223
|
+
if (!(await this.storage.getEntry(targetId))) {
|
|
224
|
+
throw new SessionError("not_found", `Entry ${targetId} not found`);
|
|
225
|
+
}
|
|
226
|
+
return this.appendTypedEntry({
|
|
227
|
+
type: "label",
|
|
228
|
+
id: await this.storage.createEntryId(),
|
|
229
|
+
parentId: await this.storage.getLeafId(),
|
|
230
|
+
timestamp: new Date().toISOString(),
|
|
231
|
+
targetId,
|
|
232
|
+
label,
|
|
233
|
+
} satisfies LabelEntry);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async appendSessionName(name: string): Promise<string> {
|
|
237
|
+
const sanitizedName = name.replace(/[\r\n]+/g, " ").trim();
|
|
238
|
+
return this.appendTypedEntry({
|
|
239
|
+
type: "session_info",
|
|
240
|
+
id: await this.storage.createEntryId(),
|
|
241
|
+
parentId: await this.storage.getLeafId(),
|
|
242
|
+
timestamp: new Date().toISOString(),
|
|
243
|
+
name: sanitizedName,
|
|
244
|
+
} satisfies SessionInfoEntry);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async moveTo(
|
|
248
|
+
entryId: string | null,
|
|
249
|
+
summary?: { summary: string; details?: unknown; fromHook?: boolean },
|
|
250
|
+
): Promise<string | undefined> {
|
|
251
|
+
if (entryId !== null && !(await this.storage.getEntry(entryId))) {
|
|
252
|
+
throw new SessionError("not_found", `Entry ${entryId} not found`);
|
|
253
|
+
}
|
|
254
|
+
await this.storage.setLeafId(entryId);
|
|
255
|
+
if (!summary) return undefined;
|
|
256
|
+
return this.appendTypedEntry({
|
|
257
|
+
type: "branch_summary",
|
|
258
|
+
id: await this.storage.createEntryId(),
|
|
259
|
+
parentId: entryId,
|
|
260
|
+
timestamp: new Date().toISOString(),
|
|
261
|
+
fromId: entryId ?? "root",
|
|
262
|
+
summary: summary.summary,
|
|
263
|
+
details: summary.details,
|
|
264
|
+
fromHook: summary.fromHook,
|
|
265
|
+
} satisfies BranchSummaryEntry);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
let lastTimestamp = -Infinity;
|
|
2
|
+
let sequence = 0;
|
|
3
|
+
|
|
4
|
+
function fillRandomBytes(bytes: Uint8Array): void {
|
|
5
|
+
const crypto = globalThis.crypto;
|
|
6
|
+
if (crypto?.getRandomValues) {
|
|
7
|
+
crypto.getRandomValues(bytes);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
11
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function uuidv7(): string {
|
|
16
|
+
const random = new Uint8Array(16);
|
|
17
|
+
fillRandomBytes(random);
|
|
18
|
+
const timestamp = Date.now();
|
|
19
|
+
|
|
20
|
+
if (timestamp > lastTimestamp) {
|
|
21
|
+
sequence = random[6] * 0x1000000 + random[7] * 0x10000 + random[8] * 0x100 + random[9];
|
|
22
|
+
lastTimestamp = timestamp;
|
|
23
|
+
} else {
|
|
24
|
+
sequence = (sequence + 1) >>> 0;
|
|
25
|
+
if (sequence === 0) {
|
|
26
|
+
lastTimestamp++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bytes = new Uint8Array(16);
|
|
31
|
+
bytes[0] = (lastTimestamp / 0x10000000000) & 0xff;
|
|
32
|
+
bytes[1] = (lastTimestamp / 0x100000000) & 0xff;
|
|
33
|
+
bytes[2] = (lastTimestamp / 0x1000000) & 0xff;
|
|
34
|
+
bytes[3] = (lastTimestamp / 0x10000) & 0xff;
|
|
35
|
+
bytes[4] = (lastTimestamp / 0x100) & 0xff;
|
|
36
|
+
bytes[5] = lastTimestamp & 0xff;
|
|
37
|
+
bytes[6] = 0x70 | ((sequence >>> 28) & 0x0f);
|
|
38
|
+
bytes[7] = (sequence >>> 20) & 0xff;
|
|
39
|
+
bytes[8] = 0x80 | ((sequence >>> 14) & 0x3f);
|
|
40
|
+
bytes[9] = (sequence >>> 6) & 0xff;
|
|
41
|
+
bytes[10] = ((sequence & 0x3f) << 2) | (random[10] & 0x03);
|
|
42
|
+
bytes[11] = random[11];
|
|
43
|
+
bytes[12] = random[12];
|
|
44
|
+
bytes[13] = random[13];
|
|
45
|
+
bytes[14] = random[14];
|
|
46
|
+
bytes[15] = random[15];
|
|
47
|
+
|
|
48
|
+
return formatUuid(bytes);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatUuid(bytes: Uint8Array): string {
|
|
52
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
|
|
53
|
+
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import ignore from "ignore";
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
import { type ExecutionEnv, type FileInfo, type Result, type Skill, toError } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
const MAX_NAME_LENGTH = 64;
|
|
6
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
7
|
+
const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"];
|
|
8
|
+
|
|
9
|
+
type IgnoreMatcher = ReturnType<typeof ignore>;
|
|
10
|
+
|
|
11
|
+
export type SkillDiagnosticCode =
|
|
12
|
+
| "file_info_failed"
|
|
13
|
+
| "list_failed"
|
|
14
|
+
| "read_failed"
|
|
15
|
+
| "parse_failed"
|
|
16
|
+
| "invalid_metadata";
|
|
17
|
+
|
|
18
|
+
/** Warning produced while loading skills. */
|
|
19
|
+
export interface SkillDiagnostic {
|
|
20
|
+
/** Diagnostic severity. Currently only warnings are emitted. */
|
|
21
|
+
type: "warning";
|
|
22
|
+
/** Stable diagnostic code. */
|
|
23
|
+
code: SkillDiagnosticCode;
|
|
24
|
+
/** Human-readable diagnostic message. */
|
|
25
|
+
message: string;
|
|
26
|
+
/** Path associated with the diagnostic. */
|
|
27
|
+
path: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SkillFrontmatter {
|
|
31
|
+
name?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
"disable-model-invocation"?: boolean;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Format a skill invocation prompt, optionally appending additional user instructions. */
|
|
38
|
+
export function formatSkillInvocation(skill: Skill, additionalInstructions?: string): string {
|
|
39
|
+
const skillBlock = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${dirnameEnvPath(skill.filePath)}.\n\n${skill.content}\n</skill>`;
|
|
40
|
+
return additionalInstructions ? `${skillBlock}\n\n${additionalInstructions}` : skillBlock;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load skills from one or more directories.
|
|
45
|
+
*
|
|
46
|
+
* Traverses directories recursively, loads `SKILL.md` files, loads direct root `.md` files as skills, honors ignore files,
|
|
47
|
+
* and returns diagnostics for invalid skill files. Missing input directories are skipped.
|
|
48
|
+
*/
|
|
49
|
+
export async function loadSkills(
|
|
50
|
+
env: ExecutionEnv,
|
|
51
|
+
dirs: string | string[],
|
|
52
|
+
): Promise<{ skills: Skill[]; diagnostics: SkillDiagnostic[] }> {
|
|
53
|
+
const skills: Skill[] = [];
|
|
54
|
+
const diagnostics: SkillDiagnostic[] = [];
|
|
55
|
+
for (const dir of Array.isArray(dirs) ? dirs : [dirs]) {
|
|
56
|
+
const rootInfoResult = await env.fileInfo(dir);
|
|
57
|
+
if (!rootInfoResult.ok) {
|
|
58
|
+
if (rootInfoResult.error.code !== "not_found") {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
type: "warning",
|
|
61
|
+
code: "file_info_failed",
|
|
62
|
+
message: rootInfoResult.error.message,
|
|
63
|
+
path: dir,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const rootInfo = rootInfoResult.value;
|
|
69
|
+
if ((await resolveKind(env, rootInfo, diagnostics)) !== "directory") continue;
|
|
70
|
+
const result = await loadSkillsFromDirInternal(env, rootInfo.path, true, ignore(), rootInfo.path);
|
|
71
|
+
skills.push(...result.skills);
|
|
72
|
+
diagnostics.push(...result.diagnostics);
|
|
73
|
+
}
|
|
74
|
+
return { skills, diagnostics };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load skills from source-tagged directories.
|
|
79
|
+
*
|
|
80
|
+
* Source values are preserved exactly and attached to every loaded skill and diagnostic. The agent package does not
|
|
81
|
+
* interpret source values; applications define their own provenance shape.
|
|
82
|
+
*/
|
|
83
|
+
export async function loadSourcedSkills<TSource, TSkill extends Skill = Skill>(
|
|
84
|
+
env: ExecutionEnv,
|
|
85
|
+
inputs: Array<{ path: string; source: TSource }>,
|
|
86
|
+
mapSkill?: (skill: Skill, source: TSource) => TSkill,
|
|
87
|
+
): Promise<{
|
|
88
|
+
skills: Array<{ skill: TSkill; source: TSource }>;
|
|
89
|
+
diagnostics: Array<SkillDiagnostic & { source: TSource }>;
|
|
90
|
+
}> {
|
|
91
|
+
const skills: Array<{ skill: TSkill; source: TSource }> = [];
|
|
92
|
+
const diagnostics: Array<SkillDiagnostic & { source: TSource }> = [];
|
|
93
|
+
for (const input of inputs) {
|
|
94
|
+
const result = await loadSkills(env, input.path);
|
|
95
|
+
for (const skill of result.skills) {
|
|
96
|
+
skills.push({ skill: mapSkill ? mapSkill(skill, input.source) : (skill as TSkill), source: input.source });
|
|
97
|
+
}
|
|
98
|
+
for (const diagnostic of result.diagnostics) diagnostics.push({ ...diagnostic, source: input.source });
|
|
99
|
+
}
|
|
100
|
+
return { skills, diagnostics };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function loadSkillsFromDirInternal(
|
|
104
|
+
env: ExecutionEnv,
|
|
105
|
+
dir: string,
|
|
106
|
+
includeRootFiles: boolean,
|
|
107
|
+
ignoreMatcher: IgnoreMatcher,
|
|
108
|
+
rootDir: string,
|
|
109
|
+
): Promise<{ skills: Skill[]; diagnostics: SkillDiagnostic[] }> {
|
|
110
|
+
const skills: Skill[] = [];
|
|
111
|
+
const diagnostics: SkillDiagnostic[] = [];
|
|
112
|
+
|
|
113
|
+
const dirInfoResult = await env.fileInfo(dir);
|
|
114
|
+
if (!dirInfoResult.ok) {
|
|
115
|
+
if (dirInfoResult.error.code !== "not_found") {
|
|
116
|
+
diagnostics.push({
|
|
117
|
+
type: "warning",
|
|
118
|
+
code: "file_info_failed",
|
|
119
|
+
message: dirInfoResult.error.message,
|
|
120
|
+
path: dir,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return { skills, diagnostics };
|
|
124
|
+
}
|
|
125
|
+
const dirInfo = dirInfoResult.value;
|
|
126
|
+
if ((await resolveKind(env, dirInfo, diagnostics)) !== "directory") return { skills, diagnostics };
|
|
127
|
+
|
|
128
|
+
await addIgnoreRules(env, ignoreMatcher, dir, rootDir, diagnostics);
|
|
129
|
+
|
|
130
|
+
const entriesResult = await env.listDir(dir);
|
|
131
|
+
if (!entriesResult.ok) {
|
|
132
|
+
diagnostics.push({ type: "warning", code: "list_failed", message: entriesResult.error.message, path: dir });
|
|
133
|
+
return { skills, diagnostics };
|
|
134
|
+
}
|
|
135
|
+
const entries = entriesResult.value;
|
|
136
|
+
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.name !== "SKILL.md") continue;
|
|
139
|
+
const fullPath = entry.path;
|
|
140
|
+
const kind = await resolveKind(env, entry, diagnostics);
|
|
141
|
+
if (kind !== "file") continue;
|
|
142
|
+
const relPath = relativeEnvPath(rootDir, fullPath);
|
|
143
|
+
if (ignoreMatcher.ignores(relPath)) continue;
|
|
144
|
+
|
|
145
|
+
const result = await loadSkillFromFile(env, fullPath);
|
|
146
|
+
if (result.skill) skills.push(result.skill);
|
|
147
|
+
diagnostics.push(...result.diagnostics);
|
|
148
|
+
return { skills, diagnostics };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
152
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
153
|
+
const fullPath = entry.path;
|
|
154
|
+
const kind = await resolveKind(env, entry, diagnostics);
|
|
155
|
+
if (!kind) continue;
|
|
156
|
+
|
|
157
|
+
const relPath = relativeEnvPath(rootDir, fullPath);
|
|
158
|
+
const ignorePath = kind === "directory" ? `${relPath}/` : relPath;
|
|
159
|
+
if (ignoreMatcher.ignores(ignorePath)) continue;
|
|
160
|
+
|
|
161
|
+
if (kind === "directory") {
|
|
162
|
+
const result = await loadSkillsFromDirInternal(env, fullPath, false, ignoreMatcher, rootDir);
|
|
163
|
+
skills.push(...result.skills);
|
|
164
|
+
diagnostics.push(...result.diagnostics);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (kind !== "file" || !includeRootFiles || !entry.name.endsWith(".md")) continue;
|
|
169
|
+
const result = await loadSkillFromFile(env, fullPath);
|
|
170
|
+
if (result.skill) skills.push(result.skill);
|
|
171
|
+
diagnostics.push(...result.diagnostics);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { skills, diagnostics };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function addIgnoreRules(
|
|
178
|
+
env: ExecutionEnv,
|
|
179
|
+
ig: IgnoreMatcher,
|
|
180
|
+
dir: string,
|
|
181
|
+
rootDir: string,
|
|
182
|
+
diagnostics: SkillDiagnostic[],
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
const relativeDir = relativeEnvPath(rootDir, dir);
|
|
185
|
+
const prefix = relativeDir ? `${relativeDir}/` : "";
|
|
186
|
+
|
|
187
|
+
for (const filename of IGNORE_FILE_NAMES) {
|
|
188
|
+
const ignorePath = joinEnvPath(dir, filename);
|
|
189
|
+
const info = await env.fileInfo(ignorePath);
|
|
190
|
+
if (!info.ok) {
|
|
191
|
+
if (info.error.code !== "not_found") {
|
|
192
|
+
diagnostics.push({
|
|
193
|
+
type: "warning",
|
|
194
|
+
code: "file_info_failed",
|
|
195
|
+
message: info.error.message,
|
|
196
|
+
path: ignorePath,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (info.value.kind !== "file") continue;
|
|
202
|
+
const content = await env.readTextFile(ignorePath);
|
|
203
|
+
if (!content.ok) {
|
|
204
|
+
diagnostics.push({ type: "warning", code: "read_failed", message: content.error.message, path: ignorePath });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const patterns = content.value
|
|
208
|
+
.split(/\r?\n/)
|
|
209
|
+
.map((line) => prefixIgnorePattern(line, prefix))
|
|
210
|
+
.filter((line): line is string => Boolean(line));
|
|
211
|
+
if (patterns.length > 0) ig.add(patterns);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function prefixIgnorePattern(line: string, prefix: string): string | null {
|
|
216
|
+
const trimmed = line.trim();
|
|
217
|
+
if (!trimmed) return null;
|
|
218
|
+
if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return null;
|
|
219
|
+
|
|
220
|
+
let pattern = line;
|
|
221
|
+
let negated = false;
|
|
222
|
+
if (pattern.startsWith("!")) {
|
|
223
|
+
negated = true;
|
|
224
|
+
pattern = pattern.slice(1);
|
|
225
|
+
} else if (pattern.startsWith("\\!")) {
|
|
226
|
+
pattern = pattern.slice(1);
|
|
227
|
+
}
|
|
228
|
+
if (pattern.startsWith("/")) pattern = pattern.slice(1);
|
|
229
|
+
const prefixed = prefix ? `${prefix}${pattern}` : pattern;
|
|
230
|
+
return negated ? `!${prefixed}` : prefixed;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function loadSkillFromFile(
|
|
234
|
+
env: ExecutionEnv,
|
|
235
|
+
filePath: string,
|
|
236
|
+
): Promise<{ skill: Skill | null; diagnostics: SkillDiagnostic[] }> {
|
|
237
|
+
const diagnostics: SkillDiagnostic[] = [];
|
|
238
|
+
const rawContent = await env.readTextFile(filePath);
|
|
239
|
+
if (!rawContent.ok) {
|
|
240
|
+
diagnostics.push({ type: "warning", code: "read_failed", message: rawContent.error.message, path: filePath });
|
|
241
|
+
return { skill: null, diagnostics };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const parsed = parseFrontmatter<SkillFrontmatter>(rawContent.value);
|
|
245
|
+
if (!parsed.ok) {
|
|
246
|
+
diagnostics.push({ type: "warning", code: "parse_failed", message: parsed.error.message, path: filePath });
|
|
247
|
+
return { skill: null, diagnostics };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const { frontmatter, body } = parsed.value;
|
|
251
|
+
const skillDir = dirnameEnvPath(filePath);
|
|
252
|
+
const parentDirName = basenameEnvPath(skillDir);
|
|
253
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined;
|
|
254
|
+
|
|
255
|
+
for (const error of validateDescription(description)) {
|
|
256
|
+
diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const frontmatterName = typeof frontmatter.name === "string" ? frontmatter.name : undefined;
|
|
260
|
+
const name = frontmatterName || parentDirName;
|
|
261
|
+
for (const error of validateName(name, parentDirName)) {
|
|
262
|
+
diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!description || description.trim() === "") {
|
|
266
|
+
return { skill: null, diagnostics };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
skill: {
|
|
271
|
+
name,
|
|
272
|
+
description,
|
|
273
|
+
content: body,
|
|
274
|
+
filePath,
|
|
275
|
+
disableModelInvocation: frontmatter["disable-model-invocation"] === true,
|
|
276
|
+
},
|
|
277
|
+
diagnostics,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function validateName(name: string, parentDirName: string): string[] {
|
|
282
|
+
const errors: string[] = [];
|
|
283
|
+
if (name !== parentDirName) errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
|
|
284
|
+
if (name.length > MAX_NAME_LENGTH) errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
|
|
285
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
286
|
+
errors.push("name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)");
|
|
287
|
+
}
|
|
288
|
+
if (name.startsWith("-") || name.endsWith("-")) errors.push("name must not start or end with a hyphen");
|
|
289
|
+
if (name.includes("--")) errors.push("name must not contain consecutive hyphens");
|
|
290
|
+
return errors;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function validateDescription(description: string | undefined): string[] {
|
|
294
|
+
const errors: string[] = [];
|
|
295
|
+
if (!description || description.trim() === "") {
|
|
296
|
+
errors.push("description is required");
|
|
297
|
+
} else if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
298
|
+
errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
|
|
299
|
+
}
|
|
300
|
+
return errors;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function parseFrontmatter<T extends Record<string, unknown>>(
|
|
304
|
+
content: string,
|
|
305
|
+
): Result<{ frontmatter: T; body: string }, Error> {
|
|
306
|
+
try {
|
|
307
|
+
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
308
|
+
if (!normalized.startsWith("---")) return { ok: true, value: { frontmatter: {} as T, body: normalized } };
|
|
309
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
310
|
+
if (endIndex === -1) return { ok: true, value: { frontmatter: {} as T, body: normalized } };
|
|
311
|
+
const yamlString = normalized.slice(4, endIndex);
|
|
312
|
+
const body = normalized.slice(endIndex + 4).trim();
|
|
313
|
+
return { ok: true, value: { frontmatter: (parse(yamlString) ?? {}) as T, body } };
|
|
314
|
+
} catch (error) {
|
|
315
|
+
return { ok: false, error: toError(error) };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function resolveKind(
|
|
320
|
+
env: ExecutionEnv,
|
|
321
|
+
info: FileInfo,
|
|
322
|
+
diagnostics: SkillDiagnostic[],
|
|
323
|
+
): Promise<"file" | "directory" | undefined> {
|
|
324
|
+
if (info.kind === "file" || info.kind === "directory") return info.kind;
|
|
325
|
+
const canonicalPath = await env.canonicalPath(info.path);
|
|
326
|
+
if (!canonicalPath.ok) {
|
|
327
|
+
if (canonicalPath.error.code !== "not_found") {
|
|
328
|
+
diagnostics.push({
|
|
329
|
+
type: "warning",
|
|
330
|
+
code: "file_info_failed",
|
|
331
|
+
message: canonicalPath.error.message,
|
|
332
|
+
path: info.path,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
const target = await env.fileInfo(canonicalPath.value);
|
|
338
|
+
if (!target.ok) {
|
|
339
|
+
if (target.error.code !== "not_found") {
|
|
340
|
+
diagnostics.push({
|
|
341
|
+
type: "warning",
|
|
342
|
+
code: "file_info_failed",
|
|
343
|
+
message: target.error.message,
|
|
344
|
+
path: info.path,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
return target.value.kind === "file" || target.value.kind === "directory" ? target.value.kind : undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function joinEnvPath(base: string, child: string): string {
|
|
353
|
+
return `${base.replace(/\/+$/, "")}/${child.replace(/^\/+/, "")}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function dirnameEnvPath(path: string): string {
|
|
357
|
+
const normalized = path.replace(/\/+$/, "");
|
|
358
|
+
const slashIndex = normalized.lastIndexOf("/");
|
|
359
|
+
return slashIndex <= 0 ? "/" : normalized.slice(0, slashIndex);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function basenameEnvPath(path: string): string {
|
|
363
|
+
const normalized = path.replace(/\/+$/, "");
|
|
364
|
+
const slashIndex = normalized.lastIndexOf("/");
|
|
365
|
+
return slashIndex === -1 ? normalized : normalized.slice(slashIndex + 1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function relativeEnvPath(root: string, path: string): string {
|
|
369
|
+
const normalizedRoot = root.replace(/\/+$/, "");
|
|
370
|
+
const normalizedPath = path.replace(/\/+$/, "");
|
|
371
|
+
if (normalizedPath === normalizedRoot) return "";
|
|
372
|
+
return normalizedPath.startsWith(`${normalizedRoot}/`)
|
|
373
|
+
? normalizedPath.slice(normalizedRoot.length + 1)
|
|
374
|
+
: normalizedPath.replace(/^\/+/, "");
|
|
375
|
+
}
|