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,156 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
|
|
5
|
+
import { JsonlSessionStorage } from "../../src/harness/session/jsonl-storage.ts";
|
|
6
|
+
import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts";
|
|
7
|
+
import { Session } from "../../src/harness/session/session.ts";
|
|
8
|
+
import type { SessionStorage } from "../../src/harness/types.ts";
|
|
9
|
+
import { createAssistantMessage, createTempDir, createUserMessage, getLatestTempDir } from "./session-test-utils.ts";
|
|
10
|
+
|
|
11
|
+
async function runSessionSuite(
|
|
12
|
+
name: string,
|
|
13
|
+
createStorage: () => SessionStorage | Promise<SessionStorage>,
|
|
14
|
+
inspect?: () => void,
|
|
15
|
+
) {
|
|
16
|
+
describe(name, () => {
|
|
17
|
+
it("appends messages and builds context in order", async () => {
|
|
18
|
+
const session = new Session(await createStorage());
|
|
19
|
+
await session.appendMessage(createUserMessage("one"));
|
|
20
|
+
await session.appendMessage(createAssistantMessage("two"));
|
|
21
|
+
const context = await session.buildContext();
|
|
22
|
+
expect(context.messages.map((message) => message.role)).toEqual(["user", "assistant"]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("tracks model and thinking level changes", async () => {
|
|
26
|
+
const session = new Session(await createStorage());
|
|
27
|
+
await session.appendMessage(createUserMessage("one"));
|
|
28
|
+
await session.appendModelChange("openai", "gpt-4.1");
|
|
29
|
+
await session.appendThinkingLevelChange("high");
|
|
30
|
+
const context = await session.buildContext();
|
|
31
|
+
expect(context.thinkingLevel).toBe("high");
|
|
32
|
+
expect(context.model).toEqual({ provider: "openai", modelId: "gpt-4.1" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("supports branching by moving the leaf and appending a new branch", async () => {
|
|
36
|
+
const session = new Session(await createStorage());
|
|
37
|
+
const user1 = await session.appendMessage(createUserMessage("one"));
|
|
38
|
+
const assistant1 = await session.appendMessage(createAssistantMessage("two"));
|
|
39
|
+
await session.appendMessage(createUserMessage("three"));
|
|
40
|
+
await session.moveTo(user1);
|
|
41
|
+
await session.appendMessage(createAssistantMessage("branched"));
|
|
42
|
+
const branch = await session.getBranch();
|
|
43
|
+
expect(branch.map((entry) => entry.id)).toContain(user1);
|
|
44
|
+
expect(branch.map((entry) => entry.id)).not.toContain(assistant1);
|
|
45
|
+
const context = await session.buildContext();
|
|
46
|
+
expect(context.messages.map((message) => message.role)).toEqual(["user", "assistant"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("supports moving the leaf to root", async () => {
|
|
50
|
+
const session = new Session(await createStorage());
|
|
51
|
+
await session.appendMessage(createUserMessage("one"));
|
|
52
|
+
await session.moveTo(null);
|
|
53
|
+
expect(await session.getLeafId()).toBeNull();
|
|
54
|
+
expect((await session.buildContext()).messages).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("reconstructs compaction summaries in context", async () => {
|
|
58
|
+
const session = new Session(await createStorage());
|
|
59
|
+
await session.appendMessage(createUserMessage("one"));
|
|
60
|
+
await session.appendMessage(createAssistantMessage("two"));
|
|
61
|
+
const user2 = await session.appendMessage(createUserMessage("three"));
|
|
62
|
+
await session.appendMessage(createAssistantMessage("four"));
|
|
63
|
+
await session.appendCompaction("summary", user2, 1234);
|
|
64
|
+
await session.appendMessage(createUserMessage("five"));
|
|
65
|
+
const context = await session.buildContext();
|
|
66
|
+
expect(context.messages[0]?.role).toBe("compactionSummary");
|
|
67
|
+
expect(context.messages).toHaveLength(4);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("supports moving with branch summary entries in context", async () => {
|
|
71
|
+
const session = new Session(await createStorage());
|
|
72
|
+
const user1 = await session.appendMessage(createUserMessage("one"));
|
|
73
|
+
const summaryId = await session.moveTo(user1, { summary: "summary text" });
|
|
74
|
+
expect(summaryId).toBeTruthy();
|
|
75
|
+
const summaryEntry = await session.getEntry(summaryId!);
|
|
76
|
+
expect(summaryEntry).toMatchObject({ type: "branch_summary", parentId: user1, fromId: user1 });
|
|
77
|
+
const context = await session.buildContext();
|
|
78
|
+
expect(context.messages[1]?.role).toBe("branchSummary");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("supports custom message entries in context", async () => {
|
|
82
|
+
const session = new Session(await createStorage());
|
|
83
|
+
await session.appendMessage(createUserMessage("one"));
|
|
84
|
+
await session.appendCustomMessageEntry("custom", "hello", true, { ok: true });
|
|
85
|
+
const context = await session.buildContext();
|
|
86
|
+
expect(context.messages[1]?.role).toBe("custom");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("normalizes session names", async () => {
|
|
90
|
+
const session = new Session(await createStorage());
|
|
91
|
+
await session.appendSessionName(" hello\nworld\r\nagain ");
|
|
92
|
+
expect(await session.getSessionName()).toBe("hello world again");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("supports labels and session info entries without affecting context", async () => {
|
|
96
|
+
const session = new Session(await createStorage());
|
|
97
|
+
const user1 = await session.appendMessage(createUserMessage("one"));
|
|
98
|
+
await session.appendLabel(user1, "checkpoint");
|
|
99
|
+
await session.appendSessionName("name");
|
|
100
|
+
const entries = await session.getEntries();
|
|
101
|
+
expect(entries.some((entry) => entry.type === "label")).toBe(true);
|
|
102
|
+
expect(entries.some((entry) => entry.type === "session_info")).toBe(true);
|
|
103
|
+
expect(await session.getLabel(user1)).toBe("checkpoint");
|
|
104
|
+
expect(await session.getSessionName()).toBe("name");
|
|
105
|
+
expect((await session.buildContext()).messages).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("rejects labels for missing entries", async () => {
|
|
109
|
+
const session = new Session(await createStorage());
|
|
110
|
+
await expect(session.appendLabel("missing", "checkpoint")).rejects.toThrow("Entry missing not found");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("persists leaf changes and appended entries via storage", async () => {
|
|
114
|
+
const storage = await createStorage();
|
|
115
|
+
const session = new Session(storage);
|
|
116
|
+
const user1 = await session.appendMessage(createUserMessage("one"));
|
|
117
|
+
await session.appendMessage(createAssistantMessage("two"));
|
|
118
|
+
await session.appendLabel(user1, "checkpoint");
|
|
119
|
+
await session.appendSessionName("name");
|
|
120
|
+
await session.moveTo(user1);
|
|
121
|
+
await session.appendMessage(createAssistantMessage("branched"));
|
|
122
|
+
const session2 = new Session(storage);
|
|
123
|
+
const context = await session2.buildContext();
|
|
124
|
+
expect(context.messages.map((message) => message.role)).toEqual(["user", "assistant"]);
|
|
125
|
+
expect(await session2.getLabel(user1)).toBe("checkpoint");
|
|
126
|
+
expect(await session2.getSessionName()).toBe("name");
|
|
127
|
+
inspect?.();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
runSessionSuite("Session with in-memory storage", () => new InMemorySessionStorage());
|
|
133
|
+
|
|
134
|
+
runSessionSuite(
|
|
135
|
+
"Session with JSONL storage",
|
|
136
|
+
async () => {
|
|
137
|
+
const dir = createTempDir();
|
|
138
|
+
const env = new NodeExecutionEnv({ cwd: dir });
|
|
139
|
+
return await JsonlSessionStorage.create(env, join(dir, "session.jsonl"), { cwd: dir, sessionId: "session-1" });
|
|
140
|
+
},
|
|
141
|
+
() => {
|
|
142
|
+
const dir = getLatestTempDir();
|
|
143
|
+
const filePath = join(dir, "session.jsonl");
|
|
144
|
+
const lines = readFileSync(filePath, "utf8").trim().split("\n");
|
|
145
|
+
expect(lines.length).toBeGreaterThan(1);
|
|
146
|
+
const header = JSON.parse(lines[0]!);
|
|
147
|
+
expect(header.type).toBe("session");
|
|
148
|
+
expect(header.version).toBe(3);
|
|
149
|
+
const entries = lines.slice(1).map((line) => JSON.parse(line));
|
|
150
|
+
expect(entries.some((entry) => entry.type === "leaf")).toBe(true);
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
expect(entry.type).not.toBe("entry");
|
|
153
|
+
expect(typeof entry.id).toBe("string");
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { symlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
|
|
5
|
+
import { loadSkills, loadSourcedSkills } from "../../src/harness/skills.ts";
|
|
6
|
+
import { createTempDir } from "./session-test-utils.ts";
|
|
7
|
+
|
|
8
|
+
describe("loadSkills", () => {
|
|
9
|
+
it("loads SKILL.md files through the execution environment", async () => {
|
|
10
|
+
const root = createTempDir();
|
|
11
|
+
const env = new NodeExecutionEnv({ cwd: root });
|
|
12
|
+
await env.createDir(".agents/skills/example", { recursive: true });
|
|
13
|
+
await env.writeFile(
|
|
14
|
+
".agents/skills/example/SKILL.md",
|
|
15
|
+
`---
|
|
16
|
+
name: example
|
|
17
|
+
description: Example skill
|
|
18
|
+
disable-model-invocation: true
|
|
19
|
+
---
|
|
20
|
+
Use this skill.
|
|
21
|
+
`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const { skills, diagnostics } = await loadSkills(env, ".agents/skills");
|
|
25
|
+
|
|
26
|
+
expect(diagnostics).toEqual([]);
|
|
27
|
+
expect(skills).toEqual([
|
|
28
|
+
{
|
|
29
|
+
name: "example",
|
|
30
|
+
description: "Example skill",
|
|
31
|
+
content: "Use this skill.",
|
|
32
|
+
filePath: join(root, ".agents/skills/example/SKILL.md"),
|
|
33
|
+
disableModelInvocation: true,
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("loads skills through symlinked directories", async () => {
|
|
39
|
+
const root = createTempDir();
|
|
40
|
+
const env = new NodeExecutionEnv({ cwd: root });
|
|
41
|
+
await env.createDir("actual/example", { recursive: true });
|
|
42
|
+
await env.writeFile(
|
|
43
|
+
"actual/example/SKILL.md",
|
|
44
|
+
"---\nname: example\ndescription: Example skill\n---\nUse this skill.",
|
|
45
|
+
);
|
|
46
|
+
await symlink(join(root, "actual"), join(root, "skills-link"));
|
|
47
|
+
|
|
48
|
+
const { skills } = await loadSkills(env, "skills-link");
|
|
49
|
+
|
|
50
|
+
expect(skills.map((skill) => skill.name)).toEqual(["example"]);
|
|
51
|
+
expect(skills[0]?.filePath).toBe(join(root, "skills-link/example/SKILL.md"));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("preserves source info for sourced skills", async () => {
|
|
55
|
+
const root = createTempDir();
|
|
56
|
+
const env = new NodeExecutionEnv({ cwd: root });
|
|
57
|
+
await env.createDir("user/example", { recursive: true });
|
|
58
|
+
await env.writeFile(
|
|
59
|
+
"user/example/SKILL.md",
|
|
60
|
+
"---\nname: example\ndescription: Example skill\n---\nUse this skill.",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const { skills, diagnostics } = await loadSourcedSkills(env, [
|
|
64
|
+
{ path: "user", source: { type: "user" as const } },
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
expect(diagnostics).toEqual([]);
|
|
68
|
+
expect(skills).toEqual([
|
|
69
|
+
{
|
|
70
|
+
skill: {
|
|
71
|
+
name: "example",
|
|
72
|
+
description: "Example skill",
|
|
73
|
+
content: "Use this skill.",
|
|
74
|
+
filePath: join(root, "user/example/SKILL.md"),
|
|
75
|
+
disableModelInvocation: false,
|
|
76
|
+
},
|
|
77
|
+
source: { type: "user" },
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("attaches source info to diagnostics", async () => {
|
|
83
|
+
const root = createTempDir();
|
|
84
|
+
const env = new NodeExecutionEnv({ cwd: root });
|
|
85
|
+
await env.createDir("user/broken", { recursive: true });
|
|
86
|
+
await env.writeFile("user/broken/SKILL.md", "---\nname: broken\n---\nMissing description.");
|
|
87
|
+
|
|
88
|
+
const { skills, diagnostics } = await loadSourcedSkills(env, [
|
|
89
|
+
{ path: "user", source: { type: "user" as const } },
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
expect(skills).toEqual([]);
|
|
93
|
+
expect(diagnostics).toEqual([
|
|
94
|
+
{
|
|
95
|
+
type: "warning",
|
|
96
|
+
code: "invalid_metadata",
|
|
97
|
+
message: "description is required",
|
|
98
|
+
path: join(root, "user/broken/SKILL.md"),
|
|
99
|
+
source: { type: "user" },
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("loads direct markdown children only from the root directory", async () => {
|
|
105
|
+
const root = createTempDir();
|
|
106
|
+
const env = new NodeExecutionEnv({ cwd: root });
|
|
107
|
+
await env.createDir("skills/nested", { recursive: true });
|
|
108
|
+
await env.writeFile("skills/root.md", "---\ndescription: Root skill\n---\nRoot content");
|
|
109
|
+
await env.writeFile("skills/nested/ignored.md", "---\ndescription: Ignored\n---\nIgnored content");
|
|
110
|
+
|
|
111
|
+
const { skills } = await loadSkills(env, "skills");
|
|
112
|
+
|
|
113
|
+
expect(skills.map((skill) => skill.name)).toEqual(["skills"]);
|
|
114
|
+
expect(skills[0]?.content).toBe("Root content");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
|
|
5
|
+
import { JsonlSessionStorage, loadJsonlSessionMetadata } from "../../src/harness/session/jsonl-storage.ts";
|
|
6
|
+
import { InMemorySessionStorage } from "../../src/harness/session/memory-storage.ts";
|
|
7
|
+
import { type MessageEntry, ok, type SessionMetadata } from "../../src/harness/types.ts";
|
|
8
|
+
import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.ts";
|
|
9
|
+
|
|
10
|
+
describe("InMemorySessionStorage", () => {
|
|
11
|
+
it("returns configured session metadata", async () => {
|
|
12
|
+
const metadata: SessionMetadata = { id: "session-1", createdAt: "2026-01-01T00:00:00.000Z" };
|
|
13
|
+
const storage = new InMemorySessionStorage({ metadata });
|
|
14
|
+
expect(await storage.getMetadata()).toEqual(metadata);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("copies initial entries and persists leaf changes", async () => {
|
|
18
|
+
const entry: MessageEntry = {
|
|
19
|
+
type: "message",
|
|
20
|
+
id: "entry-1",
|
|
21
|
+
parentId: null,
|
|
22
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
23
|
+
message: createUserMessage("one"),
|
|
24
|
+
};
|
|
25
|
+
const initialEntries = [entry];
|
|
26
|
+
const storage = new InMemorySessionStorage({ entries: initialEntries });
|
|
27
|
+
initialEntries.push({ ...entry, id: "entry-2" });
|
|
28
|
+
expect((await storage.getEntries()).map((storedEntry) => storedEntry.id)).toEqual(["entry-1"]);
|
|
29
|
+
expect(await storage.getLeafId()).toBe("entry-1");
|
|
30
|
+
await storage.setLeafId(null);
|
|
31
|
+
expect(await storage.getLeafId()).toBeNull();
|
|
32
|
+
expect((await storage.getEntries()).at(-1)).toMatchObject({ type: "leaf", targetId: null });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("rejects invalid leaf ids", async () => {
|
|
36
|
+
const storage = new InMemorySessionStorage();
|
|
37
|
+
await expect(storage.setLeafId("missing")).rejects.toThrow("Entry missing not found");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("finds entries by type", async () => {
|
|
41
|
+
const entry: MessageEntry = {
|
|
42
|
+
type: "message",
|
|
43
|
+
id: "entry-1",
|
|
44
|
+
parentId: null,
|
|
45
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
46
|
+
message: createUserMessage("one"),
|
|
47
|
+
};
|
|
48
|
+
const storage = new InMemorySessionStorage({ entries: [entry] });
|
|
49
|
+
expect((await storage.findEntries("message")).map((found) => found.id)).toEqual(["entry-1"]);
|
|
50
|
+
expect(await storage.findEntries("session_info")).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("maintains label lookup", async () => {
|
|
54
|
+
const entry: MessageEntry = {
|
|
55
|
+
type: "message",
|
|
56
|
+
id: "entry-1",
|
|
57
|
+
parentId: null,
|
|
58
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
59
|
+
message: createUserMessage("one"),
|
|
60
|
+
};
|
|
61
|
+
const storage = new InMemorySessionStorage({ entries: [entry] });
|
|
62
|
+
expect(await storage.getLabel("entry-1")).toBeUndefined();
|
|
63
|
+
await storage.appendEntry({
|
|
64
|
+
type: "label",
|
|
65
|
+
id: "label-1",
|
|
66
|
+
parentId: "entry-1",
|
|
67
|
+
timestamp: "2026-01-01T00:00:01.000Z",
|
|
68
|
+
targetId: "entry-1",
|
|
69
|
+
label: "checkpoint",
|
|
70
|
+
});
|
|
71
|
+
expect(await storage.getLabel("entry-1")).toBe("checkpoint");
|
|
72
|
+
await storage.appendEntry({
|
|
73
|
+
type: "label",
|
|
74
|
+
id: "label-2",
|
|
75
|
+
parentId: "label-1",
|
|
76
|
+
timestamp: "2026-01-01T00:00:02.000Z",
|
|
77
|
+
targetId: "entry-1",
|
|
78
|
+
label: undefined,
|
|
79
|
+
});
|
|
80
|
+
expect(await storage.getLabel("entry-1")).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("walks paths to root", async () => {
|
|
84
|
+
const root: MessageEntry = {
|
|
85
|
+
type: "message",
|
|
86
|
+
id: "root",
|
|
87
|
+
parentId: null,
|
|
88
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
89
|
+
message: createUserMessage("root"),
|
|
90
|
+
};
|
|
91
|
+
const child: MessageEntry = {
|
|
92
|
+
...root,
|
|
93
|
+
id: "child",
|
|
94
|
+
parentId: "root",
|
|
95
|
+
message: createAssistantMessage("child"),
|
|
96
|
+
};
|
|
97
|
+
const storage = new InMemorySessionStorage({ entries: [root, child] });
|
|
98
|
+
expect((await storage.getPathToRoot("child")).map((entry) => entry.id)).toEqual(["root", "child"]);
|
|
99
|
+
expect(await storage.getPathToRoot(null)).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("JsonlSessionStorage", () => {
|
|
104
|
+
it("throws for missing files when opening", async () => {
|
|
105
|
+
const dir = createTempDir();
|
|
106
|
+
const env = new NodeExecutionEnv({ cwd: dir });
|
|
107
|
+
const filePath = join(dir, "session.jsonl");
|
|
108
|
+
await expect(JsonlSessionStorage.open(env, filePath)).rejects.toMatchObject({ code: "not_found" });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("writes the header on create", async () => {
|
|
112
|
+
const dir = createTempDir();
|
|
113
|
+
const env = new NodeExecutionEnv({ cwd: dir });
|
|
114
|
+
const filePath = join(dir, "session.jsonl");
|
|
115
|
+
const storage = await JsonlSessionStorage.create(env, filePath, { cwd: dir, sessionId: "session-1" });
|
|
116
|
+
expect(existsSync(filePath)).toBe(true);
|
|
117
|
+
expect(readFileSync(filePath, "utf8").trim().split("\n")).toHaveLength(1);
|
|
118
|
+
expect(await storage.getLeafId()).toBeNull();
|
|
119
|
+
expect(await storage.getEntries()).toEqual([]);
|
|
120
|
+
await storage.appendEntry({
|
|
121
|
+
type: "message",
|
|
122
|
+
id: "user-1",
|
|
123
|
+
parentId: null,
|
|
124
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
125
|
+
message: createUserMessage("one"),
|
|
126
|
+
});
|
|
127
|
+
const lines = readFileSync(filePath, "utf8").trim().split("\n");
|
|
128
|
+
expect(JSON.parse(lines[0]!).type).toBe("session");
|
|
129
|
+
expect(JSON.parse(lines[1]!).id).toBe("user-1");
|
|
130
|
+
expect(lines).toHaveLength(2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("throws for malformed session headers", async () => {
|
|
134
|
+
const dir = createTempDir();
|
|
135
|
+
const env = new NodeExecutionEnv({ cwd: dir });
|
|
136
|
+
const filePath = join(dir, "session.jsonl");
|
|
137
|
+
writeFileSync(filePath, "not json\n");
|
|
138
|
+
await expect(JsonlSessionStorage.open(env, filePath)).rejects.toThrow("first line is not a valid session header");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("throws for malformed entry lines", async () => {
|
|
142
|
+
const dir = createTempDir();
|
|
143
|
+
const env = new NodeExecutionEnv({ cwd: dir });
|
|
144
|
+
const filePath = join(dir, "session.jsonl");
|
|
145
|
+
const header = {
|
|
146
|
+
type: "session",
|
|
147
|
+
version: 3,
|
|
148
|
+
id: "session-1",
|
|
149
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
150
|
+
cwd: dir,
|
|
151
|
+
};
|
|
152
|
+
const entry: MessageEntry = {
|
|
153
|
+
type: "message",
|
|
154
|
+
id: "entry-1",
|
|
155
|
+
parentId: null,
|
|
156
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
157
|
+
message: createUserMessage("one"),
|
|
158
|
+
};
|
|
159
|
+
writeFileSync(filePath, `${JSON.stringify(header)}\nnot json\n${JSON.stringify(entry)}\n`);
|
|
160
|
+
await expect(JsonlSessionStorage.open(env, filePath)).rejects.toMatchObject({ code: "invalid_entry" });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("creates and reads session metadata from the header", async () => {
|
|
164
|
+
const dir = createTempDir();
|
|
165
|
+
const env = new NodeExecutionEnv({ cwd: dir });
|
|
166
|
+
const filePath = join(dir, "session.jsonl");
|
|
167
|
+
const storage = await JsonlSessionStorage.create(env, filePath, {
|
|
168
|
+
cwd: dir,
|
|
169
|
+
sessionId: "session-1",
|
|
170
|
+
parentSessionPath: "/tmp/parent.jsonl",
|
|
171
|
+
});
|
|
172
|
+
const metadata = await storage.getMetadata();
|
|
173
|
+
expect(metadata).toMatchObject({
|
|
174
|
+
id: "session-1",
|
|
175
|
+
cwd: dir,
|
|
176
|
+
path: filePath,
|
|
177
|
+
parentSessionPath: "/tmp/parent.jsonl",
|
|
178
|
+
});
|
|
179
|
+
await storage.appendEntry({
|
|
180
|
+
type: "message",
|
|
181
|
+
id: "user-1",
|
|
182
|
+
parentId: null,
|
|
183
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
184
|
+
message: createUserMessage("one"),
|
|
185
|
+
});
|
|
186
|
+
expect(await loadJsonlSessionMetadata(env, filePath)).toEqual(metadata);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("loads existing entries and reconstructs leaf", async () => {
|
|
190
|
+
const dir = createTempDir();
|
|
191
|
+
const env = new NodeExecutionEnv({ cwd: dir });
|
|
192
|
+
const filePath = join(dir, "session.jsonl");
|
|
193
|
+
const storage = await JsonlSessionStorage.create(env, filePath, { cwd: dir, sessionId: "session-1" });
|
|
194
|
+
const root: MessageEntry = {
|
|
195
|
+
type: "message",
|
|
196
|
+
id: "root",
|
|
197
|
+
parentId: null,
|
|
198
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
199
|
+
message: createUserMessage("root"),
|
|
200
|
+
};
|
|
201
|
+
const child: MessageEntry = {
|
|
202
|
+
...root,
|
|
203
|
+
id: "child",
|
|
204
|
+
parentId: "root",
|
|
205
|
+
message: createAssistantMessage("child"),
|
|
206
|
+
};
|
|
207
|
+
await storage.appendEntry(root);
|
|
208
|
+
await storage.appendEntry(child);
|
|
209
|
+
const loaded = await JsonlSessionStorage.open(env, filePath);
|
|
210
|
+
expect(await loaded.getLeafId()).toBe("child");
|
|
211
|
+
expect((await loaded.getEntries()).map((entry) => entry.id)).toEqual(["root", "child"]);
|
|
212
|
+
await loaded.setLeafId("root");
|
|
213
|
+
const reloaded = await JsonlSessionStorage.open(env, filePath);
|
|
214
|
+
expect(await reloaded.getLeafId()).toBe("root");
|
|
215
|
+
expect((await reloaded.getEntries()).at(-1)).toMatchObject({ type: "leaf", targetId: "root" });
|
|
216
|
+
expect((await loaded.getPathToRoot("child")).map((entry) => entry.id)).toEqual(["root", "child"]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("finds entries by type", async () => {
|
|
220
|
+
const dir = createTempDir();
|
|
221
|
+
const env = new NodeExecutionEnv({ cwd: dir });
|
|
222
|
+
const filePath = join(dir, "session.jsonl");
|
|
223
|
+
const storage = await JsonlSessionStorage.create(env, filePath, { cwd: dir, sessionId: "session-1" });
|
|
224
|
+
await storage.appendEntry({
|
|
225
|
+
type: "message",
|
|
226
|
+
id: "entry-1",
|
|
227
|
+
parentId: null,
|
|
228
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
229
|
+
message: createUserMessage("one"),
|
|
230
|
+
});
|
|
231
|
+
expect((await storage.findEntries("message")).map((found) => found.id)).toEqual(["entry-1"]);
|
|
232
|
+
expect(await storage.findEntries("session_info")).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("maintains label lookup", async () => {
|
|
236
|
+
const dir = createTempDir();
|
|
237
|
+
const env = new NodeExecutionEnv({ cwd: dir });
|
|
238
|
+
const filePath = join(dir, "session.jsonl");
|
|
239
|
+
const storage = await JsonlSessionStorage.create(env, filePath, { cwd: dir, sessionId: "session-1" });
|
|
240
|
+
await storage.appendEntry({
|
|
241
|
+
type: "message",
|
|
242
|
+
id: "entry-1",
|
|
243
|
+
parentId: null,
|
|
244
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
245
|
+
message: createUserMessage("one"),
|
|
246
|
+
});
|
|
247
|
+
expect(await storage.getLabel("entry-1")).toBeUndefined();
|
|
248
|
+
await storage.appendEntry({
|
|
249
|
+
type: "label",
|
|
250
|
+
id: "label-1",
|
|
251
|
+
parentId: "entry-1",
|
|
252
|
+
timestamp: "2026-01-01T00:00:01.000Z",
|
|
253
|
+
targetId: "entry-1",
|
|
254
|
+
label: "checkpoint",
|
|
255
|
+
});
|
|
256
|
+
expect(await storage.getLabel("entry-1")).toBe("checkpoint");
|
|
257
|
+
await storage.appendEntry({
|
|
258
|
+
type: "label",
|
|
259
|
+
id: "label-2",
|
|
260
|
+
parentId: "label-1",
|
|
261
|
+
timestamp: "2026-01-01T00:00:02.000Z",
|
|
262
|
+
targetId: "entry-1",
|
|
263
|
+
label: undefined,
|
|
264
|
+
});
|
|
265
|
+
expect(await storage.getLabel("entry-1")).toBeUndefined();
|
|
266
|
+
const loaded = await JsonlSessionStorage.open(env, filePath);
|
|
267
|
+
expect(await loaded.getLabel("entry-1")).toBeUndefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("reads session metadata through the line-reading filesystem operation", async () => {
|
|
271
|
+
const dir = createTempDir();
|
|
272
|
+
const filePath = join(dir, "session.jsonl");
|
|
273
|
+
const header = {
|
|
274
|
+
type: "session",
|
|
275
|
+
version: 3,
|
|
276
|
+
id: "session-1",
|
|
277
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
278
|
+
cwd: dir,
|
|
279
|
+
};
|
|
280
|
+
const metadata = await loadJsonlSessionMetadata(
|
|
281
|
+
{
|
|
282
|
+
readTextLines: async () => ok([JSON.stringify(header)]),
|
|
283
|
+
readTextFile: async () => {
|
|
284
|
+
throw new Error("readTextFile should not be called for metadata");
|
|
285
|
+
},
|
|
286
|
+
writeFile: async () => ok(undefined),
|
|
287
|
+
appendFile: async () => ok(undefined),
|
|
288
|
+
},
|
|
289
|
+
filePath,
|
|
290
|
+
);
|
|
291
|
+
expect(metadata).toEqual({
|
|
292
|
+
id: "session-1",
|
|
293
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
294
|
+
cwd: dir,
|
|
295
|
+
path: filePath,
|
|
296
|
+
parentSessionPath: undefined,
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatSkillsForSystemPrompt } from "../../src/harness/system-prompt.ts";
|
|
3
|
+
|
|
4
|
+
const visibleSkill = {
|
|
5
|
+
name: "visible",
|
|
6
|
+
description: "Use <this> & that",
|
|
7
|
+
content: "visible content",
|
|
8
|
+
filePath: "/skills/visible/SKILL.md",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const secondSkill = {
|
|
12
|
+
name: "second",
|
|
13
|
+
description: "Second skill",
|
|
14
|
+
content: "second content",
|
|
15
|
+
filePath: "/skills/second/SKILL.md",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const disabledSkill = {
|
|
19
|
+
name: "hidden",
|
|
20
|
+
description: "Hidden",
|
|
21
|
+
content: "hidden content",
|
|
22
|
+
filePath: "/skills/hidden/SKILL.md",
|
|
23
|
+
disableModelInvocation: true,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("formatSkillsForSystemPrompt", () => {
|
|
27
|
+
it("formats visible skills in order and skips model-disabled skills", () => {
|
|
28
|
+
expect(formatSkillsForSystemPrompt([visibleSkill, disabledSkill, secondSkill])).toBe(
|
|
29
|
+
`The following skills provide specialized instructions for specific tasks.
|
|
30
|
+
Read the full skill file when the task matches its description.
|
|
31
|
+
When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.
|
|
32
|
+
|
|
33
|
+
<available_skills>
|
|
34
|
+
<skill>
|
|
35
|
+
<name>visible</name>
|
|
36
|
+
<description>Use <this> & that</description>
|
|
37
|
+
<location>/skills/visible/SKILL.md</location>
|
|
38
|
+
</skill>
|
|
39
|
+
<skill>
|
|
40
|
+
<name>second</name>
|
|
41
|
+
<description>Second skill</description>
|
|
42
|
+
<location>/skills/second/SKILL.md</location>
|
|
43
|
+
</skill>
|
|
44
|
+
</available_skills>`,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns an empty string when no skills are model-visible", () => {
|
|
49
|
+
expect(formatSkillsForSystemPrompt([disabledSkill])).toBe("");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("escapes XML in all model-visible skill fields", () => {
|
|
53
|
+
expect(
|
|
54
|
+
formatSkillsForSystemPrompt([
|
|
55
|
+
{
|
|
56
|
+
name: "a&b",
|
|
57
|
+
description: `Quote "double" and 'single'`,
|
|
58
|
+
content: "content",
|
|
59
|
+
filePath: '/skills/<bad>&"quote"/SKILL.md',
|
|
60
|
+
},
|
|
61
|
+
]),
|
|
62
|
+
).toContain(
|
|
63
|
+
"<name>a&b</name>\n <description>Quote "double" and 'single'</description>\n <location>/skills/<bad>&"quote"/SKILL.md</location>",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|