@amoree-code/ocean 0.0.0-stage → 1.2.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/README.md +284 -2
- package/dist/application/auth/auth-orchestrator.js +77 -0
- package/dist/application/clients/clients.js +646 -0
- package/dist/application/clients/editors.js +398 -0
- package/dist/application/clients/hooks.js +335 -0
- package/dist/application/clients/native.js +410 -0
- package/dist/application/clients/opencode-plugin.js +158 -0
- package/dist/application/clients/registry.js +188 -0
- package/dist/application/gateway/webhook-gateway.js +137 -0
- package/dist/application/install/provider-installer.js +184 -0
- package/dist/application/integrations/charter-copies.js +76 -0
- package/dist/application/integrations/claude-vscode.js +106 -0
- package/dist/application/integrations/engine-entry-repair.js +105 -0
- package/dist/application/integrations/skill-filter.js +254 -0
- package/dist/application/layout/layout-apply.js +443 -0
- package/dist/application/layout/layout-plan.js +236 -0
- package/dist/application/layout/workspace-template.js +34 -0
- package/dist/application/onboarding/state.js +23 -0
- package/dist/application/onboarding/steps.js +249 -0
- package/dist/application/profiles/profile-distribution.js +53 -0
- package/dist/application/profiles/profile-suggestion.js +39 -0
- package/dist/application/profiles/profile-upgrade-service.js +208 -0
- package/dist/application/pull/history-index.js +476 -0
- package/dist/application/pull/project-match.js +132 -0
- package/dist/application/pull/pull.js +202 -0
- package/dist/application/pull/server-capture.js +307 -0
- package/dist/application/scheduler/local-scheduler.js +210 -0
- package/dist/composition/runtime.js +64 -0
- package/dist/engine-root.js +9 -0
- package/dist/infrastructure/filesystem/profile-loader.js +46 -0
- package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
- package/dist/infrastructure/filesystem/skill-loader.js +84 -0
- package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
- package/dist/infrastructure/observability/runtime-logger.js +16 -0
- package/dist/infrastructure/process/agent-tool-executor.js +99 -0
- package/dist/infrastructure/process/cli-process.js +49 -0
- package/dist/infrastructure/process/interactive-process.js +145 -0
- package/dist/infrastructure/process/process-supervisor.js +194 -0
- package/dist/infrastructure/process/service.js +252 -0
- package/dist/infrastructure/providers/browser-provider.js +2 -0
- package/dist/infrastructure/providers/claude-sdk.js +137 -0
- package/dist/infrastructure/providers/client-home.js +14 -0
- package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
- package/dist/infrastructure/providers/ollama-embedder.js +74 -0
- package/dist/infrastructure/providers/openai-compatible.js +268 -0
- package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
- package/dist/infrastructure/providers/provider-capabilities.js +50 -0
- package/dist/infrastructure/providers/provider-invocation.js +184 -0
- package/dist/infrastructure/providers/provider-registry.js +77 -0
- package/dist/infrastructure/providers/providers.js +134 -0
- package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
- package/dist/interfaces/cli/auth-command.js +54 -0
- package/dist/interfaces/cli/browser-bridge-command.js +93 -0
- package/dist/interfaces/cli/browser-command.js +119 -0
- package/dist/interfaces/cli/capture-command.js +125 -0
- package/dist/interfaces/cli/client-test-command.js +133 -0
- package/dist/interfaces/cli/clients-command.js +129 -0
- package/dist/interfaces/cli/command-registry.js +1211 -0
- package/dist/interfaces/cli/context-command.js +172 -0
- package/dist/interfaces/cli/daily-command.js +131 -0
- package/dist/interfaces/cli/governance-command.js +55 -0
- package/dist/interfaces/cli/handoff-command.js +35 -0
- package/dist/interfaces/cli/help-command.js +207 -0
- package/dist/interfaces/cli/idea-command.js +47 -0
- package/dist/interfaces/cli/intercept-command.js +317 -0
- package/dist/interfaces/cli/layout-command.js +27 -0
- package/dist/interfaces/cli/market-command.js +52 -0
- package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
- package/dist/interfaces/cli/memory-command.js +317 -0
- package/dist/interfaces/cli/migrate-command.js +26 -0
- package/dist/interfaces/cli/observe-command.js +78 -0
- package/dist/interfaces/cli/onboard-command.js +123 -0
- package/dist/interfaces/cli/operate-command.js +59 -0
- package/dist/interfaces/cli/profiles-command.js +70 -0
- package/dist/interfaces/cli/pull-command.js +54 -0
- package/dist/interfaces/cli/setup-command.js +143 -0
- package/dist/interfaces/cli/tasks-command.js +249 -0
- package/dist/main.js +49 -0
- package/dist/node-warnings.js +17 -0
- package/dist/version.js +8 -0
- package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
- package/market/mcp/gateway/ocean.capability.json +21 -0
- package/market/mcp/obsidian/ocean.capability.json +21 -0
- package/market/mcp/ocean/ocean.capability.json +21 -0
- package/market/mcp/playwright/ocean.capability.json +23 -0
- package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
- package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
- package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
- package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
- package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
- package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
- package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
- package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
- package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
- package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
- package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
- package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
- package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
- package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
- package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
- package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
- package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
- package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
- package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
- package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
- package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
- package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
- package/node_modules/@ocean/brain/package.json +11 -0
- package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
- package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
- package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
- package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
- package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
- package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
- package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
- package/node_modules/@ocean/context/package.json +11 -0
- package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
- package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
- package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
- package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
- package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
- package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
- package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
- package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
- package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
- package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
- package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
- package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
- package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
- package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
- package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
- package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
- package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
- package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
- package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
- package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
- package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
- package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
- package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
- package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
- package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
- package/node_modules/@ocean/core/package.json +14 -0
- package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
- package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
- package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
- package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
- package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
- package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
- package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
- package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
- package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
- package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
- package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
- package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
- package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
- package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
- package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
- package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
- package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
- package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
- package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
- package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
- package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
- package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
- package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
- package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
- package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
- package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
- package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
- package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
- package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
- package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
- package/node_modules/@ocean/loop/package.json +11 -0
- package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
- package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
- package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
- package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
- package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
- package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
- package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
- package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
- package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
- package/node_modules/@ocean/market/dist/install/lock.js +34 -0
- package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
- package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
- package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
- package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
- package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
- package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
- package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
- package/node_modules/@ocean/market/package.json +11 -0
- package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
- package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
- package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
- package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
- package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
- package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
- package/node_modules/@ocean/services/package.json +11 -0
- package/ocean-marketplace.json +415 -0
- package/package.json +67 -4
- package/skills/core/business-logic/SKILL.md +22 -0
- package/skills/core/catch-up/SKILL.md +17 -0
- package/skills/core/core-thinking/SKILL.md +11 -0
- package/skills/core/design-thinking/SKILL.md +19 -0
- package/skills/core/graft/SKILL.md +156 -0
- package/skills/core/loop/SKILL.md +20 -0
- package/skills/core/session-handoff/SKILL.md +15 -0
- package/skills/core/use-browser/SKILL.md +69 -0
- package/skills/core/verification/SKILL.md +11 -0
- package/skills/index.json +56 -0
- package/templates/README.md +4 -0
- package/templates/profiles/default.json +51 -0
- package/templates/profiles/developer.json +51 -0
- package/templates/profiles/reviewer.json +51 -0
- package/templates/profiles/strategist.json +51 -0
- package/templates/workspace/00-inbox/INBOX.md +7 -0
- package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
- package/templates/workspace/00-inbox/native/.gitkeep +0 -0
- package/templates/workspace/01-daily/.gitkeep +0 -0
- package/templates/workspace/02-personal/CONFLICTS.md +7 -0
- package/templates/workspace/02-personal/MEMORY.md +15 -0
- package/templates/workspace/02-personal/goals/.gitkeep +0 -0
- package/templates/workspace/02-personal/identity/.gitkeep +0 -0
- package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
- package/templates/workspace/03-professional/.gitkeep +0 -0
- package/templates/workspace/04-projects/PROJECTS.md +5 -0
- package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
- package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
- package/templates/workspace/06-templates/TEMPLATES.md +25 -0
- package/templates/workspace/06-templates/decision.md +8 -0
- package/templates/workspace/06-templates/intelligence.md +32 -0
- package/templates/workspace/06-templates/project-claude.md +48 -0
- package/templates/workspace/06-templates/session.md +38 -0
- package/templates/workspace/06-templates/task.md +42 -0
- package/templates/workspace/MANIFEST.json +51 -0
- package/templates/workspace/bridge/config/CONFIG.md +21 -0
- package/templates/workspace/bridge/config/runtime.json +4 -0
- package/templates/workspace/charter/core.md +104 -0
- package/templates/workspace/charter/policies/README.md +31 -0
- package/templates/workspace/charter/policies/claude-code-context.md +90 -0
- package/templates/workspace/charter/policies/concise-output.md +29 -0
- package/templates/workspace/charter/policies/context.md +91 -0
- package/templates/workspace/charter/policies/contracts/task.md +189 -0
- package/templates/workspace/charter/policies/git.md +60 -0
- package/templates/workspace/charter/policies/graph.md +47 -0
- package/templates/workspace/charter/policies/knowledge.md +33 -0
- package/templates/workspace/charter/policies/lifecycle.md +119 -0
- package/templates/workspace/charter/policies/memory.md +67 -0
- package/templates/workspace/charter/policies/minimal-change.md +43 -0
- package/templates/workspace/charter/policies/models.md +57 -0
- package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
- package/templates/workspace/charter/policies/response.md +39 -0
- package/templates/workspace/charter/policies/skill-observer.md +44 -0
- package/templates/workspace/charter/policies/stack.md +23 -0
- package/templates/workspace/charter/policies/task.md +139 -0
- package/templates/workspace/charter/policies/verification.md +29 -0
- package/templates/workspace/sessions/.gitkeep +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Annotation, END, MemorySaver, START, StateGraph, } from "@langchain/langgraph";
|
|
5
|
+
const IterationState = Annotation.Root({
|
|
6
|
+
loop: Annotation(),
|
|
7
|
+
next: Annotation(),
|
|
8
|
+
ran: Annotation(),
|
|
9
|
+
});
|
|
10
|
+
// Only record is retried in place, and only on a filesystem error: it writes files and is safe
|
|
11
|
+
// to repeat. work (a provider run) and verify (the task's own checks, a test suite say) are
|
|
12
|
+
// never re-run here; the loop's interval and attempt budget decide when they run again. No
|
|
13
|
+
// jitter and no log line: retries must not write to stdout, which carries command JSON.
|
|
14
|
+
const recordRetry = {
|
|
15
|
+
maxAttempts: 3,
|
|
16
|
+
initialInterval: 200,
|
|
17
|
+
jitter: false,
|
|
18
|
+
logWarning: false,
|
|
19
|
+
retryOn: (error) => typeof error?.code === "string",
|
|
20
|
+
};
|
|
21
|
+
export function iterationGraph(steps, checkpointer) {
|
|
22
|
+
return new StateGraph(IterationState)
|
|
23
|
+
.addNode("work", (state) => steps.work(state.loop))
|
|
24
|
+
.addNode("verify", async (state) => ({
|
|
25
|
+
loop: await steps.verify(state.loop),
|
|
26
|
+
}))
|
|
27
|
+
.addNode("record", async (state) => ({ loop: await steps.record(state.loop), ran: true }), { retryPolicy: recordRetry })
|
|
28
|
+
.addEdge(START, "work")
|
|
29
|
+
.addConditionalEdges("work", (state) => (state.next === "verify" ? "verify" : END), ["verify", END])
|
|
30
|
+
.addEdge("verify", "record")
|
|
31
|
+
.addEdge("record", END)
|
|
32
|
+
.compile({ checkpointer });
|
|
33
|
+
}
|
|
34
|
+
// LangChain sends every run to LangSmith when one of these is "true", which a shell set up for
|
|
35
|
+
// another project may well have. It reads them only from process.env, with no per-run switch,
|
|
36
|
+
// so loop state (prompts, task ids, paths) stays on this machine by turning off just the ones
|
|
37
|
+
// that are on; processes the loop starts inherit that.
|
|
38
|
+
export function keepRunsLocal(env = process.env) {
|
|
39
|
+
for (const name of [
|
|
40
|
+
"LANGSMITH_TRACING_V2",
|
|
41
|
+
"LANGCHAIN_TRACING_V2",
|
|
42
|
+
"LANGSMITH_TRACING",
|
|
43
|
+
"LANGCHAIN_TRACING",
|
|
44
|
+
])
|
|
45
|
+
if (env[name] === "true")
|
|
46
|
+
env[name] = "false";
|
|
47
|
+
}
|
|
48
|
+
const bytesKey = "$bytes";
|
|
49
|
+
const safeThreadId = /^[A-Za-z0-9_-]+$/;
|
|
50
|
+
// MemorySaver keeps its maps prototype-free so no key can reach Object.prototype; JSON.parse
|
|
51
|
+
// does not, so rebuild them that way.
|
|
52
|
+
function prototypeFree(value, depth) {
|
|
53
|
+
const out = Object.create(null);
|
|
54
|
+
for (const [key, item] of Object.entries(value))
|
|
55
|
+
out[key] = depth > 0 ? prototypeFree(item, depth - 1) : item;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
// MemorySaver's checkpoints kept on disk, one file per thread (a loop), so a thread resumes after
|
|
59
|
+
// the process is gone and two processes working different loops never overwrite each other.
|
|
60
|
+
// The LangGraph version is pinned exactly because this relies on MemorySaver's typed `storage`
|
|
61
|
+
// and `writes` maps and its `[thread, namespace, checkpoint]` write keys.
|
|
62
|
+
export class FileCheckpointSaver extends MemorySaver {
|
|
63
|
+
dir;
|
|
64
|
+
#saving = new Map();
|
|
65
|
+
constructor(dir) {
|
|
66
|
+
super();
|
|
67
|
+
this.dir = dir;
|
|
68
|
+
}
|
|
69
|
+
static async open(dir) {
|
|
70
|
+
const saver = new FileCheckpointSaver(dir);
|
|
71
|
+
let files = [];
|
|
72
|
+
try {
|
|
73
|
+
files = await readdir(dir);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code !== "ENOENT")
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
for (const file of files.filter((name) => name.endsWith(".json"))) {
|
|
80
|
+
const full = path.join(dir, file);
|
|
81
|
+
try {
|
|
82
|
+
const saved = JSON.parse(await readFile(full, "utf8"), (_key, value) => value &&
|
|
83
|
+
typeof value === "object" &&
|
|
84
|
+
typeof value[bytesKey] === "string" &&
|
|
85
|
+
Object.keys(value).length === 1
|
|
86
|
+
? new Uint8Array(Buffer.from(value[bytesKey], "base64"))
|
|
87
|
+
: value);
|
|
88
|
+
if (!safeThreadId.test(saved.threadId))
|
|
89
|
+
throw new Error("unsafe thread id");
|
|
90
|
+
saver.storage[saved.threadId] = prototypeFree(saved.storage, 1);
|
|
91
|
+
for (const [key, writes] of Object.entries(saved.writes))
|
|
92
|
+
saver.writes[key] = prototypeFree(writes, 0);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// An unreadable thread is set aside rather than stopping every loop; the worst case is
|
|
96
|
+
// that its iteration runs again from the start.
|
|
97
|
+
await rename(full, `${full}.unreadable-${Date.now()}`).catch(() => undefined);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return saver;
|
|
101
|
+
}
|
|
102
|
+
async put(...args) {
|
|
103
|
+
const result = await super.put(...args);
|
|
104
|
+
await this.#save(args[0].configurable?.thread_id);
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
async putWrites(...args) {
|
|
108
|
+
await super.putWrites(...args);
|
|
109
|
+
await this.#save(args[0].configurable?.thread_id);
|
|
110
|
+
}
|
|
111
|
+
async deleteThread(threadId) {
|
|
112
|
+
await super.deleteThread(threadId);
|
|
113
|
+
await this.#save(threadId);
|
|
114
|
+
}
|
|
115
|
+
// One thread's saves run one after another, each writing what is current when it starts; a
|
|
116
|
+
// failed save fails its own caller, not the ones after it.
|
|
117
|
+
#save(threadId) {
|
|
118
|
+
if (typeof threadId !== "string" || !safeThreadId.test(threadId))
|
|
119
|
+
return Promise.reject(new Error(`Unsafe checkpoint thread id: ${String(threadId)}`));
|
|
120
|
+
const file = path.join(this.dir, `${threadId}.json`);
|
|
121
|
+
const run = (this.#saving.get(threadId) ?? Promise.resolve())
|
|
122
|
+
.catch(() => undefined)
|
|
123
|
+
.then(async () => {
|
|
124
|
+
const storage = this.storage[threadId];
|
|
125
|
+
const writes = Object.fromEntries(Object.entries(this.writes).filter(([key]) => JSON.parse(key)[0] === threadId));
|
|
126
|
+
if (!storage && !Object.keys(writes).length) {
|
|
127
|
+
await rm(file, { force: true });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const payload = JSON.stringify({ threadId, storage: storage ?? {}, writes },
|
|
131
|
+
// `this[key]` is the value before toJSON (a Buffer would otherwise arrive as JSON).
|
|
132
|
+
function (key, value) {
|
|
133
|
+
const raw = this[key];
|
|
134
|
+
return raw instanceof Uint8Array
|
|
135
|
+
? { [bytesKey]: Buffer.from(raw).toString("base64") }
|
|
136
|
+
: value;
|
|
137
|
+
});
|
|
138
|
+
await mkdir(this.dir, { recursive: true });
|
|
139
|
+
const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
140
|
+
await writeFile(temp, `${payload}\n`, { mode: 0o600 });
|
|
141
|
+
await rename(temp, file);
|
|
142
|
+
});
|
|
143
|
+
this.#saving.set(threadId, run);
|
|
144
|
+
return run;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=iteration-graph.js.map
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { TASK_ID } from "@ocean/core/domain/tasks/task-id.js";
|
|
5
|
+
import { oceanPath, projectDir, resolveWithin, SYSTEM_DIR, } from "@ocean/core/shared/paths.js";
|
|
6
|
+
import { folderProject } from "@ocean/core/shared/project-resolution.js";
|
|
7
|
+
import { resolveTaskReference, } from "@ocean/core/shared/task-reference.js";
|
|
8
|
+
import { runAgent } from "../runs/run-agent.js";
|
|
9
|
+
import { checkpointTask } from "../tasks/checkpoint-task.js";
|
|
10
|
+
import { verifyTask } from "../tasks/verify-task.js";
|
|
11
|
+
const loopsFile = () => oceanPath(SYSTEM_DIR, "loops.json");
|
|
12
|
+
const checkpointsDir = () => oceanPath(SYSTEM_DIR, "loop-checkpoints");
|
|
13
|
+
export async function listTaskLoops() {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(await readFile(loopsFile(), "utf8"));
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === "ENOENT")
|
|
19
|
+
return [];
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function startTaskLoop(input) {
|
|
24
|
+
if (!input.approved)
|
|
25
|
+
throw new Error("Loop start requires explicit approval.");
|
|
26
|
+
if (!input.taskId || !TASK_ID.test(input.taskId))
|
|
27
|
+
throw new Error("Loop requires a valid task id.");
|
|
28
|
+
const loop = {
|
|
29
|
+
id: `loop-${randomUUID()}`,
|
|
30
|
+
taskId: input.taskId,
|
|
31
|
+
projectId: await folderProject(input.cwd),
|
|
32
|
+
profile: input.profile,
|
|
33
|
+
prompt: input.prompt.trim(),
|
|
34
|
+
cwd: input.cwd,
|
|
35
|
+
status: "active",
|
|
36
|
+
approved: true,
|
|
37
|
+
maxIterations: bounded(input.maxIterations ?? 10, 1, 100),
|
|
38
|
+
intervalMs: bounded(input.intervalMs ?? 60_000, 1_000, 86_400_000),
|
|
39
|
+
maxAttempts: bounded(input.maxAttempts ?? 2, 1, 5),
|
|
40
|
+
iterations: 0,
|
|
41
|
+
failures: 0,
|
|
42
|
+
nextRunAt: new Date().toISOString(),
|
|
43
|
+
lastSessionId: null,
|
|
44
|
+
lastError: null,
|
|
45
|
+
checkpoint: "Loop created; inspect the task and choose the first bounded action.",
|
|
46
|
+
resultPath: null,
|
|
47
|
+
};
|
|
48
|
+
await saveLoops([...(await listTaskLoops()), loop]);
|
|
49
|
+
return loop;
|
|
50
|
+
}
|
|
51
|
+
export async function stopTaskLoop(id) {
|
|
52
|
+
const loops = await listTaskLoops();
|
|
53
|
+
const loop = loops.find((item) => item.id === id);
|
|
54
|
+
if (!loop)
|
|
55
|
+
throw new Error(`Loop not found: ${id}`);
|
|
56
|
+
loop.status = "stopped";
|
|
57
|
+
await saveLoops(loops);
|
|
58
|
+
return loop;
|
|
59
|
+
}
|
|
60
|
+
export async function runTaskLoopsOnce(cwd, runtime, options = {}) {
|
|
61
|
+
// LangGraph loads only when loops actually run, not on every `ocean` command.
|
|
62
|
+
const { FileCheckpointSaver, iterationGraph, keepRunsLocal } = await import("./iteration-graph.js");
|
|
63
|
+
keepRunsLocal();
|
|
64
|
+
const checkpointer = await FileCheckpointSaver.open(checkpointsDir());
|
|
65
|
+
const graph = iterationGraph(iterationSteps(cwd, runtime, options), checkpointer);
|
|
66
|
+
const loops = await listTaskLoops();
|
|
67
|
+
const ran = [];
|
|
68
|
+
for (const loop of loops) {
|
|
69
|
+
const config = { configurable: { thread_id: loop.id } };
|
|
70
|
+
try {
|
|
71
|
+
const saved = await graph.getState(config);
|
|
72
|
+
const reached = saved.values.loop;
|
|
73
|
+
if (reached && ["stopped", "completed", "failed"].includes(loop.status)) {
|
|
74
|
+
// Stopped (or finished) since the process died: its iteration is dropped, not resumed.
|
|
75
|
+
await checkpointer.deleteThread(loop.id);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (reached && saved.next.length === 0) {
|
|
79
|
+
// The iteration finished but its result never reached loops.json: take it, don't redo it.
|
|
80
|
+
Object.assign(loop, reached);
|
|
81
|
+
await saveLoops(loops);
|
|
82
|
+
await checkpointer.deleteThread(loop.id);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// An iteration the process died in resumes at its step; a paused loop keeps it for later.
|
|
86
|
+
const interrupted = saved.next.length > 0;
|
|
87
|
+
if (loop.status !== "active")
|
|
88
|
+
continue;
|
|
89
|
+
if (!interrupted && Date.parse(loop.nextRunAt) > Date.now())
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
// "sync": a step's checkpoint is on disk before the next step starts, so a crash in
|
|
93
|
+
// verify never re-runs the agent session.
|
|
94
|
+
const result = await graph.invoke(interrupted ? null : { loop: { ...loop }, ran: false }, { ...config, durability: "sync" });
|
|
95
|
+
Object.assign(loop, result.loop);
|
|
96
|
+
if (result.ran)
|
|
97
|
+
ran.push(loop.id);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
// A step that fails (record: after its retries) is one failed attempt of the loop.
|
|
101
|
+
const failed = (await graph.getState(config)).values.loop;
|
|
102
|
+
if (failed)
|
|
103
|
+
Object.assign(loop, failed);
|
|
104
|
+
recordFailure(loop, error);
|
|
105
|
+
}
|
|
106
|
+
// The record first, then the checkpoint: a crash between the two leaves a finished
|
|
107
|
+
// thread, which the next pass takes instead of running again.
|
|
108
|
+
await saveLoops(loops);
|
|
109
|
+
await checkpointer.deleteThread(loop.id);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
// Checkpoint storage failing for one loop is that loop's failed attempt, not the pass's.
|
|
113
|
+
recordFailure(loop, error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await saveLoops(loops);
|
|
117
|
+
return ran;
|
|
118
|
+
}
|
|
119
|
+
function recordFailure(loop, error) {
|
|
120
|
+
loop.failures += 1;
|
|
121
|
+
loop.lastError = error instanceof Error ? error.message : String(error);
|
|
122
|
+
loop.nextRunAt = new Date(Date.now() + loop.intervalMs).toISOString();
|
|
123
|
+
if (loop.failures >= loop.maxAttempts)
|
|
124
|
+
loop.status = "failed";
|
|
125
|
+
}
|
|
126
|
+
// The loop's policy, step by step. Each step takes the record as the graph checkpointed it and
|
|
127
|
+
// returns the next one; none of them mutates its input, so a resumed step starts clean.
|
|
128
|
+
function iterationSteps(cwd, runtime, options) {
|
|
129
|
+
return {
|
|
130
|
+
async work(current) {
|
|
131
|
+
const loop = { ...current };
|
|
132
|
+
if (loop.iterations >= loop.maxIterations) {
|
|
133
|
+
loop.status = "failed";
|
|
134
|
+
loop.lastError = "iteration budget exhausted";
|
|
135
|
+
return { loop, next: "end" };
|
|
136
|
+
}
|
|
137
|
+
const sessionId = randomUUID();
|
|
138
|
+
loop.iterations += 1;
|
|
139
|
+
loop.lastSessionId = sessionId;
|
|
140
|
+
try {
|
|
141
|
+
const session = await runAgent({
|
|
142
|
+
sessionId,
|
|
143
|
+
profileName: loop.profile,
|
|
144
|
+
prompt: `${loop.prompt}\n\nLoop checkpoint: ${loop.checkpoint}\nWork only on task ${loop.taskId}. Complete one bounded action, verify it, and record the next checkpoint.`,
|
|
145
|
+
cwd: loop.cwd || cwd,
|
|
146
|
+
taskId: loop.taskId,
|
|
147
|
+
runContract: {
|
|
148
|
+
runId: loop.id,
|
|
149
|
+
sessionId,
|
|
150
|
+
profile: loop.profile,
|
|
151
|
+
workingDirectory: loop.cwd || cwd,
|
|
152
|
+
allowedTools: [],
|
|
153
|
+
deniedTools: [],
|
|
154
|
+
stopConditions: [
|
|
155
|
+
"task complete",
|
|
156
|
+
"missing input",
|
|
157
|
+
"approval required",
|
|
158
|
+
"verification failure",
|
|
159
|
+
"budget exhausted",
|
|
160
|
+
],
|
|
161
|
+
approval: { required: true, approved: loop.approved },
|
|
162
|
+
budget: {
|
|
163
|
+
timeoutMs: loop.intervalMs,
|
|
164
|
+
maxAttempts: loop.maxAttempts,
|
|
165
|
+
maxOutputBytes: 1_000_000,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}, runtime);
|
|
169
|
+
if (session.status !== "completed")
|
|
170
|
+
throw new Error(`session ${session.status}`);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
recordFailure(loop, error);
|
|
174
|
+
return { loop, next: "end" };
|
|
175
|
+
}
|
|
176
|
+
loop.checkpoint = `Iteration ${loop.iterations} completed; inspect task ${loop.taskId} before continuing.`;
|
|
177
|
+
loop.lastError = null;
|
|
178
|
+
loop.nextRunAt = new Date(Date.now() + loop.intervalMs).toISOString();
|
|
179
|
+
return { loop, next: "verify" };
|
|
180
|
+
},
|
|
181
|
+
// A task is only complete when it claims done AND an independent verifier agrees. A
|
|
182
|
+
// self-reported `state: done` whose checks fail keeps the loop active and records why.
|
|
183
|
+
async verify(current) {
|
|
184
|
+
const loop = { ...current };
|
|
185
|
+
const task = await loopTask(loop, cwd);
|
|
186
|
+
if (await taskClaimsDone(task.file)) {
|
|
187
|
+
const outcome = await verifyTask(task.id, tasksRootOf(task), {
|
|
188
|
+
cwd: loop.cwd || cwd,
|
|
189
|
+
run: options.verifyRun,
|
|
190
|
+
});
|
|
191
|
+
if (outcome.passed)
|
|
192
|
+
loop.status = "completed";
|
|
193
|
+
else
|
|
194
|
+
loop.lastError = `not complete: ${outcome.reason}`;
|
|
195
|
+
}
|
|
196
|
+
return loop;
|
|
197
|
+
},
|
|
198
|
+
// Durable per-step checkpoint: survives a cold restart because it is written into the task
|
|
199
|
+
// file, not just held in loops.json. The result file (an idempotent overwrite) goes first
|
|
200
|
+
// and the task note (an append that never throws) last, so a retried record adds no
|
|
201
|
+
// duplicate note.
|
|
202
|
+
async record(current) {
|
|
203
|
+
const loop = { ...current };
|
|
204
|
+
const sessionId = loop.lastSessionId;
|
|
205
|
+
if (!sessionId)
|
|
206
|
+
throw new Error("record step reached without a session");
|
|
207
|
+
const task = await loopTask(loop, cwd);
|
|
208
|
+
loop.resultPath = await persistWorkerResult(loop, sessionId, "completed");
|
|
209
|
+
await recordStepCheckpoint(loop, task, tasksRootOf(task), "completed");
|
|
210
|
+
return loop;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// The loop's task, found where it lives now (renumbered or archived tasks included).
|
|
215
|
+
function loopTask(loop, cwd) {
|
|
216
|
+
return resolveTaskReference(loop.taskId, {
|
|
217
|
+
projectId: loop.projectId ?? undefined,
|
|
218
|
+
cwd: loop.projectId ? undefined : loop.cwd || cwd,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const tasksRootOf = (task) => oceanPath(projectDir(task.projectId), "tasks");
|
|
222
|
+
async function recordStepCheckpoint(loop, task, root, status) {
|
|
223
|
+
try {
|
|
224
|
+
await checkpointTask(task.id, root, {
|
|
225
|
+
note: `loop ${loop.id} iteration ${loop.iterations}: session ${status}; ${loop.lastError ?? "verified progress"}`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// The task file may live under a non-default project; a missing file must
|
|
230
|
+
// not crash the loop. Loop-level state in loops.json still persists.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function persistWorkerResult(loop, sessionId, status) {
|
|
234
|
+
const payload = JSON.stringify({
|
|
235
|
+
loopId: loop.id,
|
|
236
|
+
taskId: loop.taskId,
|
|
237
|
+
iteration: loop.iterations,
|
|
238
|
+
sessionId,
|
|
239
|
+
status,
|
|
240
|
+
checkpoint: loop.checkpoint,
|
|
241
|
+
lastError: loop.lastError,
|
|
242
|
+
recordedAt: new Date().toISOString(),
|
|
243
|
+
}, null, 2);
|
|
244
|
+
const dir = oceanPath(SYSTEM_DIR, "runs");
|
|
245
|
+
await mkdir(dir, { recursive: true });
|
|
246
|
+
const target = resolveWithin(dir, `${sessionId}.json`);
|
|
247
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
248
|
+
await writeFile(temp, `${payload}\n`, { mode: 0o600 });
|
|
249
|
+
await rename(temp, target);
|
|
250
|
+
return target;
|
|
251
|
+
}
|
|
252
|
+
export async function runTaskLoopWorker(cwd, runtime, options = {}) {
|
|
253
|
+
const pollMs = options.pollMs ?? 30_000;
|
|
254
|
+
while (!options.signal?.aborted) {
|
|
255
|
+
await runTaskLoopsOnce(cwd, runtime);
|
|
256
|
+
await new Promise((resolve) => {
|
|
257
|
+
const timer = setTimeout(resolve, pollMs);
|
|
258
|
+
options.signal?.addEventListener("abort", () => {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
resolve();
|
|
261
|
+
}, { once: true });
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function taskClaimsDone(file) {
|
|
266
|
+
const source = await readFile(file, "utf8");
|
|
267
|
+
return /^state:\s*done\s*$/m.test(source);
|
|
268
|
+
}
|
|
269
|
+
async function saveLoops(loops) {
|
|
270
|
+
const target = loopsFile();
|
|
271
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
272
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
273
|
+
await writeFile(temp, `${JSON.stringify(loops, null, 2)}\n`, { mode: 0o600 });
|
|
274
|
+
await rename(temp, target);
|
|
275
|
+
}
|
|
276
|
+
function bounded(value, min, max) {
|
|
277
|
+
if (!Number.isInteger(value) || value < min || value > max)
|
|
278
|
+
throw new Error(`Value must be an integer between ${min} and ${max}.`);
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=task-loop.js.map
|